Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit EMA Page #26644

Closed
57 tasks done
fmontes opened this issue Nov 9, 2023 · 2 comments
Closed
57 tasks done

Edit EMA Page #26644

fmontes opened this issue Nov 9, 2023 · 2 comments

Comments

@fmontes
Copy link
Member

fmontes commented Nov 9, 2023

Due Date

1/22/2024

Parent Issue

No response

User Story

As a developer, I want to be able to edit the pages of my nextjs app with the dotCMS page builder.

Acceptance Criteria

  1. The user should be able to add content
  2. The user should be able to edit content
  3. The user should be able to move content from one container to another by drag and drop
  4. The user should be able to update the page properties
  5. The user should be able to publish the page

Proposed Objective

Core Features

Proposed Priority

Priority 2 - Important

External Links... Slack Conversations, Support Tickets, Figma Designs, etc.

N/A

Assumptions & Initiation Needs

Even tho we have edit mode right now, the approach wasn't designed to edit pages created with modern JavaScript frameworks.

Our current approach is too intrusive because we inject HTML, CSS, and JavaScript to the pages to get the tools (buttons) and drag and drop functionality.

Injecting code into a page managed by a JavaScript framework can cause conflicts. HTML injections can disrupt the virtual DOM, leading to render issues, CSS injections may break styles and layouts, and JavaScript injections can interfere with event handling and component lifecycle, potentially breaking page functionality. This leads to compatibility problems with the framework's expected operations.

This is why we need to work on a solution that is less intrusive and that doesn't require injecting code on runtime.


⚠️ Important
We are not redoing edit mode from the ground up, we are creating a new version by reusing as much all the components and services of the current version.


Quality Assurance Notes & Workarounds

N/A

Sub-Tasks & Estimates

N/A

Tasks

  1. OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : Task
    zJaaal
  2. Priority : 3 Average Team : Lunik Team : UX
    fmontes
  3. OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    fmontes
  4. QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  5. Team : Lunik Type : New Functionality
    zJaaal
  6. QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  7. 2 of 2
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    fmontes
  8. QA : Not Needed Team : Lunik Type : Task
    zJaaal
  9. Team : Lunik Type : New Functionality
    zJaaal
  10. 3 of 3
    OKR : Core Features Priority : 3 Average Team : Lunik Type : New Functionality
    zJaaal
  11. 3 of 3
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    fmontes zJaaal
  12. OKR : Core Features Priority : 3 Average Team : Lunik
    fmontes
  13. Team : Lunik Type : Task
    zJaaal
  14. QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  15. 2 of 4
    OKR : Core Features Priority : 3 Average Team : Lunik Type : New Functionality
    zJaaal
  16. 2 of 2
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    fmontes
  17. 2 of 2
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  18. 2 of 2
    OKR : Core Features Priority : 3 Average Team : Lunik Type : New Functionality
    zJaaal
  19. 3 of 3
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  20. 1 of 1
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  21. 0 of 3
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    fmontes
  22. 4 of 4
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  23. 6 of 6
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  24. OKR : User Experience Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  25. 6 of 6
    OKR : Core Features Priority : 3 Average Team : Lunik Type : New Functionality
    KevinDavilaDotCMS
  26. 0 of 3
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    KevinDavilaDotCMS
  27. OKR : Core Features Priority : 3 Average Team : Lunik Type : New Functionality
    zJaaal
  28. 5 of 5
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    fmontes
  29. 3 of 3
    OKR : Core Features Priority : 3 Average Team : Lunik Type : New Functionality
    zJaaal
  30. 0 of 7
    OKR : Core Features Priority : 3 Average Team : Lunik Type : New Functionality
    fmontes
  31. 3 of 3
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    fmontes
  32. OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  33. 0 of 3
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    KevinDavilaDotCMS
  34. 0 of 2
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    KevinDavilaDotCMS
  35. OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    rjvelazco
  36. 0 of 3
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    KevinDavilaDotCMS
  37. OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    KevinDavilaDotCMS
  38. 6 of 6
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  39. OKR : User Experience Priority : 2 High QA : Not Needed Team : Lunik Type : Task
    KevinDavilaDotCMS
  40. 4 of 4
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    zJaaal
  41. OKR : Core Features Priority : 3 Average QA : Approved Release : 24.03.1 Team : Lunik Type : Defect
    jdotcms
  42. OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : Defect
    KevinDavilaDotCMS
  43. OKR : Core Features QA : Not Needed Team : Lunik Type : Task
    fmontes
  44. 4 of 5
    OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : New Functionality
    fmontes
  45. OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : Defect
    fmontes
  46. 0 of 2
    Priority : 3 Average QA : Approved Release : 24.03.1 Team : Lunik Type : Task
    KevinDavilaDotCMS rjvelazco
  47. 0 of 3
    OKR : Core Features Priority : 3 Average QA : Approved Release : 24.03.22 Team : Lunik Type : New Functionality
    rjvelazco
  48. 0 of 3
    OKR : Core Features Priority : 3 Average QA : Approved Release : 24.03.1 Team : Lunik Type : New Functionality
    KevinDavilaDotCMS rjvelazco
  49. OKR : Core Features Priority : 2 High QA : Not Needed Team : Lunik Type : Task
    fmontes
  50. OKR : Core Features Priority : 3 Average Team : Lunik Type : New Functionality
    fmontes
  51. Team : Lunik Type : New Functionality
  52. Type : New Functionality dotCMS : Content Management
    jcastro-dotcms
  53. OKR : Core Features Priority : 3 Average Team : Lunik Type : New Functionality
    fmontes
  54. QA : Not Needed Team : Lunik Type : Task
    zJaaal
  55. OKR : Technical User Experience Priority : 2 High QA : Approved Release : 24.03.22 Team : Lunik Type : Defect
    zJaaal
  56. OKR : User Experience Priority : 3 Average QA : Not Needed Team : Lunik Triage Type : Task
    rjvelazco
  57. OKR : Core Features Priority : 3 Average QA : Not Needed Team : Lunik Type : Task
    fmontes
@fmontes fmontes changed the title Edit EMA Page [EPIC]: Edit EMA Page Nov 9, 2023
@fmontes
Copy link
Member Author

fmontes commented Nov 9, 2023

Edit mode for external pages

Author(s): @fmontes

Status: [Working]

Last Updated: 2023-11-09

Objective

Enhance dotCMS edit mode to better integrate with pages rendered by modern frontend frameworks, improving developer implementation and the editing experience for marketers and content creators.

Goals

  • Provide a simplified, robust solution for edit mode integration with modern JavaScript frameworks.
  • Enhance UX for marketers and content creators editing pages rendered headlessly.

Non-Goals

  • This document does not aim to address backend changes, performance optimization of non-edit mode features, or non-headless CMS rendering enhancements. In short, current edit mode stays the same.

Background

Originally, dotCMS was tailored for page rendering using VTL and containers, and thus the edit mode was designed accordingly. With the advent of JamStack and the demand for headless CMS, dotCMS allowed external page editing but the implementation is complex and provides a subpar experience for both developers and editors.

Current Problems

For developers

  • Creation of a tunnel to localhost is required.
  • Lack of robust libraries for HTML markup integration.
  • Insufficient examples and documentation.
  • Unpredictable and erratic behavior.

For marketers and content creator

  • Slow performance.
  • Lack of reliability.

For us

  • Improvement needed in state management.
  • Confusing communication protocols with the iframe.
  • Necessity for individual client support.
  • Documentation improvements required.

Overview

Introduce a simplified edit mode that doesn't rely on runtime HTML, CSS, and JavaScript injection.

Injecting code into a page managed by a JavaScript framework can cause conflicts. HTML injections can disrupt the virtual DOM, leading to render issues, CSS injections may break styles and layouts, and JavaScript injections can interfere with event handling and component lifecycle, potentially breaking page functionality. This leads to compatibility problems with the framework's expected operations.

The new approach is very simple:

  1. dotCMS Iframe directly loads the developer's localhost (eliminating the need for a tunnel).
  2. The developer’s app sends a postMessage to window.parent.
  3. dotCMS window receives the message and executes the corresponding action.
  4. dotCMS reloads the iframe quickly due to direct localhost loading.
image

Detailed Design

The following four components require development:

Edit mode

We have the Angular-based page builder at /edit-page it lives in this component:

apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content

We can implement the new solution in this component behind a feature flag.

Add, edit, delete content

When a event from the iframe is emmited the edit mode will be poping the dialog to work with the content, the same way we do it right now in the current edit mode.

After the user finish with it dialog, we need to reload the iframe so the customer app reflects those changes.

Customer nextjs app

We're going to provide a starter of this. It will be a simple app that will be rendering pages from demo.dotcms.com and it will implement the dotcms react library.

A React library

We provide that customer will need to implement into their nextjs app.

This library will contain:

  1. A react component to render a page from the page api
  2. A way to show the mark up needed only in edit mode
  3. A way to pass custom components to render the differents content types in the page
  4. It will have the buttons in the containers and contentlets to add, edit, remove content
  5. It will emit the events to the window.parent when the user click those buttons
  6. For now it will be react, but take into account we need that can be use with any frameworks.

Solution 1

Frontend

We can implement the feature flag to show/hide old/new iframe here: core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content.component.html:80

For the new version:

🚫 We don't need to load the code in the iframe.

🚫 We don't need to subscribe to ng-event, because we are using postMessage only.

🚫 We don't need to subscribe to the iframe actions because new iframe will use postMessage.

🚫 We don't need to do drag and drop events here because will be moving that to the customer webapp.

✅ We do need to the iframe overlay, so our new iframe should have an olverlay as well because some controls that show on top of the iframe needs to react to iframe clicks.

private subscribeOverlayService(): void {
this.iframeOverlayService.overlay
.pipe(takeUntil(this.destroy$))
.subscribe((val: boolean) => (this.showOverlay = val));
}

✅ We need to reload the page when the site change.

private subscribeSwitchSite(): void {
this.siteService.switchSite$.pipe(skip(1), takeUntil(this.destroy$)).subscribe(() => {
this.reload(null);
});
}

✅ We need the pageState$ because part of this object needs to be passed to all the tools components.

✅ We need to set allowed contents for the palette

private setAllowedContent(pageState: DotPageRenderState): void {
const CONTENT_HIDDEN_KEY = 'CONTENT_PALETTE_HIDDEN_CONTENT_TYPES';
this.dotConfigurationService
.getKeyAsList(CONTENT_HIDDEN_KEY)
.pipe(take(1))
.subscribe((results) => {
this.allowedContent = this.filterAllowedContentTypes(results, pageState) || [];
});
}

⁉️ How do we get the SEO metatags from the nextjs app? Maybe we postMessage this from nextjs?

this.dotEditContentHtmlService.renderPage(pageState, this.iframe)?.then(() => {
this.seoOGTags = this.dotEditContentHtmlService.getMetaTags();
this.seoOGTagsResults = this.dotEditContentHtmlService.getMetaTagsResults();
});

⁉️ We need to get events

this.customEventsHandler = {
'remote-render-edit': ({ pathname }) => {
this.dotRouterService.goToEditPage({ url: pathname.slice(1) });
},
'load-edit-mode-page': (pageRendered: DotPageRender) => {
/*
This is the events that gets emitted from the backend when the user
browse from the page internal links
*/
const dotRenderedPageState = new DotPageRenderState(
this.pageStateInternal.user,
pageRendered
);
this.variantData = null; // internal navigation, reset variant data - leave experiments.
if (this.isInternallyNavigatingToSamePage(pageRendered.page.pageURI)) {
this.dotPageStateService.setLocalState(dotRenderedPageState);
} else {
this.dotPageStateService.setInternalNavigationState(dotRenderedPageState);
this.dotRouterService.goToEditPage({ url: pageRendered.page.pageURI });
}
},
'in-iframe': () => {
this.reload(null);
},
'reorder-menu': (reorderMenuUrl: string) => {
this.reorderMenuUrl = reorderMenuUrl;
},
'save-menu-order': () => {
this.reorderMenuUrl = '';
this.reload(null);
},
'error-saving-menu-order': () => {
this.reorderMenuUrl = '';
this.dotGlobalMessageService.error(
this.dotMessageService.get('an-unexpected-system-error-occurred')
);
},
'cancel-save-menu-order': () => {
this.reorderMenuUrl = '';
this.reload(null);
},
'edit-block-editor': (element) => {
this.dotEventsService.notify(EDIT_BLOCK_EDITOR_CUSTOM_EVENT, element);
}
};
}

Event list:

Events from the buttons on containers and contentlets:

  • code opens <dot-edit-contentlet /> edit a vtl inside a contenetlet
  • edit opens <dot-edit-contentlet /> edit a contentlet
  • remove fire a primeng confirmation remove a contentlet
  • add opens <dot-add-contentlet /> or <dot-form-selector /> add a contentlet or form to a container

Events from edit/add contentlet dialogs:

  • select from <dot-add-contentlet /> to add a contentlet to a container
  • add-content when drop from palette opens <dot-create-contentlet /> to add a contentlet
  • save (only on EMA remoteRender): from all components when trigger workflow action on jsp

All this events are handle here:

.pipe(takeUntil(this.destroy$))
.subscribe((contentletEvent: DotIframeEditEvent) => {
this.ngZone.run(() => {
this.iframeActionsHandler(contentletEvent.name)(contentletEvent);
});
});
}
private setInitalData(): void {

Events from the code we inject in the iframe:

  • reorder triggers a savePage when the user drags and drops a contentlet on the same container, as reordering them
  • relocate triggers a re render when the user drags and drops a contentlet in another container, as relocating it.
  • handle-http-error calls the dotHttpErrorManagerService with the HTTPError
  • add-contentlet opens the <dot-add-contentlet /> when the user drags and drop a content type that is not form
  • add-form opens the <dot-form-selector /> when the user drags and drop the form content type
  • add-content triggers an iframe action to add the content to the model and iframe dom
  • add-uploaded-dotAsset triggers a re render when the user drags and drops an asset from the SO file browser

All this events are hold here:

private handlerContentletEvents(
event: string
): (contentletEvent: DotPageContent | DotRelocatePayload) => void {
const contentletEventsMap = {
// When an user create or edit a contentlet from the jsp
save: (contentlet: DotPageContent) => {
/*
* The Save event is triggered when the user edit a contentlet or edit the vtl code from the jsp.
* When the user edit the vtl code from the jsp the data sent is the vtl code information.
*/
const contentletEdited = this.isEditAction() ? contentlet : this.currentContentlet;
if (this.currentAction === DotContentletAction.ADD) {
this.renderAddedContentlet(contentlet);
} else {
if (this.updateContentletInode) {
this.currentContentlet.inode = contentlet.inode;
}
// because: https://github.com/dotCMS/core/issues/21818
setTimeout(() => {
this.renderEditedContentlet(contentletEdited || this.currentContentlet);
}, 1800);
}
},
showCopyModal: (data: DotShowCopyModal) => {
const { contentlet, container, initEdit, selector } = data;
this.showCopyModal(contentlet, container).subscribe((contentlet) => {
const element = (
selector ? contentlet.querySelector(selector) : contentlet
) as HTMLElement;
initEdit(element);
});
},
inlineEdit: (data: DotInlineEditContent) => {
const { eventType: type } = data;
if (type === 'focus') {
this.handleTinyMCEOnFocusEvent(data);
}
if (type === 'blur') {
this.handleTinyMCEOnBlurEvent(data);
}
},
// When a user select a content from the search jsp
select: (contentlet: DotPageContent) => {
this.renderAddedContentlet(contentlet);
this.iframeActions$.next({
name: 'select'
});
},
// When a user drag and drop a contentlet in the anohter container in the iframe
relocate: (relocateInfo: DotRelocatePayload) => {
if (!this.remoteRendered) {
this.renderRelocatedContentlet(relocateInfo);
}
},
// When a user drag and drop a contentlet in the same container in the iframe
reorder: (model: DotPageContainer[]) => {
this.savePage(model)
.pipe(take(1))
.subscribe(() => {
this.pageModel$.next({
type: PageModelChangeEventType.MOVE_CONTENT,
model
});
});
},
'deleted-contenlet': () => {
this.removeCurrentContentlet();
},
'add-uploaded-dotAsset': (dotAssetData: DotAssetPayload) => {
this.renderAddedContentlet(dotAssetData.contentlet, true);
},
'add-content': (data: DotAddContentTypePayload) => {
this.iframeActions$.next({
name: 'add-content',
data: data
});
},
'add-contentlet': (dotAssetData: DotAssetPayload) => {
this.renderAddedContentlet(dotAssetData.contentlet, true);
},
'add-form': (formId: string) => {
this.renderAddedForm(formId, true);
},
'handle-http-error': (err: HttpErrorResponse) => {
this.dotHttpErrorManagerService.handle(err).pipe(take(1)).subscribe();
}
};
return contentletEventsMap[event];
}

All this events are handle here.

Note that in the string we use to inject the code, we make use of RxJS to trigger the events. Because, the handler is a RxJS Subject.

Solution 2

Angular

Create a new path in our Angular routing /edit-ema, to pass URL pages as query parameters, akin to the current approach.

Ex. https://demo.dotcms.com/#/edit-ema/content?url=%2Ftestpage&language_id=N

Then we can add an iframe with the src combining the customer's URL and the dotcms path:

Ex. http://localhost:3000/testpage?language_id=1

Where /testpage?language_id=1 comes from the Angular routing queryparams.

Other queryparams we can have:

  • persona
  • variant

These parameters can be passed to the page API.

This approach facilitate direct page editing in our editor from the developer's localhost, no tunnels or hacks.

1️⃣ Load the page
Obtain page details (title, path, identifier, etc.) using the page API:

  • Method: GET
  • URL: https://demo.dotcms.com/api/v1/page/json/path?language_id=N

Store this information in Angular's state for use during editing.

  1. Event Listener:

2️⃣ Event Listener
Use window.postMessage for minimal intrusion. Angular will listen for events like:

  • add load <dot-add-contentlet /> or <dot-form-selector />
  • edit load <dot-edit-contentlet />
  • remove primeng confirmation
  • relocate || drop reload the page

This events will be posted by the customer webapp (we provide a library to facilitate).

3️⃣ Save

After any action that add, delete or relocate a contentlet within the page we need to save the page object to the Page API.

The request:

  • Url: /api/v1/page/{pageId}/content?variantName={variantName}
  • Method: POST
  • Payload:
[
    {
        "identifier": "{containerA-Id}",
        "uuid": "{containerA-UUID}",
        "contentletsId": [
            "{contentletA-ID}"
        ]
    },
    {
        "identifier": "{containerB-Id}",
        "uuid": "{containerB-UUID}",
        "contentletsId": [
            "contentletB-ID",
            "contentletC-ID"
        ]
    }
]

So the payload is a list of containers instances (UUID) with all the contentlets inside, that the way the page API know where each contentlet, form or widget goes.

To get all the containers and contentlets ids, we have two ways:

  1. We send it in the event from the customer app (dotcms library)

  2. We do a request in Angular to refresh those in memory

I feel the option 1 is better.

For actions like edit a contentlet, we just need to reload the iframe.

For action like edit the page contentlet (title, url, etc) we need to reload the iframe but also update the page state in Angular.

Nextjs

⚠️ If you are not familiar with Nextjs take a look at the doc.

Next.js will render pages, sourcing data from dotCMS via the PageAPI. Customizations include:

1️⃣ Routing

Implement dynamic routing which allow us to have one nextjs file to render all dotCMS pages. In short dotCMS provide the routes.

2️⃣ Fetching the page

Use the "slug" that we can use to fetch the page from the dotCMS page API.

Pass URL query parameters (language_id, variant, persona) to the page API.

3️⃣ Multilang

Nextjs have a i18n functionality, we need research how will fit our needs.

The JS Library

The library simplifies dotCMS page rendering and editor communication for content editing actions.

3️⃣ Components

  • <DotcmsPage /> take the page response and render the page
  • <Row /> to render each row
  • <Column /> to render each column
  • <Container /> to render a container and add UI tooling, like add contentlets
  • <Contentlet /> to render a contentlet and add UI tolling, like: edit, remove and relocate.

4️⃣ React Context
Use react context to avoid prop drilling.

E.g. Page > Row > Columns > Containers > Contentlets.

  1. Styling: Employ pure CSS for styling to avoid conflicts with customer frameworks. Utilize CSS grids for rows and columns.

5️⃣ Styles

Employ pure CSS for styling to avoid conflicts with customer frameworks. Utilize CSS grids for rows and columns.

Create 12 cols grid and use grid-column-start and grid-column-end.

6️⃣ UI tooling

Encapsulate all buttons and styles using react-shadow to prevent CSS specificity conflicts.

7️⃣ Event system

We need to postMessage to the parent window with any of the actions a content editor do and include the information needed to perform the action, like the contentlet and/or container object:

  • edit a contentlent or vtl file
  • remove a contentlet, widget or form
  • add a contentlet, widget or form (new or existing)
  • drop a contentlet, widget or form from the palette
  • drop-image: from the SO into the page
  • relocate a contentlet, widget or form from one container to another

State Management

E.g of a state:

interface state {
  language_id: number;
  persona_id: string;
  currentPath: string;
  mode: 'edit' | 'preview' | 'loading';
  previewSelected: string;
  action: 'add' | 'edit' | 'delete' | 'bookmark' | 'worklow';
  pageState: any;
}

Considerations

  • We need loading indicators on long tasks.
  • Internal link handling strategies.
  • Drag and drop functionalities for content reordering.
  • Navigation and sorting order management.
  • Inline editing support requirements.
  • We need to support multilanguage nextjs sites
  • On edit we need to show the dialog for "this" or "all the contentlet"

Metrics

  • Speed and responsiveness of user interactions in edit mode.
  • Simplification of the edit mode implementation process
  • Loading speed
  • Refresh speed
  • Time to implement from zero to running

github-merge-queue bot pushed a commit that referenced this issue Jan 11, 2024
)

* dev: allow wigdets drag and drop in any container

* clean up
@damen-dotcms damen-dotcms changed the title [EPIC]: Edit EMA Page Edit EMA Page Jan 30, 2024
@fmontes
Copy link
Member Author

fmontes commented Feb 9, 2024

Closing and we starting next: #27546

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

No branches or pull requests

2 participants