Merge branch 'main' into chore/tracking-edit-field-name

This commit is contained in:
Simone 2022-11-21 22:50:16 +01:00 committed by GitHub
commit 2ee3af2b5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 151 additions and 1 deletions

View File

@ -0,0 +1,144 @@
---
title: Relations
slug: /content-manager/relations
description: Conceptual guide to relations in the Content Manager focussing on the technical decisions taken.
tags:
- content-manager
- relations
- redux-store
---
## Summary
Relations are a term used to describe how two or more entities are connected. Previously in the sidebar of an entity,
in Nov2020 we released a refactor that moved these fields into the main editing flow for a better editor experience
and to improve performance of the CMS application when many relations were used.
<img
src="/img/content-manager/relations/component-example.png"
alt="An example of the relations input in the CMS edit view"
/>
_above: An example of the relations input in the CMS edit view_
## Data management in frontend
<img
src="/img/content-manager/relations/relations-statemanagemen-diagram.png"
alt="a diagram overview explaining how state management works in relations"
/>
_above: A high-level diagram of how relations state management works_
### Preparing relation fields in the store
When you first open an existing entity, we call the admin API and put the data into the store to pre-populate fields
with existing values. However, its important to know when you have fields with `type === 'relation'` in your schema
that the data you receive will not be an array, but rather an object with the count of how many relations in that
field exist. For example, a section of the response may look like this:
```json
{
"my_relations": {
"count": 6
}
}
```
So without intervention, your inputs would try to append new relations to the `my_relations` object, which would not
work. Instead of this, before calling the redux action `INIT_FORM` we recursively find the paths fields based on the
following conditions:
- The field is a relation
- The field is a component
- The field is a repeatable component
- The field is a dynamic zone
These paths _do not_ take into account index values. So if you have a repetable component field where the schema looks like:
```json
{
"repeatable_single_component_relation": {
"type": "component",
"repeatable": true,
"component": "basic.relation"
}
}
```
and the components looks like:
```json
{
"basic.relation": {
"attributes": {
"id": {
"type": "integer"
},
"categories": {
"type": "relation",
"relation": "oneToMany",
"target": "api::category.category",
"targetModel": "api::category.category",
"relationType": "oneToMany"
},
"my_name": {
"type": "string"
}
}
}
}
```
Then the path to the relation field would be `repeatable_single_component_relation.categories`. Even though when
relations are added the path to the field in the redux store would be `repeatable_single_component_relation.0.categories`.
Inside the reducer we reduce the array of `relationalFieldPaths` to an object with the `initialValues` clone as
as the base. If there is `modifiedData` in the browser i.e. you've made changes to the entity and saved those changes,
we just replace the first level of the field with the `modifiedData` so the data structure is preserved and we're not
loosing the relations we had already loaded in the component. If the first part of the path is highlighted as the
`relationalField` then we simply replace that intial object with an empty array.
However, if the first part of the path is either a repeatable component, a dynamic zone or a regular component then we
recursively find the relation fields and replace the object with an array. This is handled by the `findLeafByPathAndReplace`
utility function. This function in short, takes an end path (in this case the relational field) and a primitive to replace
when it finds the endpath (an empty array in this case). It then recursively reduces the paths to the relational field mapping
through arrays if necessary (in the instance of repetable components for example) replacing the endpath with the primitive.
When this is done, we have sucessfully prepared our initial data for usage with relations.
### Handling updates to relation fields
Because we've prepared the fields prior to the component loading, adding & removing relations, it's relatively easy to do so.
When a relation is added, we simply push the new relation to the array of relations. When a relation is removed, we simply
filter out the relation from the array of relations. This is handled inside the reducer actions `CONNECT_RELATION` &
`DISCONNECT_RELATION` respectively.
:::note
Connecting relations adds the item to the end of the list, whilst loading more relations prepends to
the beginning of the list. This is the expected behaviour.
:::
The `RelationInput` component takes the field in `modifiedData` as its source of truth. You could therefore consider this to
be the `browserState` and `initialData` to be the `serverState`. When relations are loaded they're added to both the `intialData`
and `modifiedData` objects, but when you connect/disconnect only the `modifiedData` is updated. This is useful when we're preparing
data for the api.
### Cleaning data to be posted to the API
The API to update the enttiy expects relations to be categorised into two groups, a `connect` array and `disconnect` array.
You could do this as the user interacts with the input but we found this to be confusing and then involved us managing three
different arrays which makes the code more complex. Instead, because the browser doesn't really care about whats new and removed
and we have a copy of the slice of data we're mutating from the server we can run a small diff algorithm to determine which
relations have been connected and which have been disconnected. Returning an object like so:
```json
{
"my_relations": {
"connect": [{ "id": 1 }, { "id": 2 }],
"disconnect": []
}
}
```
## Frontend component architecture

View File

@ -31,7 +31,13 @@ const sidebars = {
type: 'doc',
id: 'core/content-manager/intro',
},
items: ['example'],
items: [
{
type: 'doc',
label: 'Relations',
id: 'core/content-manager/relations',
},
],
},
{
type: 'category',

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB