strapi-relation-names 0.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -12
- package/dist/admin/{SettingsPage-C1nFFJjH.js → SettingsPage-CaBXOirN.js} +62 -8
- package/dist/admin/{SettingsPage-BQo5mpAY.mjs → SettingsPage-CpTyZSvB.mjs} +62 -8
- package/dist/admin/{index-SsQXjQq0.js → index-CKaZcnCM.js} +1 -1
- package/dist/admin/{index-CJO9wtUk.mjs → index-DGXo1dxk.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/types.d.ts +2 -0
- package/dist/admin/src/utils/template.d.ts +11 -4
- package/dist/server/index.js +78 -27
- package/dist/server/index.mjs +78 -27
- package/dist/server/src/index.d.ts +1 -1
- package/dist/server/src/services/index.d.ts +1 -1
- package/dist/server/src/services/relation-labels.d.ts +3 -1
- package/dist/server/src/services/utils/relation-labels.d.ts +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,10 @@ Display Strapi relations using readable labels built from your content fields in
|
|
|
9
9
|

|
|
10
10
|

|
|
11
11
|
|
|
12
|
+
Ever wanted to display a relation as custom text or with nested field values? This plugin solves exactly that:
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
12
16
|
---
|
|
13
17
|
|
|
14
18
|
## ✨ Features
|
|
@@ -92,14 +96,30 @@ After saving the settings, the Strapi Admin reloads automatically so open Conten
|
|
|
92
96
|
|
|
93
97
|
---
|
|
94
98
|
|
|
95
|
-
## ⚙️
|
|
99
|
+
## ⚙️ Configuration
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
Possible configuration keys are listed below; omitted keys keep the plugin defaults.
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
| Key | Description | Possible values |
|
|
104
|
+
| ------------- | ----------------------------------------------------------------------------------------------- | --------------- |
|
|
105
|
+
| `collections` | Limit relation names to the listed collection and single type UIDs. Leave empty to include all. | `string[]` |
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// config/plugins.{js,ts}
|
|
111
|
+
'strapi-relation-names': {
|
|
112
|
+
enabled: true,
|
|
113
|
+
config: {
|
|
114
|
+
collections: ['api::article.article', 'api::author.author'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
101
117
|
```
|
|
102
118
|
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## ⚙️ Templates
|
|
122
|
+
|
|
103
123
|
Multiple placeholders can be combined:
|
|
104
124
|
|
|
105
125
|
```text
|
|
@@ -122,13 +142,7 @@ Customer: {firstName} {lastName}
|
|
|
122
142
|
|
|
123
143
|
## 🗂️ How It Works
|
|
124
144
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
The first placeholder in a template is used internally by Strapi's Admin relation renderer.
|
|
128
|
-
|
|
129
|
-
Strapi's existing relation search and sorting continue to use the configured default main field.
|
|
130
|
-
|
|
131
|
-
Missing field values are replaced with an empty string.
|
|
145
|
+
At runtime, the server-side plugin decorates Strapi Content Manager relation responses and field metadata. It reads the configured template, validates each placeholder against the related content type schema, and renders the label from the returned relation data. If a value is not present, it performs a permission-aware hydration query; nested component paths are loaded by populating their component root. The rendered label is then exposed through Strapi's normal relation display mechanism. Missing field values are replaced with an empty string.
|
|
132
146
|
|
|
133
147
|
For example:
|
|
134
148
|
|
|
@@ -156,9 +170,10 @@ The default label is also used when:
|
|
|
156
170
|
|
|
157
171
|
- [ ] Live label previews while editing templates
|
|
158
172
|
- [ ] Define default relation templates in plugin configuration
|
|
159
|
-
- [
|
|
173
|
+
- [x] Support nested field paths in templates
|
|
160
174
|
- [ ] Import and export relation-label settings
|
|
161
175
|
- [ ] Conditional labels and configurable fallback rules
|
|
176
|
+
- [ ] Support label transformation functions (e.g., `toUpperCase`, `toLowerCase`, `capitalize`, etc.)
|
|
162
177
|
|
|
163
178
|
If you have any feature requests or suggestions, please open a dedicated issue.
|
|
164
179
|
|
|
@@ -5,7 +5,7 @@ const designSystem = require("@strapi/design-system");
|
|
|
5
5
|
const react = require("react");
|
|
6
6
|
const reactIntl = require("react-intl");
|
|
7
7
|
const admin = require("@strapi/strapi/admin");
|
|
8
|
-
const index = require("./index-
|
|
8
|
+
const index = require("./index-CKaZcnCM.js");
|
|
9
9
|
const FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
10
10
|
"string",
|
|
11
11
|
"text",
|
|
@@ -21,6 +21,16 @@ const FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
|
21
21
|
"time",
|
|
22
22
|
"boolean"
|
|
23
23
|
]);
|
|
24
|
+
const SYSTEM_SCALAR_FIELDS = [
|
|
25
|
+
"id",
|
|
26
|
+
"documentId",
|
|
27
|
+
"createdAt",
|
|
28
|
+
"updatedAt",
|
|
29
|
+
"publishedAt",
|
|
30
|
+
"locale",
|
|
31
|
+
"status"
|
|
32
|
+
];
|
|
33
|
+
const PLACEHOLDER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
24
34
|
const getPlaceholders = (template) => {
|
|
25
35
|
if (!template.trim()) {
|
|
26
36
|
return null;
|
|
@@ -33,7 +43,7 @@ const getPlaceholders = (template) => {
|
|
|
33
43
|
return null;
|
|
34
44
|
}
|
|
35
45
|
const name = match[1];
|
|
36
|
-
if (
|
|
46
|
+
if (!PLACEHOLDER_PATTERN.test(name)) {
|
|
37
47
|
return null;
|
|
38
48
|
}
|
|
39
49
|
placeholders.push(name);
|
|
@@ -44,16 +54,60 @@ const getPlaceholders = (template) => {
|
|
|
44
54
|
}
|
|
45
55
|
return placeholders.length > 0 ? placeholders : null;
|
|
46
56
|
};
|
|
47
|
-
const validateTemplate = (template, attributes) => {
|
|
57
|
+
const validateTemplate = (template, attributes, schemas) => {
|
|
48
58
|
const placeholders = getPlaceholders(template);
|
|
49
59
|
if (!placeholders) {
|
|
50
60
|
return "Use at least one valid {fieldName} placeholder.";
|
|
51
61
|
}
|
|
52
|
-
const invalidField = placeholders.find(
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
const invalidField = placeholders.find((path) => {
|
|
63
|
+
const segments = path.split(".");
|
|
64
|
+
let currentAttributes = attributes;
|
|
65
|
+
for (const [index2, segment] of segments.entries()) {
|
|
66
|
+
if (index2 === 0 && segments.length === 1 && SYSTEM_SCALAR_FIELDS.includes(segment)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const attribute = currentAttributes?.[segment];
|
|
70
|
+
const isLastSegment = index2 === segments.length - 1;
|
|
71
|
+
if (!attribute || attribute.private === true) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (isLastSegment) {
|
|
75
|
+
return !FIELD_TYPES.has(attribute.type ?? "");
|
|
76
|
+
}
|
|
77
|
+
if (attribute.type !== "component" || !attribute.component) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
currentAttributes = schemas?.get(attribute.component)?.attributes;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
55
84
|
return invalidField ? `“${invalidField}” is not an available scalar field.` : null;
|
|
56
85
|
};
|
|
86
|
+
const getAvailableFields = (schema, schemas, prefix = "", ancestors = /* @__PURE__ */ new Set()) => {
|
|
87
|
+
if (!schema) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const fields = Object.entries(schema.attributes ?? {}).flatMap(([name, attribute]) => {
|
|
91
|
+
if (attribute.private === true) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const path = `${prefix}${name}`;
|
|
95
|
+
if (FIELD_TYPES.has(attribute.type ?? "")) {
|
|
96
|
+
return [path];
|
|
97
|
+
}
|
|
98
|
+
if (attribute.type !== "component" || !attribute.component || ancestors.has(attribute.component)) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
return getAvailableFields(
|
|
102
|
+
schemas.get(attribute.component),
|
|
103
|
+
schemas,
|
|
104
|
+
`${path}.`,
|
|
105
|
+
/* @__PURE__ */ new Set([...ancestors, attribute.component])
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
const systemFields = prefix === "" ? SYSTEM_SCALAR_FIELDS : [];
|
|
109
|
+
return [...systemFields, ...fields.filter((field) => !systemFields.includes(field))];
|
|
110
|
+
};
|
|
57
111
|
const PLUGIN_SETTINGS_PATH = "/strapi-relation-names/settings";
|
|
58
112
|
const getRelationTarget = (attribute) => attribute.targetModel ?? attribute.target;
|
|
59
113
|
const SettingsPage = () => {
|
|
@@ -143,7 +197,7 @@ const SettingsPage = () => {
|
|
|
143
197
|
!isLoading ? rows.map(({ source, fieldName, target }) => {
|
|
144
198
|
const targetAttributes = target?.attributes ?? {};
|
|
145
199
|
const template = settings.relations[source.uid]?.[fieldName] ?? "";
|
|
146
|
-
const validationError = template ? validateTemplate(template, targetAttributes) : null;
|
|
200
|
+
const validationError = template ? validateTemplate(template, targetAttributes, schemaMap) : null;
|
|
147
201
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
148
202
|
designSystem.Box,
|
|
149
203
|
{
|
|
@@ -186,7 +240,7 @@ const SettingsPage = () => {
|
|
|
186
240
|
formatMessage({ id: index.getTranslation("settings.availableFields") }),
|
|
187
241
|
":",
|
|
188
242
|
" ",
|
|
189
|
-
|
|
243
|
+
getAvailableFields(target, schemaMap).join(", ")
|
|
190
244
|
] }) })
|
|
191
245
|
]
|
|
192
246
|
},
|
|
@@ -3,7 +3,7 @@ import { Main, Box, Typography, Field, TextInput, Flex, Button } from "@strapi/d
|
|
|
3
3
|
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
4
4
|
import { useIntl } from "react-intl";
|
|
5
5
|
import { useFetchClient } from "@strapi/strapi/admin";
|
|
6
|
-
import { g as getTranslation } from "./index-
|
|
6
|
+
import { g as getTranslation } from "./index-DGXo1dxk.mjs";
|
|
7
7
|
const FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
8
8
|
"string",
|
|
9
9
|
"text",
|
|
@@ -19,6 +19,16 @@ const FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
|
19
19
|
"time",
|
|
20
20
|
"boolean"
|
|
21
21
|
]);
|
|
22
|
+
const SYSTEM_SCALAR_FIELDS = [
|
|
23
|
+
"id",
|
|
24
|
+
"documentId",
|
|
25
|
+
"createdAt",
|
|
26
|
+
"updatedAt",
|
|
27
|
+
"publishedAt",
|
|
28
|
+
"locale",
|
|
29
|
+
"status"
|
|
30
|
+
];
|
|
31
|
+
const PLACEHOLDER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
22
32
|
const getPlaceholders = (template) => {
|
|
23
33
|
if (!template.trim()) {
|
|
24
34
|
return null;
|
|
@@ -31,7 +41,7 @@ const getPlaceholders = (template) => {
|
|
|
31
41
|
return null;
|
|
32
42
|
}
|
|
33
43
|
const name = match[1];
|
|
34
|
-
if (
|
|
44
|
+
if (!PLACEHOLDER_PATTERN.test(name)) {
|
|
35
45
|
return null;
|
|
36
46
|
}
|
|
37
47
|
placeholders.push(name);
|
|
@@ -42,16 +52,60 @@ const getPlaceholders = (template) => {
|
|
|
42
52
|
}
|
|
43
53
|
return placeholders.length > 0 ? placeholders : null;
|
|
44
54
|
};
|
|
45
|
-
const validateTemplate = (template, attributes) => {
|
|
55
|
+
const validateTemplate = (template, attributes, schemas) => {
|
|
46
56
|
const placeholders = getPlaceholders(template);
|
|
47
57
|
if (!placeholders) {
|
|
48
58
|
return "Use at least one valid {fieldName} placeholder.";
|
|
49
59
|
}
|
|
50
|
-
const invalidField = placeholders.find(
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
const invalidField = placeholders.find((path) => {
|
|
61
|
+
const segments = path.split(".");
|
|
62
|
+
let currentAttributes = attributes;
|
|
63
|
+
for (const [index, segment] of segments.entries()) {
|
|
64
|
+
if (index === 0 && segments.length === 1 && SYSTEM_SCALAR_FIELDS.includes(segment)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const attribute = currentAttributes?.[segment];
|
|
68
|
+
const isLastSegment = index === segments.length - 1;
|
|
69
|
+
if (!attribute || attribute.private === true) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (isLastSegment) {
|
|
73
|
+
return !FIELD_TYPES.has(attribute.type ?? "");
|
|
74
|
+
}
|
|
75
|
+
if (attribute.type !== "component" || !attribute.component) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
currentAttributes = schemas?.get(attribute.component)?.attributes;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
53
82
|
return invalidField ? `“${invalidField}” is not an available scalar field.` : null;
|
|
54
83
|
};
|
|
84
|
+
const getAvailableFields = (schema, schemas, prefix = "", ancestors = /* @__PURE__ */ new Set()) => {
|
|
85
|
+
if (!schema) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const fields = Object.entries(schema.attributes ?? {}).flatMap(([name, attribute]) => {
|
|
89
|
+
if (attribute.private === true) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const path = `${prefix}${name}`;
|
|
93
|
+
if (FIELD_TYPES.has(attribute.type ?? "")) {
|
|
94
|
+
return [path];
|
|
95
|
+
}
|
|
96
|
+
if (attribute.type !== "component" || !attribute.component || ancestors.has(attribute.component)) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return getAvailableFields(
|
|
100
|
+
schemas.get(attribute.component),
|
|
101
|
+
schemas,
|
|
102
|
+
`${path}.`,
|
|
103
|
+
/* @__PURE__ */ new Set([...ancestors, attribute.component])
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
const systemFields = prefix === "" ? SYSTEM_SCALAR_FIELDS : [];
|
|
107
|
+
return [...systemFields, ...fields.filter((field) => !systemFields.includes(field))];
|
|
108
|
+
};
|
|
55
109
|
const PLUGIN_SETTINGS_PATH = "/strapi-relation-names/settings";
|
|
56
110
|
const getRelationTarget = (attribute) => attribute.targetModel ?? attribute.target;
|
|
57
111
|
const SettingsPage = () => {
|
|
@@ -141,7 +195,7 @@ const SettingsPage = () => {
|
|
|
141
195
|
!isLoading ? rows.map(({ source, fieldName, target }) => {
|
|
142
196
|
const targetAttributes = target?.attributes ?? {};
|
|
143
197
|
const template = settings.relations[source.uid]?.[fieldName] ?? "";
|
|
144
|
-
const validationError = template ? validateTemplate(template, targetAttributes) : null;
|
|
198
|
+
const validationError = template ? validateTemplate(template, targetAttributes, schemaMap) : null;
|
|
145
199
|
return /* @__PURE__ */ jsxs(
|
|
146
200
|
Box,
|
|
147
201
|
{
|
|
@@ -184,7 +238,7 @@ const SettingsPage = () => {
|
|
|
184
238
|
formatMessage({ id: getTranslation("settings.availableFields") }),
|
|
185
239
|
":",
|
|
186
240
|
" ",
|
|
187
|
-
|
|
241
|
+
getAvailableFields(target, schemaMap).join(", ")
|
|
188
242
|
] }) })
|
|
189
243
|
]
|
|
190
244
|
},
|
|
@@ -89,7 +89,7 @@ const plugin = {
|
|
|
89
89
|
id: `${PLUGIN_ID}.plugin.name`,
|
|
90
90
|
defaultMessage: "Relation Names"
|
|
91
91
|
},
|
|
92
|
-
Component: () => Promise.resolve().then(() => require("./SettingsPage-
|
|
92
|
+
Component: () => Promise.resolve().then(() => require("./SettingsPage-CaBXOirN.js")).then((module2) => ({ default: module2.SettingsPage })),
|
|
93
93
|
permissions: []
|
|
94
94
|
});
|
|
95
95
|
app.registerPlugin({
|
|
@@ -88,7 +88,7 @@ const plugin = {
|
|
|
88
88
|
id: `${PLUGIN_ID}.plugin.name`,
|
|
89
89
|
defaultMessage: "Relation Names"
|
|
90
90
|
},
|
|
91
|
-
Component: () => import("./SettingsPage-
|
|
91
|
+
Component: () => import("./SettingsPage-CpTyZSvB.mjs").then((module) => ({ default: module.SettingsPage })),
|
|
92
92
|
permissions: []
|
|
93
93
|
});
|
|
94
94
|
app.registerPlugin({
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
declare const validateTemplate: (template: string, attributes: Record<string, {
|
|
1
|
+
type TemplateAttribute = {
|
|
3
2
|
type?: string;
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
component?: string;
|
|
4
|
+
private?: boolean;
|
|
5
|
+
};
|
|
6
|
+
type TemplateSchema = {
|
|
7
|
+
attributes?: Record<string, TemplateAttribute>;
|
|
8
|
+
};
|
|
9
|
+
declare const getPlaceholders: (template: string) => string[] | null;
|
|
10
|
+
declare const validateTemplate: (template: string, attributes: Record<string, TemplateAttribute> | undefined, schemas?: ReadonlyMap<string, TemplateSchema>) => string | null;
|
|
11
|
+
declare const getAvailableFields: (schema: TemplateSchema | undefined, schemas: ReadonlyMap<string, TemplateSchema>, prefix?: string, ancestors?: Set<string>) => string[];
|
|
12
|
+
export { getAvailableFields, getPlaceholders, validateTemplate };
|
package/dist/server/index.js
CHANGED
|
@@ -151,7 +151,22 @@ const SCALAR_TYPES = /* @__PURE__ */ new Set([
|
|
|
151
151
|
"time",
|
|
152
152
|
"boolean"
|
|
153
153
|
]);
|
|
154
|
+
const SYSTEM_SCALAR_FIELDS = /* @__PURE__ */ new Set([
|
|
155
|
+
"id",
|
|
156
|
+
"documentId",
|
|
157
|
+
"createdAt",
|
|
158
|
+
"updatedAt",
|
|
159
|
+
"publishedAt",
|
|
160
|
+
"locale",
|
|
161
|
+
"status"
|
|
162
|
+
]);
|
|
163
|
+
const PLACEHOLDER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
154
164
|
const isRecord$1 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
165
|
+
const getPathValue = (value, path) => {
|
|
166
|
+
return path.split(".").reduce((current, segment) => {
|
|
167
|
+
return isRecord$1(current) ? current[segment] : void 0;
|
|
168
|
+
}, value);
|
|
169
|
+
};
|
|
155
170
|
const parseTemplate = (template) => {
|
|
156
171
|
if (!template.trim()) {
|
|
157
172
|
return null;
|
|
@@ -172,7 +187,7 @@ const parseTemplate = (template) => {
|
|
|
172
187
|
return null;
|
|
173
188
|
}
|
|
174
189
|
const name = template.slice(openingBrace + 1, end);
|
|
175
|
-
if (
|
|
190
|
+
if (!PLACEHOLDER_PATTERN.test(name)) {
|
|
176
191
|
return null;
|
|
177
192
|
}
|
|
178
193
|
placeholders.push(name);
|
|
@@ -180,17 +195,35 @@ const parseTemplate = (template) => {
|
|
|
180
195
|
}
|
|
181
196
|
return placeholders.length > 0 ? placeholders : null;
|
|
182
197
|
};
|
|
183
|
-
const compileTemplate = (template, targetSchema) => {
|
|
198
|
+
const compileTemplate = (template, targetSchema, resolveSchema) => {
|
|
184
199
|
const placeholders = parseTemplate(template);
|
|
185
|
-
const attributes = targetSchema?.attributes ?? {};
|
|
186
200
|
if (!placeholders || !targetSchema) {
|
|
187
201
|
return null;
|
|
188
202
|
}
|
|
189
|
-
const valid = placeholders.every((
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
203
|
+
const valid = placeholders.every((path) => {
|
|
204
|
+
const segments = path.split(".");
|
|
205
|
+
let schema = targetSchema;
|
|
206
|
+
for (const [index2, segment] of segments.entries()) {
|
|
207
|
+
const attribute = schema.attributes?.[segment];
|
|
208
|
+
const isLastSegment = index2 === segments.length - 1;
|
|
209
|
+
if (index2 === 0 && isLastSegment && SYSTEM_SCALAR_FIELDS.has(segment)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (!attribute || attribute.private === true) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (isLastSegment) {
|
|
216
|
+
return SCALAR_TYPES.has(attribute.type ?? "");
|
|
217
|
+
}
|
|
218
|
+
if (attribute.type !== "component" || !attribute.component || !resolveSchema) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
schema = resolveSchema(attribute.component);
|
|
222
|
+
if (!schema) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
194
227
|
});
|
|
195
228
|
if (!valid) {
|
|
196
229
|
return null;
|
|
@@ -198,14 +231,14 @@ const compileTemplate = (template, targetSchema) => {
|
|
|
198
231
|
return {
|
|
199
232
|
template,
|
|
200
233
|
placeholders,
|
|
201
|
-
displayField: placeholders[0]
|
|
234
|
+
displayField: placeholders[0].split(".").slice(-1)[0] ?? placeholders[0]
|
|
202
235
|
};
|
|
203
236
|
};
|
|
204
237
|
const renderTemplate = (compiled, values) => {
|
|
205
238
|
const rendered = compiled.template.replace(
|
|
206
|
-
/\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
|
|
239
|
+
/\{([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\}/g,
|
|
207
240
|
(_match, name) => {
|
|
208
|
-
const value = values
|
|
241
|
+
const value = getPathValue(values, name);
|
|
209
242
|
return value === null || value === void 0 ? "" : String(value);
|
|
210
243
|
}
|
|
211
244
|
);
|
|
@@ -272,28 +305,40 @@ const getRuntime = async (strapi, settings2, sourceUid, targetField, userAbility
|
|
|
272
305
|
}
|
|
273
306
|
const template = settings2.relations[sourceUid]?.[targetField];
|
|
274
307
|
const targetSchema = strapi.getModel(attribute.target);
|
|
275
|
-
const compiled = typeof template === "string" ? compileTemplate(
|
|
308
|
+
const compiled = typeof template === "string" ? compileTemplate(
|
|
309
|
+
template,
|
|
310
|
+
targetSchema,
|
|
311
|
+
(uid) => strapi.getModel(uid)
|
|
312
|
+
) : null;
|
|
276
313
|
if (!compiled || !targetSchema) {
|
|
277
314
|
return null;
|
|
278
315
|
}
|
|
316
|
+
const originalMainField = await getOriginalMainField(
|
|
317
|
+
strapi,
|
|
318
|
+
sourceSchema,
|
|
319
|
+
targetSchema,
|
|
320
|
+
targetField,
|
|
321
|
+
userAbility
|
|
322
|
+
);
|
|
279
323
|
return {
|
|
280
324
|
...compiled,
|
|
325
|
+
displayField: compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? originalMainField : compiled.displayField,
|
|
281
326
|
sourceUid,
|
|
282
327
|
targetUid: targetSchema.uid,
|
|
283
|
-
originalMainField
|
|
284
|
-
strapi,
|
|
285
|
-
sourceSchema,
|
|
286
|
-
targetSchema,
|
|
287
|
-
targetField,
|
|
288
|
-
userAbility
|
|
289
|
-
)
|
|
328
|
+
originalMainField
|
|
290
329
|
};
|
|
291
330
|
};
|
|
331
|
+
const buildPopulate = (placeholders) => {
|
|
332
|
+
const roots = new Set(
|
|
333
|
+
placeholders.filter((placeholder) => placeholder.includes(".")).map((placeholder) => placeholder.split(".")[0])
|
|
334
|
+
);
|
|
335
|
+
return roots.size > 0 ? Object.fromEntries(Array.from(roots, (root) => [root, true])) : void 0;
|
|
336
|
+
};
|
|
292
337
|
const hydrateValues = async (strapi, ctx, runtime, values) => {
|
|
293
338
|
const targetSchema = strapi.getModel(runtime.targetUid);
|
|
294
339
|
const targetModelType = targetSchema.modelType;
|
|
295
340
|
const missingFields = runtime.placeholders.filter(
|
|
296
|
-
(field) => values.some((value) =>
|
|
341
|
+
(field) => values.some((value) => getPathValue(value, field) === void 0)
|
|
297
342
|
);
|
|
298
343
|
if (missingFields.length === 0) {
|
|
299
344
|
return new Map(values.map((value) => [value, value]));
|
|
@@ -306,7 +351,7 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
|
|
|
306
351
|
const permissionChecker = strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: runtime.targetUid });
|
|
307
352
|
const fields = Array.from(
|
|
308
353
|
/* @__PURE__ */ new Set([
|
|
309
|
-
...runtime.placeholders,
|
|
354
|
+
...runtime.placeholders.filter((field) => !field.includes(".")),
|
|
310
355
|
runtime.originalMainField,
|
|
311
356
|
"id",
|
|
312
357
|
"documentId",
|
|
@@ -314,10 +359,12 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
|
|
|
314
359
|
"publishedAt"
|
|
315
360
|
])
|
|
316
361
|
);
|
|
362
|
+
const populate = buildPopulate(runtime.placeholders);
|
|
317
363
|
const identityField = targetModelType === "component" ? "id" : "documentId";
|
|
318
364
|
const permissionQuery = await permissionChecker.sanitizedQuery.read({
|
|
319
365
|
fields,
|
|
320
|
-
filters: { [identityField]: { $in: identities } }
|
|
366
|
+
filters: { [identityField]: { $in: identities } },
|
|
367
|
+
...populate ? { populate } : {}
|
|
321
368
|
});
|
|
322
369
|
const query = strapi.get("query-params").transform(runtime.targetUid, permissionQuery);
|
|
323
370
|
const hydrated = await strapi.db.query(runtime.targetUid).findMany(query);
|
|
@@ -370,23 +417,27 @@ const relationLabels = ({ strapi }) => {
|
|
|
370
417
|
const targetSchema = strapi.getModel(attribute.target);
|
|
371
418
|
const compiled = compileTemplate(
|
|
372
419
|
settings2.relations[sourceUid]?.[fieldName] ?? "",
|
|
373
|
-
targetSchema
|
|
420
|
+
targetSchema,
|
|
421
|
+
(uid) => strapi.getModel(uid)
|
|
374
422
|
);
|
|
375
423
|
if (!compiled || !isRecord$1(metadata)) {
|
|
376
424
|
continue;
|
|
377
425
|
}
|
|
426
|
+
const editMetadata = isRecord$1(metadata.edit) ? metadata.edit : {};
|
|
427
|
+
const configuredMainField = typeof editMetadata.mainField === "string" ? editMetadata.mainField : "id";
|
|
428
|
+
const displayField = compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? configuredMainField : compiled.displayField;
|
|
378
429
|
metadatas[fieldName] = {
|
|
379
430
|
...metadata,
|
|
380
431
|
edit: {
|
|
381
|
-
...
|
|
382
|
-
mainField:
|
|
432
|
+
...editMetadata,
|
|
433
|
+
mainField: displayField
|
|
383
434
|
},
|
|
384
435
|
list: {
|
|
385
436
|
...isRecord$1(metadata.list) ? metadata.list : {},
|
|
386
|
-
mainField:
|
|
437
|
+
mainField: displayField
|
|
387
438
|
}
|
|
388
439
|
};
|
|
389
|
-
relationNames[fieldName] = { mainField:
|
|
440
|
+
relationNames[fieldName] = { mainField: displayField };
|
|
390
441
|
}
|
|
391
442
|
return {
|
|
392
443
|
...configuration,
|
package/dist/server/index.mjs
CHANGED
|
@@ -149,7 +149,22 @@ const SCALAR_TYPES = /* @__PURE__ */ new Set([
|
|
|
149
149
|
"time",
|
|
150
150
|
"boolean"
|
|
151
151
|
]);
|
|
152
|
+
const SYSTEM_SCALAR_FIELDS = /* @__PURE__ */ new Set([
|
|
153
|
+
"id",
|
|
154
|
+
"documentId",
|
|
155
|
+
"createdAt",
|
|
156
|
+
"updatedAt",
|
|
157
|
+
"publishedAt",
|
|
158
|
+
"locale",
|
|
159
|
+
"status"
|
|
160
|
+
]);
|
|
161
|
+
const PLACEHOLDER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
152
162
|
const isRecord$1 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
163
|
+
const getPathValue = (value, path) => {
|
|
164
|
+
return path.split(".").reduce((current, segment) => {
|
|
165
|
+
return isRecord$1(current) ? current[segment] : void 0;
|
|
166
|
+
}, value);
|
|
167
|
+
};
|
|
153
168
|
const parseTemplate = (template) => {
|
|
154
169
|
if (!template.trim()) {
|
|
155
170
|
return null;
|
|
@@ -170,7 +185,7 @@ const parseTemplate = (template) => {
|
|
|
170
185
|
return null;
|
|
171
186
|
}
|
|
172
187
|
const name = template.slice(openingBrace + 1, end);
|
|
173
|
-
if (
|
|
188
|
+
if (!PLACEHOLDER_PATTERN.test(name)) {
|
|
174
189
|
return null;
|
|
175
190
|
}
|
|
176
191
|
placeholders.push(name);
|
|
@@ -178,17 +193,35 @@ const parseTemplate = (template) => {
|
|
|
178
193
|
}
|
|
179
194
|
return placeholders.length > 0 ? placeholders : null;
|
|
180
195
|
};
|
|
181
|
-
const compileTemplate = (template, targetSchema) => {
|
|
196
|
+
const compileTemplate = (template, targetSchema, resolveSchema) => {
|
|
182
197
|
const placeholders = parseTemplate(template);
|
|
183
|
-
const attributes = targetSchema?.attributes ?? {};
|
|
184
198
|
if (!placeholders || !targetSchema) {
|
|
185
199
|
return null;
|
|
186
200
|
}
|
|
187
|
-
const valid = placeholders.every((
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
const valid = placeholders.every((path) => {
|
|
202
|
+
const segments = path.split(".");
|
|
203
|
+
let schema = targetSchema;
|
|
204
|
+
for (const [index2, segment] of segments.entries()) {
|
|
205
|
+
const attribute = schema.attributes?.[segment];
|
|
206
|
+
const isLastSegment = index2 === segments.length - 1;
|
|
207
|
+
if (index2 === 0 && isLastSegment && SYSTEM_SCALAR_FIELDS.has(segment)) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (!attribute || attribute.private === true) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if (isLastSegment) {
|
|
214
|
+
return SCALAR_TYPES.has(attribute.type ?? "");
|
|
215
|
+
}
|
|
216
|
+
if (attribute.type !== "component" || !attribute.component || !resolveSchema) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
schema = resolveSchema(attribute.component);
|
|
220
|
+
if (!schema) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
192
225
|
});
|
|
193
226
|
if (!valid) {
|
|
194
227
|
return null;
|
|
@@ -196,14 +229,14 @@ const compileTemplate = (template, targetSchema) => {
|
|
|
196
229
|
return {
|
|
197
230
|
template,
|
|
198
231
|
placeholders,
|
|
199
|
-
displayField: placeholders[0]
|
|
232
|
+
displayField: placeholders[0].split(".").slice(-1)[0] ?? placeholders[0]
|
|
200
233
|
};
|
|
201
234
|
};
|
|
202
235
|
const renderTemplate = (compiled, values) => {
|
|
203
236
|
const rendered = compiled.template.replace(
|
|
204
|
-
/\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
|
|
237
|
+
/\{([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\}/g,
|
|
205
238
|
(_match, name) => {
|
|
206
|
-
const value = values
|
|
239
|
+
const value = getPathValue(values, name);
|
|
207
240
|
return value === null || value === void 0 ? "" : String(value);
|
|
208
241
|
}
|
|
209
242
|
);
|
|
@@ -270,28 +303,40 @@ const getRuntime = async (strapi, settings2, sourceUid, targetField, userAbility
|
|
|
270
303
|
}
|
|
271
304
|
const template = settings2.relations[sourceUid]?.[targetField];
|
|
272
305
|
const targetSchema = strapi.getModel(attribute.target);
|
|
273
|
-
const compiled = typeof template === "string" ? compileTemplate(
|
|
306
|
+
const compiled = typeof template === "string" ? compileTemplate(
|
|
307
|
+
template,
|
|
308
|
+
targetSchema,
|
|
309
|
+
(uid) => strapi.getModel(uid)
|
|
310
|
+
) : null;
|
|
274
311
|
if (!compiled || !targetSchema) {
|
|
275
312
|
return null;
|
|
276
313
|
}
|
|
314
|
+
const originalMainField = await getOriginalMainField(
|
|
315
|
+
strapi,
|
|
316
|
+
sourceSchema,
|
|
317
|
+
targetSchema,
|
|
318
|
+
targetField,
|
|
319
|
+
userAbility
|
|
320
|
+
);
|
|
277
321
|
return {
|
|
278
322
|
...compiled,
|
|
323
|
+
displayField: compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? originalMainField : compiled.displayField,
|
|
279
324
|
sourceUid,
|
|
280
325
|
targetUid: targetSchema.uid,
|
|
281
|
-
originalMainField
|
|
282
|
-
strapi,
|
|
283
|
-
sourceSchema,
|
|
284
|
-
targetSchema,
|
|
285
|
-
targetField,
|
|
286
|
-
userAbility
|
|
287
|
-
)
|
|
326
|
+
originalMainField
|
|
288
327
|
};
|
|
289
328
|
};
|
|
329
|
+
const buildPopulate = (placeholders) => {
|
|
330
|
+
const roots = new Set(
|
|
331
|
+
placeholders.filter((placeholder) => placeholder.includes(".")).map((placeholder) => placeholder.split(".")[0])
|
|
332
|
+
);
|
|
333
|
+
return roots.size > 0 ? Object.fromEntries(Array.from(roots, (root) => [root, true])) : void 0;
|
|
334
|
+
};
|
|
290
335
|
const hydrateValues = async (strapi, ctx, runtime, values) => {
|
|
291
336
|
const targetSchema = strapi.getModel(runtime.targetUid);
|
|
292
337
|
const targetModelType = targetSchema.modelType;
|
|
293
338
|
const missingFields = runtime.placeholders.filter(
|
|
294
|
-
(field) => values.some((value) =>
|
|
339
|
+
(field) => values.some((value) => getPathValue(value, field) === void 0)
|
|
295
340
|
);
|
|
296
341
|
if (missingFields.length === 0) {
|
|
297
342
|
return new Map(values.map((value) => [value, value]));
|
|
@@ -304,7 +349,7 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
|
|
|
304
349
|
const permissionChecker = strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: runtime.targetUid });
|
|
305
350
|
const fields = Array.from(
|
|
306
351
|
/* @__PURE__ */ new Set([
|
|
307
|
-
...runtime.placeholders,
|
|
352
|
+
...runtime.placeholders.filter((field) => !field.includes(".")),
|
|
308
353
|
runtime.originalMainField,
|
|
309
354
|
"id",
|
|
310
355
|
"documentId",
|
|
@@ -312,10 +357,12 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
|
|
|
312
357
|
"publishedAt"
|
|
313
358
|
])
|
|
314
359
|
);
|
|
360
|
+
const populate = buildPopulate(runtime.placeholders);
|
|
315
361
|
const identityField = targetModelType === "component" ? "id" : "documentId";
|
|
316
362
|
const permissionQuery = await permissionChecker.sanitizedQuery.read({
|
|
317
363
|
fields,
|
|
318
|
-
filters: { [identityField]: { $in: identities } }
|
|
364
|
+
filters: { [identityField]: { $in: identities } },
|
|
365
|
+
...populate ? { populate } : {}
|
|
319
366
|
});
|
|
320
367
|
const query = strapi.get("query-params").transform(runtime.targetUid, permissionQuery);
|
|
321
368
|
const hydrated = await strapi.db.query(runtime.targetUid).findMany(query);
|
|
@@ -368,23 +415,27 @@ const relationLabels = ({ strapi }) => {
|
|
|
368
415
|
const targetSchema = strapi.getModel(attribute.target);
|
|
369
416
|
const compiled = compileTemplate(
|
|
370
417
|
settings2.relations[sourceUid]?.[fieldName] ?? "",
|
|
371
|
-
targetSchema
|
|
418
|
+
targetSchema,
|
|
419
|
+
(uid) => strapi.getModel(uid)
|
|
372
420
|
);
|
|
373
421
|
if (!compiled || !isRecord$1(metadata)) {
|
|
374
422
|
continue;
|
|
375
423
|
}
|
|
424
|
+
const editMetadata = isRecord$1(metadata.edit) ? metadata.edit : {};
|
|
425
|
+
const configuredMainField = typeof editMetadata.mainField === "string" ? editMetadata.mainField : "id";
|
|
426
|
+
const displayField = compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? configuredMainField : compiled.displayField;
|
|
376
427
|
metadatas[fieldName] = {
|
|
377
428
|
...metadata,
|
|
378
429
|
edit: {
|
|
379
|
-
...
|
|
380
|
-
mainField:
|
|
430
|
+
...editMetadata,
|
|
431
|
+
mainField: displayField
|
|
381
432
|
},
|
|
382
433
|
list: {
|
|
383
434
|
...isRecord$1(metadata.list) ? metadata.list : {},
|
|
384
|
-
mainField:
|
|
435
|
+
mainField: displayField
|
|
385
436
|
}
|
|
386
437
|
};
|
|
387
|
-
relationNames[fieldName] = { mainField:
|
|
438
|
+
relationNames[fieldName] = { mainField: displayField };
|
|
388
439
|
}
|
|
389
440
|
return {
|
|
390
441
|
...configuration,
|
|
@@ -48,7 +48,7 @@ declare const _default: {
|
|
|
48
48
|
decorateCollectionResults: (ctx: any, sourceUid: string, results: unknown[]) => Promise<unknown[]>;
|
|
49
49
|
decorateConfiguration: (data: any, sourceUid: string) => Promise<any>;
|
|
50
50
|
decorateRelationResults: (ctx: any, sourceUid: string, targetField: string, results: unknown[]) => Promise<unknown[]>;
|
|
51
|
-
compileTemplate: (template: string, targetSchema: import('./services/utils/relation-labels').SchemaLike | undefined) => import('./services/utils/relation-labels').CompiledTemplate | null;
|
|
51
|
+
compileTemplate: (template: string, targetSchema: import('./services/utils/relation-labels').SchemaLike | undefined, resolveSchema?: import('./services/utils/relation-labels').SchemaResolver) => import('./services/utils/relation-labels').CompiledTemplate | null;
|
|
52
52
|
parseTemplate: (template: string) => string[] | null;
|
|
53
53
|
renderTemplate: (compiled: import('./services/utils/relation-labels').CompiledTemplate, values: Record<string, unknown>) => string;
|
|
54
54
|
};
|
|
@@ -5,7 +5,7 @@ declare const _default: {
|
|
|
5
5
|
decorateCollectionResults: (ctx: any, sourceUid: string, results: unknown[]) => Promise<unknown[]>;
|
|
6
6
|
decorateConfiguration: (data: any, sourceUid: string) => Promise<any>;
|
|
7
7
|
decorateRelationResults: (ctx: any, sourceUid: string, targetField: string, results: unknown[]) => Promise<unknown[]>;
|
|
8
|
-
compileTemplate: (template: string, targetSchema: import('./utils/relation-labels').SchemaLike | undefined) => import('./utils/relation-labels').CompiledTemplate | null;
|
|
8
|
+
compileTemplate: (template: string, targetSchema: import('./utils/relation-labels').SchemaLike | undefined, resolveSchema?: import('./utils/relation-labels').SchemaResolver) => import('./utils/relation-labels').CompiledTemplate | null;
|
|
9
9
|
parseTemplate: (template: string) => string[] | null;
|
|
10
10
|
renderTemplate: (compiled: import('./utils/relation-labels').CompiledTemplate, values: Record<string, unknown>) => string;
|
|
11
11
|
};
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Core } from '@strapi/strapi';
|
|
2
2
|
import { SchemaLike } from './utils/relation-labels';
|
|
3
|
+
declare const buildPopulate: (placeholders: string[]) => Record<string, true> | undefined;
|
|
3
4
|
declare const relationLabels: ({ strapi }: {
|
|
4
5
|
strapi: Core.Strapi;
|
|
5
6
|
}) => {
|
|
6
7
|
decorateCollectionResults: (ctx: any, sourceUid: string, results: unknown[]) => Promise<unknown[]>;
|
|
7
8
|
decorateConfiguration: (data: any, sourceUid: string) => Promise<any>;
|
|
8
9
|
decorateRelationResults: (ctx: any, sourceUid: string, targetField: string, results: unknown[]) => Promise<unknown[]>;
|
|
9
|
-
compileTemplate: (template: string, targetSchema: SchemaLike | undefined) => import('./utils/relation-labels').CompiledTemplate | null;
|
|
10
|
+
compileTemplate: (template: string, targetSchema: SchemaLike | undefined, resolveSchema?: import('./utils/relation-labels').SchemaResolver) => import('./utils/relation-labels').CompiledTemplate | null;
|
|
10
11
|
parseTemplate: (template: string) => string[] | null;
|
|
11
12
|
renderTemplate: (compiled: import('./utils/relation-labels').CompiledTemplate, values: Record<string, unknown>) => string;
|
|
12
13
|
};
|
|
14
|
+
export { buildPopulate };
|
|
13
15
|
export { compileTemplate, parseTemplate, renderTemplate } from './utils/relation-labels';
|
|
14
16
|
export default relationLabels;
|
|
@@ -11,6 +11,7 @@ export type SchemaLike = {
|
|
|
11
11
|
modelType?: string;
|
|
12
12
|
attributes?: Record<string, SchemaAttribute>;
|
|
13
13
|
};
|
|
14
|
+
export type SchemaResolver = (uid: string) => SchemaLike | undefined;
|
|
14
15
|
export type CompiledTemplate = {
|
|
15
16
|
template: string;
|
|
16
17
|
placeholders: string[];
|
|
@@ -22,6 +23,7 @@ export type RelationRuntime = CompiledTemplate & {
|
|
|
22
23
|
originalMainField: string;
|
|
23
24
|
};
|
|
24
25
|
export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
|
|
26
|
+
export declare const getPathValue: (value: unknown, path: string) => unknown;
|
|
25
27
|
export declare const parseTemplate: (template: string) => string[] | null;
|
|
26
|
-
export declare const compileTemplate: (template: string, targetSchema: SchemaLike | undefined) => CompiledTemplate | null;
|
|
28
|
+
export declare const compileTemplate: (template: string, targetSchema: SchemaLike | undefined, resolveSchema?: SchemaResolver) => CompiledTemplate | null;
|
|
27
29
|
export declare const renderTemplate: (compiled: CompiledTemplate, values: Record<string, unknown>) => string;
|