Developing Custom Editors using existing Editors

Hello,

i am currently debugging a custom Editor for the Inspector that is using already existing editors. Here is a mockup of the component in question:

import React, { useState, useEffect } from 'react'
import axios from 'axios';
import { SelectBox } from '@neos-project/neos-ui-editors'
import { useSelector } from 'react-redux';
import { selectors } from '@neos-project/neos-ui-redux-store';

export default function SchedulerExceptions(props) {
	const currentNode = useSelector(selectors.CR.Nodes.documentNodeSelector)
	const site = currentNode.contextPath.split("/")[2]


	const [publishedNewsletters, setPublishedNewsletters] = useState([])
	const [selectedNewsletters, setSelectedNewsletters] = useState(props.value.map(newsletter => newsletter.node_id))
	const [exceptionsNewsletters, setExceptionsNewsletters] = useState([])
	const [isLoading, setIsLoading] = useState(false)
	const selectBoxOptions = {
		multiple: true,
		allowEmpty: true,
		values: publishedNewsletters,
		placeholder: 'Weitere Newsletter wählen'
	}

	const handleCommit = newsletters => {
		props.commit(newsletters)
	}

	function newsletterFormater (newsletter) {
		return newsletter.map(el => ({
			value: el.node_id,
			label: el.title,
			group: el.group
		}))
	}
	useEffect(() => {
		const getPublishedNewsletters = async () => {
			setIsLoading(true)
			//const response = await axios.get("/api/v1/nodes/sending/" + site)
            //Mockup for API-Data
            const response = {
                data: [
                    {
                        "group": "Group 1",
                        "node_id": "581d7a77-9104-4947-bacc-743c0cbb9ee3",
                        "title": "Newsletter 1",
                        "nextSendingDates": [
                            {
                                "date": "2024-04-18T00:00:00+02:00"
                            }
                        ]
                    },
                    {
                        "group": "Group 2",
                        "node_id": "73508c7b-452c-40a0-bf53-ebf6e5a5fea5",
                        "title": "Newsletter 2",
                        "nextSendingDates": []
                    },
                    {
                        "group": "Group 3",
                        "node_id": "fd3c2398-ad8a-4738-8abe-a7a78ee98e95",
                        "title": "Newsletter 3",
                        "nextSendingDates": []
                    }
                ]
            }
			const exceptionsNewsletters = toExceptionsNewsletters(currentNode.identifier, response.data)

			const formattedNewsletters = newsletterFormater(exceptionsNewsletters).sort(function (a, b) {
				if (a.group.toLowerCase() + a.label.toLowerCase() < b.group.toLowerCase() + b.label.toLowerCase()) {
				  return -1;
				}
				if (a.group.toLowerCase() + a.label.toLowerCase() > b.group.toLowerCase() + b.label.toLowerCase()) {
				  return 1;
				}
				return 0;
			  });
			setExceptionsNewsletters(exceptionsNewsletters)
			setPublishedNewsletters([...formattedNewsletters])
			setIsLoading(false)

		}
		getPublishedNewsletters()
	}, [])
	useEffect(() => {
		setSelectedNewsletters(props.value.map(newsletter => newsletter.node_id))
	}, [props.value])
	const updateValue = newsletters => {
		setSelectedNewsletters(newsletters)
		let filteredNewsletters = getExceptionsObjectsByID(exceptionsNewsletters, newsletters)
		handleCommit(filteredNewsletters)
	}

	return (
		<div>
			{
				isLoading ?
					<p>Lade</p> :
					<SelectBox value={selectedNewsletters} options={selectBoxOptions} commit={event => updateValue(event)} />
			}
		</div>
	)
}


function toExceptionsNewsletters(currentNodeId, data) {
    let otherNewsLetters = [];

    if (data !== null && data.length > 0) {
        data.forEach(el => {
            if (el.node_id !== currentNodeId) {
                otherNewsLetters.push(el);
            }
        });
    }

    return otherNewsLetters;

}


function getExceptionsObjectsByID(objects, newsletterIDs) {
    let result = objects.filter(item => newsletterIDs.includes(item.node_id));
    return result;
}

There is a bug using this component: Once one discards their changes, there seems to be another commit, making it yet again possible to discard or apply changes. As of now, i am fairly certain that this “second commit” is not directly triggering inside the parent-component, so i am kinda lost.

Is there anything Neos-specific about the Inspector / Editors in general that i did not consider?

Best Regards,
Theodor

Can this even work?

it will be null or should throw an error, because its not wired.
The “proper” (unplanned ^^) way to get ahold of this component is via the globalRegistry:

const SelectBoxEditor = globalRegistry.get('inspector').get('editors').get('Neos.Neos/Inspector/Editors/SelectBoxEditor');

edit: i was lost it is actually exported (see extensibilityMap.json) …

Could you please show a video / image describing what you mean? ^^

But alternatively you can import the lower level components as well:

import {SelectBox, MultiSelectBox} from '@neos-project/react-ui-components';

which should ™ work correctly :slight_smile:

Sure, here’s a gif showing the problem:

BrokenEditor

You can see that the Discard / apply Button is not disabled even though i did discard my changes. Although my component apparently updates its state correctly, there seems to be a commit happening at or after discard-time

We wanted to circumvent going too deep into understanding the according React-component, but if there is no other option then i guess that is what it takes :thinking:

hmm the logic with mapping props.value to selectedNewsletters looks a bit fragile but then again i havent done react lately ^^

i would definitely try out using the native <MultiSelectBox> directly (there are many plugins using it or “just” check the neos ui code)

… but for debugging the react dev tools or the redux dev tools might shed some light into who issues that the state is dirty and the apply visible