Closed skurfuerst closed 7 years ago
Niiice! Thanks a lot for thinking it through in such detail! Totally agree to making simple stateless components, without inheritance and confusing mixing of presentation and logic that we have now.
I'm not so sure of the benefits of putting the components state into Redux/Sagas. A higher level component that would encapsulate the state could work too. How would the shape of the Redux state look and where would it be put for all of the usecases that we have? These editors have to be rather self-contained, e.g. so we could put then inside inspector, inside node creation wizard, as structured inline editors and so on.
@dimaip I added some sections about the state structure; detailing it a lot and hopefully answering your questions :)
Now this concept is not WIP anymore, but ready for full-blown discussion :)
@skurfuerst Thanks a lot for creating this Issue with all the in-depth descriptions and explanations! Can't believe your created all that in an hour! This is the ideal way how to discuss topics like this! Everyone who is interested can quickly get some insight knowledge how things currently are and how you propose them to be in the new UI.
I really like the concept, here is some feedback / questions:
"Load the options lazily on typing (autocomplete). Here, the initial case needs to be handled separately, as we need to resolve by ID and not by label:"
I though we always solve by ID (=== option value?), because that would be the value the node property holds, or am I mistaking?
"useful to just load the data once and cache it for some time."
I really like the idea and the advandage of having cached data shared across multiple select fields. I'm trying to think of scenarios where this might be dangerous, but I think we'll be okay :) Since there is one (shared) data source this could also open the possibilty for options in multipe (indepentet) select fields, where an option may only be applied once (and in no other field at the same time). This of course can be implemented sometime in the future when it's even useful to someone, because basically we can already do this with a single MultiSelect.
Make the components completely stateless
Hooray for small stateless components :) Can it also be a functional stateless component, since we won't need state or lifecycle methods there? Could also become a great starting point for React newcomers who want to see how lightweight a custom component in the React UI can be.
The LRU mechanism
This part I do not understand "If the cache is not used 10 times in a row, we drop it." What counts as usage? If every single letter creates a separate cache entry, the cached entry I could have needed might already be dropped until I've reached my full search term?
Hey @gerhard-boden,
actually it took more like 3 hours to create all that; but I guess it's worth it ;)
Question 1: Well, when loading a dropdown initially we select by ID (f.e. Node Identifier). However, when you start typing something into the field, we actually do a search "by value" (f.e. Node Label).
Question 3: We'll see :-) That's something I'd decide when implementing the components.
Question 4 (LRU): A "Usage" is the following:
As an idea, we always increase the "cache usage counter" when the currently active node is changed
So basically, it means we cache everything for the last 10 visited nodes.
All the best, Sebastian
Hey a little late to the party. Your proposal looks pretty solid @skurfuerst, well done! And awesome to see some of these issues we've been wanting to solve in the old UI being solved now. Goes very much in line with some of the ideas for improvement I personally had while working on the old stuff.
Definitely see a great benefit from caching the fetched data for re-use across similar editors. Now whether that's better done using redux or a re-useable stateful component I'm not sure. I think there's a fine line depending on which editor and how the options are calculated. E.g. if they are calculated server-side, it's expensive, however if it's filtering an array of already available data, it's less expensive. E.g. node type editor uses data already available, same goes for a data source that loads all options (here it makes sense to store those for re-use). However a data source that needs to be calculated server side during search is expensive, but also difficult to cache (unlikely much useful re-use). If everything is treated the same way using LRU it might result in a not so optimal solution overall, thus maybe differentiating between cheap and expensive and applying the LRU cache on the expensive ones with high chance of re-use make sense.
Also regarding the dataProvider, in the old UI it's even possible to add additional data to the provider in addition to the node, which would potentially complicate that cache identifier further.
You don't mention the plugin editors, but wanted to mention that in the old UI they're also based on the select editor (MasterPlugin, PluginView, PluginViews).
Just for the completeness of this issue, wanted to mention some of the features/options available in the old UI to keep track of it in one place.
Lastly you mention the asset editor in the state/structure section, but don't include it in the listing of editors in the end. Do you intend to base the asset editor on these components as well or is that a separate topic? In the old UI it could be build using it, but totally rolls it's own implementation. If I were to implement it again I would probably go a different route all together and create a asset list editor with support for file selection, file upload and image cropping instead of reimplementing the asset list, knowing that's totally out of scope here.
Thanks @aertmann for your detailed feedback and comments -- now as I started implementing this I am also thinking more detailedly about the different loading-use-cases (which you have explained very well)!
The point I am currently unsure about is:
Should the Saga figure out by itself if data needs to be loaded (uni-directional data flow); or should the SelectBox say "you know, I am currently being rendered and I need a label for my value "XYZ"
Conceptually I prefer the first solution somehow, but while implementing it it seems it might actually be more difficult than the second one; and both get the job done. I'll think some more about their consequences :)
I'll try to update the Pull Request today :)
All the best, Sebastian
FYI: The second solution works nicely!
implemented by now :)
References / Select Editor
<- must-have for 1.0.0
the current Pull Request (WIP) can be found at #726
This document is divided into three parts:
Use Cases
The Select component is used in a variety of ways in the UI:
as a "simple select" to choose from predefined options in various UI parts
in the inline link editor, where the user starts typing and gets an async list of link suggestions
in the reference+link editors, where the user starts typing and gets an async list of reference suggestions
in the select editor:
in the MultiSelect editor:
In all cases, the select value bound from the outside is not the label shown in the select box; but often some kind of identifier (i.e. node identifier). Sometimes, the select box includes the "empty" value, sometimes not (depending on the configuration).
In all cases, if an already-selected option value is not existing anymore, show [invalid value: XY].
In the async cases, we need to show a loading indicator.
Asynchronous loading
It turns out the synchronous cases are all rather easy; it's the async cases which are difficult. We need to distinguish between the following cases:
getOptions() returns Promise
getSelectedValuesInitial(IDlist) returns Promise
getOptions(SearchString) returns Promise
Problems with the current implementation
The current version of the Select Box is already the second bigger iteration. In the first iteration, we had just a single stateful component dealing with all aspects of the implementation.
In the second iteration (which is currently in master), we tore apart different components, leading to the structure shown in the linked diagram:
open DIAGRAM - current implementation
The problem is that complexity grows out of hand when using an asynchronous MultiSelect editor, as on the one side, the MultiSelect is responsible for loading options; but on the other side, it embeds the (single) SelectBox, which is also responsible for loading options.
This makes it hard to implement the full behavior in a consistent manner.
Additionally, if people use nodes with lots of Select Editors (data providers or references); currently there is a request triggered per editor for loading the options; and previous results are not cached at all.
Implementation idea
Basic idea:
Component Architecture
The diagram linked below shows the proposed component and state architecture.
open DIAGRAM - proposed component architecture
Base Components
All base components will inherit from PureComponent, and be stateless.
State Structure / Reducers / Sagas
When thinking about the State structure, you might ask yourself why we should not just use a stateful React component containing the state. I think there are multiple answers to this why that's a bad idea (for me at least):
In my opinion, the data flow should be as unidirectional as possible. This means: It should not be a component's responsibility to determine whether some data it needs is missing (e.g. option labels). Instead, to me, that are different concerns; neatly encapsulated in Sagas.
How would the State be structured? I propose the following:
UI.asynchronousValueCache.<CachePartKey>
Inline-Link
for inline linksDataProvider-<DataProviderName>-<NodeIdentifier>
NodeReference-<NodeTypeListAllowedAsReferenceTarget>
Asset
for Asset editorvaluesByIdentifier
: an Identity Map, where each key is the "identifier" (f.e. the Node Identifier) for the object; and the value is an JSON structure describing the object - like{label: "foo", previewImage: "...."}
. This is dependent on the cache type.searchStrings
: A map which maps each "search string" the user has entered to a list of identifiers returned by the search.A full example looks as follows:
The LRU mechanism
Actually, the example above is not fully correct; as cache size might grow quite quickly; and never shrink.
Thus, we need a mechanism to evict caches; and we suggest to use a variant of LRU (Least Recently Used) here. If the cache is not used 10 times in a row, we drop it. In order to implement this, we need a notion of how to count cache usage; which is application-specific.
As an idea, we always increase the "cache usage counter" when the currently active node is changed; or when a new node is inserted (because the creation wizard might also contain select boxes and async loading).
I'd propose to do the LRU mechanism on the level of Cache Parts, and NOT on the level of individual cache entries; as otherwise the bookkeeping effort (and possible time to update the LRU counters) will be quite huge.
As a single Cache Part could then grow unconstrained, the idea would be: If it reaches a certain size, we drop all entries except the ones we need for the current point in time.
Loading options through Sagas
The actual option-loading (including the LRU mechanism) should be controlled and triggered by Sagas.
Example / Pseudo Code:
Probably we won't create a single Saga, but multiple of them; and move out common utility functions for code reuse.
Reference/References Editor
The reference editor basically needs to connect to the Redux Store; and calculate the CachePartKey.
Select/MultiSelect Editor
we probably have one editor, which, depending on defined options, will delegate to nested editors which then will be connected to the Redux Store.
Link Editor (Inspector)
Inline Link Editor
Estimation
(how much work is it to implement this?)
I'd guess 1-2 Weeks in total.
Further Work