diff --git a/docs/docs/core/content-manager/relations.mdx b/docs/docs/core/content-manager/relations.mdx new file mode 100644 index 0000000000..8a080eacda --- /dev/null +++ b/docs/docs/core/content-manager/relations.mdx @@ -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. + +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 + +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 diff --git a/docs/sidebars.js b/docs/sidebars.js index 24047f984b..c64e6f077a 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -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', diff --git a/docs/static/img/content-manager/relations/component-example.png b/docs/static/img/content-manager/relations/component-example.png new file mode 100644 index 0000000000..71630c2142 Binary files /dev/null and b/docs/static/img/content-manager/relations/component-example.png differ diff --git a/docs/static/img/content-manager/relations/relations-statemanagemen-diagram.png b/docs/static/img/content-manager/relations/relations-statemanagemen-diagram.png new file mode 100644 index 0000000000..7092b517f4 Binary files /dev/null and b/docs/static/img/content-manager/relations/relations-statemanagemen-diagram.png differ