mirror of
https://github.com/strapi/strapi.git
synced 2025-08-07 08:16:35 +00:00
Merge branch 'main' into chore/tracking-edit-field-name
This commit is contained in:
commit
2ee3af2b5f
144
docs/docs/core/content-manager/relations.mdx
Normal file
144
docs/docs/core/content-manager/relations.mdx
Normal 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
|
@ -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',
|
||||
|
BIN
docs/static/img/content-manager/relations/component-example.png
vendored
Normal file
BIN
docs/static/img/content-manager/relations/component-example.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
docs/static/img/content-manager/relations/relations-statemanagemen-diagram.png
vendored
Normal file
BIN
docs/static/img/content-manager/relations/relations-statemanagemen-diagram.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 419 KiB |
Loading…
x
Reference in New Issue
Block a user