strapi-relation-names 1.2.2 → 1.3.1
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 +17 -42
- package/dist/{admin/SettingsPage-BebR4ghK.js → _chunks/SettingsPage-BMwx-BuP.js} +122 -48
- package/dist/{admin/SettingsPage-DFZP5CZs.mjs → _chunks/SettingsPage-D8leItKF.mjs} +122 -48
- package/dist/{admin/index-CiS6cuI9.mjs → _chunks/index-BeLPoAiU.mjs} +1 -1
- package/dist/{admin/index-DfSAe961.js → _chunks/index-COuIED98.js} +1 -1
- package/dist/admin/index.js +2 -3
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/index.d.ts +1 -1
- package/dist/admin/src/pages/SettingsPage.d.ts +1 -0
- package/dist/admin/src/utils/template.d.ts +1 -14
- package/dist/server/index.js +267 -129
- package/dist/server/index.mjs +266 -127
- package/dist/server/src/bootstrap.d.ts +1 -1
- package/dist/server/src/config/index.d.ts +2 -0
- package/dist/server/src/controllers/index.d.ts +1 -1
- package/dist/server/src/controllers/settings.d.ts +1 -1
- package/dist/server/src/destroy.d.ts +1 -1
- package/dist/server/src/index.d.ts +20 -11
- package/dist/server/src/register.d.ts +1 -1
- package/dist/server/src/services/index.d.ts +8 -7
- package/dist/server/src/services/relation-labels.d.ts +6 -9
- package/dist/server/src/services/settings.d.ts +2 -2
- package/dist/server/src/services/utils/relation-hydration.d.ts +7 -0
- package/dist/server/src/services/utils/relation-labels.d.ts +4 -22
- package/dist/server/src/services/utils/relation-runtime.d.ts +6 -0
- package/dist/server/src/services/utils/relation-values.d.ts +1 -1
- package/dist/shared/records.d.ts +1 -0
- package/dist/shared/template.d.ts +55 -0
- package/package.json +28 -28
- /package/dist/{admin → _chunks}/de-Bc81LkbY.mjs +0 -0
- /package/dist/{admin → _chunks}/de-CsBvOsWR.js +0 -0
- /package/dist/{admin → _chunks}/en-Bk-HIXNd.mjs +0 -0
- /package/dist/{admin → _chunks}/en-DT4g6X2H.js +0 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Display Strapi relations using readable labels built from your content fields in
|
|
|
11
11
|
|
|
12
12
|
Ever wanted to display a relation as custom text or with nested field values? This plugin solves exactly that:
|
|
13
13
|
|
|
14
|
-

|
|
14
|
+

|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -67,32 +67,25 @@ After installing and enabling the plugin, open the Strapi Admin settings panel:
|
|
|
67
67
|
|
|
68
68
|
Each relation field is displayed together with the scalar fields available on its related content type or component.
|
|
69
69
|
|
|
70
|
-
Create a label using placeholders
|
|
70
|
+
Create a label using placeholders e.g.:
|
|
71
71
|
|
|
72
72
|
```text
|
|
73
73
|
{firstName} {lastName}
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```text
|
|
79
|
-
firstName = John
|
|
80
|
-
lastName = Doe
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
the relation will be displayed as:
|
|
76
|
+
You can also mix placeholders with static text:
|
|
84
77
|
|
|
85
78
|
```text
|
|
86
|
-
|
|
79
|
+
Name: {firstName} {lastName}
|
|
87
80
|
```
|
|
88
81
|
|
|
89
|
-
|
|
82
|
+
It is also possible to transform a label's value using pipe syntax:
|
|
83
|
+
{createdAt | toLocalDateTime}
|
|
90
84
|
|
|
91
|
-
|
|
92
|
-
{firstName} {lastName} ({customerNumber})
|
|
93
|
-
```
|
|
85
|
+
Currently the following transformations exist:
|
|
94
86
|
|
|
95
|
-
|
|
87
|
+
- `toLocalDate` — formats date values using the configured locale and time zone.
|
|
88
|
+
- `toLocalDateTime` — formats date-time values using the configured locale and time zone.
|
|
96
89
|
|
|
97
90
|
---
|
|
98
91
|
|
|
@@ -100,9 +93,11 @@ After saving the settings, the Strapi Admin reloads automatically so open Conten
|
|
|
100
93
|
|
|
101
94
|
Possible configuration keys are listed below; omitted keys keep the plugin defaults.
|
|
102
95
|
|
|
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[]`
|
|
96
|
+
| Key | Description | Possible values |
|
|
97
|
+
| ------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------ |
|
|
98
|
+
| `collections` | Limit relation names to the listed collection and single type UIDs. Leave empty to include all. | `string[]` (default: `[]`) |
|
|
99
|
+
| `locale` | Locale used by date transformations. | `string` (default: runtime locale) |
|
|
100
|
+
| `timeZone` | IANA timezone used by date transformations. | `string` (default: runtime timezone) |
|
|
106
101
|
|
|
107
102
|
Example:
|
|
108
103
|
|
|
@@ -112,34 +107,14 @@ Example:
|
|
|
112
107
|
enabled: true,
|
|
113
108
|
config: {
|
|
114
109
|
collections: ['api::article.article', 'api::author.author'],
|
|
110
|
+
locale: 'de-AT',
|
|
111
|
+
timeZone: 'Europe/Vienna',
|
|
115
112
|
},
|
|
116
113
|
},
|
|
117
114
|
```
|
|
118
115
|
|
|
119
116
|
---
|
|
120
117
|
|
|
121
|
-
## ⚙️ Templates
|
|
122
|
-
|
|
123
|
-
Multiple placeholders can be combined:
|
|
124
|
-
|
|
125
|
-
```text
|
|
126
|
-
{firstName} {lastName}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Placeholders can also be repeated:
|
|
130
|
-
|
|
131
|
-
```text
|
|
132
|
-
{name} - {name}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Static text can be included anywhere:
|
|
136
|
-
|
|
137
|
-
```text
|
|
138
|
-
Customer: {firstName} {lastName}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
118
|
## 🗂️ How It Works
|
|
144
119
|
|
|
145
120
|
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.
|
|
@@ -174,7 +149,7 @@ The default label is also used when:
|
|
|
174
149
|
- [x] Support nested relation fields in templates
|
|
175
150
|
- [ ] Import and export relation-label settings
|
|
176
151
|
- [ ] Conditional labels and configurable fallback rules
|
|
177
|
-
- [
|
|
152
|
+
- [x] Support label transformation functions
|
|
178
153
|
|
|
179
154
|
If you have any feature requests or suggestions, please open a dedicated issue.
|
|
180
155
|
|
|
@@ -6,7 +6,7 @@ const styled = require("styled-components");
|
|
|
6
6
|
const react = require("react");
|
|
7
7
|
const reactIntl = require("react-intl");
|
|
8
8
|
const admin = require("@strapi/strapi/admin");
|
|
9
|
-
const index = require("./index-
|
|
9
|
+
const index = require("./index-COuIED98.js");
|
|
10
10
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
11
11
|
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
12
12
|
const FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -24,16 +24,19 @@ const FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
|
24
24
|
"time",
|
|
25
25
|
"boolean"
|
|
26
26
|
]);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
const DATE_TYPES = /* @__PURE__ */ new Set(["date", "datetime"]);
|
|
28
|
+
const SYSTEM_SCALAR_TYPES = {
|
|
29
|
+
id: "integer",
|
|
30
|
+
documentId: "string",
|
|
31
|
+
createdAt: "datetime",
|
|
32
|
+
updatedAt: "datetime",
|
|
33
|
+
publishedAt: "datetime",
|
|
34
|
+
locale: "string",
|
|
35
|
+
status: "string"
|
|
36
|
+
};
|
|
37
|
+
const SYSTEM_SCALAR_FIELDS = Object.keys(SYSTEM_SCALAR_TYPES);
|
|
38
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
39
|
+
const PATH_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
37
40
|
const getNestedSchemaUid = (attribute) => {
|
|
38
41
|
if (attribute.type === "component") {
|
|
39
42
|
return attribute.component;
|
|
@@ -43,58 +46,129 @@ const getNestedSchemaUid = (attribute) => {
|
|
|
43
46
|
}
|
|
44
47
|
return void 0;
|
|
45
48
|
};
|
|
46
|
-
const
|
|
49
|
+
const getDate = (value) => {
|
|
50
|
+
if (value instanceof Date) {
|
|
51
|
+
return Number.isNaN(value.getTime()) ? void 0 : value;
|
|
52
|
+
}
|
|
53
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
const date = new Date(value);
|
|
57
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
58
|
+
};
|
|
59
|
+
const isDateOnly = (value) => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
60
|
+
const formatDate = (value, options, context, preserveDateOnly) => {
|
|
61
|
+
const date = getDate(value);
|
|
62
|
+
if (!date) {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
const timeZone = preserveDateOnly && isDateOnly(value) ? "UTC" : context.timeZone;
|
|
66
|
+
return new Intl.DateTimeFormat(context.locale, {
|
|
67
|
+
...options,
|
|
68
|
+
...timeZone ? { timeZone } : {}
|
|
69
|
+
}).format(date);
|
|
70
|
+
};
|
|
71
|
+
const TRANSFORMS = {
|
|
72
|
+
toLocalDate: {
|
|
73
|
+
accepts: (fieldType) => DATE_TYPES.has(fieldType ?? ""),
|
|
74
|
+
apply: (value, context) => formatDate(value, { dateStyle: "short" }, context, true)
|
|
75
|
+
},
|
|
76
|
+
toLocalDateTime: {
|
|
77
|
+
accepts: (fieldType) => fieldType === "datetime",
|
|
78
|
+
apply: (value, context) => formatDate(value, { dateStyle: "short", timeStyle: "short" }, context, false)
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const parseExpression = (value) => {
|
|
82
|
+
const parts = value.split("|").map((part) => part.trim());
|
|
83
|
+
const [path, ...transforms] = parts;
|
|
84
|
+
if (!path || !PATH_PATTERN.test(path) || transforms.some((name) => !IDENTIFIER_PATTERN.test(name))) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return { path, transforms };
|
|
88
|
+
};
|
|
89
|
+
const parseTemplateAst = (template) => {
|
|
47
90
|
if (!template.trim()) {
|
|
48
91
|
return null;
|
|
49
92
|
}
|
|
50
|
-
const
|
|
51
|
-
const
|
|
93
|
+
const tokens = [];
|
|
94
|
+
const expressions = [];
|
|
52
95
|
let cursor = 0;
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
while (cursor < template.length) {
|
|
97
|
+
const openingBrace = template.indexOf("{", cursor);
|
|
98
|
+
const closingBrace = template.indexOf("}", cursor);
|
|
99
|
+
if (closingBrace !== -1 && (openingBrace === -1 || closingBrace < openingBrace)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (openingBrace === -1) {
|
|
103
|
+
tokens.push({ type: "text", value: template.slice(cursor) });
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (openingBrace > cursor) {
|
|
107
|
+
tokens.push({ type: "text", value: template.slice(cursor, openingBrace) });
|
|
108
|
+
}
|
|
109
|
+
const end = template.indexOf("}", openingBrace + 1);
|
|
110
|
+
if (end === -1) {
|
|
55
111
|
return null;
|
|
56
112
|
}
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
113
|
+
const expression = parseExpression(template.slice(openingBrace + 1, end));
|
|
114
|
+
if (!expression) {
|
|
59
115
|
return null;
|
|
60
116
|
}
|
|
61
|
-
|
|
62
|
-
|
|
117
|
+
tokens.push({ type: "expression", expression });
|
|
118
|
+
expressions.push(expression);
|
|
119
|
+
cursor = end + 1;
|
|
63
120
|
}
|
|
64
|
-
|
|
65
|
-
|
|
121
|
+
return expressions.length > 0 ? { tokens, expressions } : null;
|
|
122
|
+
};
|
|
123
|
+
const resolvePathType = (path, targetSchema, resolveSchema) => {
|
|
124
|
+
const segments = path.split(".");
|
|
125
|
+
let schema = targetSchema;
|
|
126
|
+
for (const [index2, segment] of segments.entries()) {
|
|
127
|
+
const isLastSegment = index2 === segments.length - 1;
|
|
128
|
+
if (index2 === 0 && isLastSegment && SYSTEM_SCALAR_TYPES[segment]) {
|
|
129
|
+
return SYSTEM_SCALAR_TYPES[segment];
|
|
130
|
+
}
|
|
131
|
+
const attribute = schema.attributes?.[segment];
|
|
132
|
+
if (!attribute || attribute.private === true) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
if (isLastSegment) {
|
|
136
|
+
return FIELD_TYPES.has(attribute.type ?? "") ? attribute.type : void 0;
|
|
137
|
+
}
|
|
138
|
+
const nestedSchemaUid = getNestedSchemaUid(attribute);
|
|
139
|
+
if (!nestedSchemaUid || !resolveSchema) {
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
const nestedSchema = resolveSchema(nestedSchemaUid);
|
|
143
|
+
if (!nestedSchema) {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
schema = nestedSchema;
|
|
66
147
|
}
|
|
67
|
-
return
|
|
148
|
+
return void 0;
|
|
68
149
|
};
|
|
69
150
|
const validateTemplate = (template, attributes, schemas) => {
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
151
|
+
const ast = parseTemplateAst(template);
|
|
152
|
+
if (!ast) {
|
|
72
153
|
return "Use at least one valid {fieldName} placeholder.";
|
|
73
154
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
const attribute = currentAttributes?.[segment];
|
|
82
|
-
const isLastSegment = index2 === segments.length - 1;
|
|
83
|
-
if (!attribute || attribute.private === true) {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
if (isLastSegment) {
|
|
87
|
-
return !FIELD_TYPES.has(attribute.type ?? "");
|
|
88
|
-
}
|
|
89
|
-
const nestedSchemaUid = getNestedSchemaUid(attribute);
|
|
90
|
-
if (!nestedSchemaUid) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
currentAttributes = schemas?.get(nestedSchemaUid)?.attributes;
|
|
155
|
+
const targetSchema = { attributes };
|
|
156
|
+
const invalidExpression = ast.expressions.find(({ path, transforms }) => {
|
|
157
|
+
const fieldType = resolvePathType(path, targetSchema, (uid) => schemas?.get(uid));
|
|
158
|
+
if (!fieldType) {
|
|
159
|
+
return true;
|
|
94
160
|
}
|
|
95
|
-
return
|
|
161
|
+
return transforms.some((name) => !TRANSFORMS[name] || !TRANSFORMS[name].accepts(fieldType));
|
|
96
162
|
});
|
|
97
|
-
|
|
163
|
+
if (!invalidExpression) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const invalidTransform = invalidExpression.transforms.find(
|
|
167
|
+
(name) => !TRANSFORMS[name] || !TRANSFORMS[name].accepts(
|
|
168
|
+
resolvePathType(invalidExpression.path, targetSchema, (uid) => schemas?.get(uid))
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
return invalidTransform ? `“${invalidTransform}” is not a valid transformation for “${invalidExpression.path}”.` : `“${invalidExpression.path}” is not an available scalar field.`;
|
|
98
172
|
};
|
|
99
173
|
const getAvailableFields = (schema, schemas, prefix = "", ancestors = /* @__PURE__ */ new Set()) => {
|
|
100
174
|
if (!schema) {
|
|
@@ -191,7 +265,7 @@ const SettingsPage = () => {
|
|
|
191
265
|
);
|
|
192
266
|
const updateTemplate = (sourceUid, fieldName, template) => {
|
|
193
267
|
setSettings((current) => {
|
|
194
|
-
const sourceRelations = { ...current.relations[sourceUid]
|
|
268
|
+
const sourceRelations = { ...current.relations[sourceUid] };
|
|
195
269
|
if (template.length === 0) {
|
|
196
270
|
delete sourceRelations[fieldName];
|
|
197
271
|
} else {
|
|
@@ -4,7 +4,7 @@ import styled from "styled-components";
|
|
|
4
4
|
import { useState, useCallback, useEffect, useMemo, Fragment } from "react";
|
|
5
5
|
import { useIntl } from "react-intl";
|
|
6
6
|
import { useFetchClient } from "@strapi/strapi/admin";
|
|
7
|
-
import { g as getTranslation } from "./index-
|
|
7
|
+
import { g as getTranslation } from "./index-BeLPoAiU.mjs";
|
|
8
8
|
const FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
9
9
|
"string",
|
|
10
10
|
"text",
|
|
@@ -20,16 +20,19 @@ const FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
|
20
20
|
"time",
|
|
21
21
|
"boolean"
|
|
22
22
|
]);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
const DATE_TYPES = /* @__PURE__ */ new Set(["date", "datetime"]);
|
|
24
|
+
const SYSTEM_SCALAR_TYPES = {
|
|
25
|
+
id: "integer",
|
|
26
|
+
documentId: "string",
|
|
27
|
+
createdAt: "datetime",
|
|
28
|
+
updatedAt: "datetime",
|
|
29
|
+
publishedAt: "datetime",
|
|
30
|
+
locale: "string",
|
|
31
|
+
status: "string"
|
|
32
|
+
};
|
|
33
|
+
const SYSTEM_SCALAR_FIELDS = Object.keys(SYSTEM_SCALAR_TYPES);
|
|
34
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
35
|
+
const PATH_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
33
36
|
const getNestedSchemaUid = (attribute) => {
|
|
34
37
|
if (attribute.type === "component") {
|
|
35
38
|
return attribute.component;
|
|
@@ -39,58 +42,129 @@ const getNestedSchemaUid = (attribute) => {
|
|
|
39
42
|
}
|
|
40
43
|
return void 0;
|
|
41
44
|
};
|
|
42
|
-
const
|
|
45
|
+
const getDate = (value) => {
|
|
46
|
+
if (value instanceof Date) {
|
|
47
|
+
return Number.isNaN(value.getTime()) ? void 0 : value;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
const date = new Date(value);
|
|
53
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
54
|
+
};
|
|
55
|
+
const isDateOnly = (value) => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
56
|
+
const formatDate = (value, options, context, preserveDateOnly) => {
|
|
57
|
+
const date = getDate(value);
|
|
58
|
+
if (!date) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
const timeZone = preserveDateOnly && isDateOnly(value) ? "UTC" : context.timeZone;
|
|
62
|
+
return new Intl.DateTimeFormat(context.locale, {
|
|
63
|
+
...options,
|
|
64
|
+
...timeZone ? { timeZone } : {}
|
|
65
|
+
}).format(date);
|
|
66
|
+
};
|
|
67
|
+
const TRANSFORMS = {
|
|
68
|
+
toLocalDate: {
|
|
69
|
+
accepts: (fieldType) => DATE_TYPES.has(fieldType ?? ""),
|
|
70
|
+
apply: (value, context) => formatDate(value, { dateStyle: "short" }, context, true)
|
|
71
|
+
},
|
|
72
|
+
toLocalDateTime: {
|
|
73
|
+
accepts: (fieldType) => fieldType === "datetime",
|
|
74
|
+
apply: (value, context) => formatDate(value, { dateStyle: "short", timeStyle: "short" }, context, false)
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const parseExpression = (value) => {
|
|
78
|
+
const parts = value.split("|").map((part) => part.trim());
|
|
79
|
+
const [path, ...transforms] = parts;
|
|
80
|
+
if (!path || !PATH_PATTERN.test(path) || transforms.some((name) => !IDENTIFIER_PATTERN.test(name))) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return { path, transforms };
|
|
84
|
+
};
|
|
85
|
+
const parseTemplateAst = (template) => {
|
|
43
86
|
if (!template.trim()) {
|
|
44
87
|
return null;
|
|
45
88
|
}
|
|
46
|
-
const
|
|
47
|
-
const
|
|
89
|
+
const tokens = [];
|
|
90
|
+
const expressions = [];
|
|
48
91
|
let cursor = 0;
|
|
49
|
-
|
|
50
|
-
|
|
92
|
+
while (cursor < template.length) {
|
|
93
|
+
const openingBrace = template.indexOf("{", cursor);
|
|
94
|
+
const closingBrace = template.indexOf("}", cursor);
|
|
95
|
+
if (closingBrace !== -1 && (openingBrace === -1 || closingBrace < openingBrace)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (openingBrace === -1) {
|
|
99
|
+
tokens.push({ type: "text", value: template.slice(cursor) });
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (openingBrace > cursor) {
|
|
103
|
+
tokens.push({ type: "text", value: template.slice(cursor, openingBrace) });
|
|
104
|
+
}
|
|
105
|
+
const end = template.indexOf("}", openingBrace + 1);
|
|
106
|
+
if (end === -1) {
|
|
51
107
|
return null;
|
|
52
108
|
}
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
109
|
+
const expression = parseExpression(template.slice(openingBrace + 1, end));
|
|
110
|
+
if (!expression) {
|
|
55
111
|
return null;
|
|
56
112
|
}
|
|
57
|
-
|
|
58
|
-
|
|
113
|
+
tokens.push({ type: "expression", expression });
|
|
114
|
+
expressions.push(expression);
|
|
115
|
+
cursor = end + 1;
|
|
59
116
|
}
|
|
60
|
-
|
|
61
|
-
|
|
117
|
+
return expressions.length > 0 ? { tokens, expressions } : null;
|
|
118
|
+
};
|
|
119
|
+
const resolvePathType = (path, targetSchema, resolveSchema) => {
|
|
120
|
+
const segments = path.split(".");
|
|
121
|
+
let schema = targetSchema;
|
|
122
|
+
for (const [index, segment] of segments.entries()) {
|
|
123
|
+
const isLastSegment = index === segments.length - 1;
|
|
124
|
+
if (index === 0 && isLastSegment && SYSTEM_SCALAR_TYPES[segment]) {
|
|
125
|
+
return SYSTEM_SCALAR_TYPES[segment];
|
|
126
|
+
}
|
|
127
|
+
const attribute = schema.attributes?.[segment];
|
|
128
|
+
if (!attribute || attribute.private === true) {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
if (isLastSegment) {
|
|
132
|
+
return FIELD_TYPES.has(attribute.type ?? "") ? attribute.type : void 0;
|
|
133
|
+
}
|
|
134
|
+
const nestedSchemaUid = getNestedSchemaUid(attribute);
|
|
135
|
+
if (!nestedSchemaUid || !resolveSchema) {
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
const nestedSchema = resolveSchema(nestedSchemaUid);
|
|
139
|
+
if (!nestedSchema) {
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
schema = nestedSchema;
|
|
62
143
|
}
|
|
63
|
-
return
|
|
144
|
+
return void 0;
|
|
64
145
|
};
|
|
65
146
|
const validateTemplate = (template, attributes, schemas) => {
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
147
|
+
const ast = parseTemplateAst(template);
|
|
148
|
+
if (!ast) {
|
|
68
149
|
return "Use at least one valid {fieldName} placeholder.";
|
|
69
150
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
const attribute = currentAttributes?.[segment];
|
|
78
|
-
const isLastSegment = index === segments.length - 1;
|
|
79
|
-
if (!attribute || attribute.private === true) {
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
if (isLastSegment) {
|
|
83
|
-
return !FIELD_TYPES.has(attribute.type ?? "");
|
|
84
|
-
}
|
|
85
|
-
const nestedSchemaUid = getNestedSchemaUid(attribute);
|
|
86
|
-
if (!nestedSchemaUid) {
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
currentAttributes = schemas?.get(nestedSchemaUid)?.attributes;
|
|
151
|
+
const targetSchema = { attributes };
|
|
152
|
+
const invalidExpression = ast.expressions.find(({ path, transforms }) => {
|
|
153
|
+
const fieldType = resolvePathType(path, targetSchema, (uid) => schemas?.get(uid));
|
|
154
|
+
if (!fieldType) {
|
|
155
|
+
return true;
|
|
90
156
|
}
|
|
91
|
-
return
|
|
157
|
+
return transforms.some((name) => !TRANSFORMS[name] || !TRANSFORMS[name].accepts(fieldType));
|
|
92
158
|
});
|
|
93
|
-
|
|
159
|
+
if (!invalidExpression) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const invalidTransform = invalidExpression.transforms.find(
|
|
163
|
+
(name) => !TRANSFORMS[name] || !TRANSFORMS[name].accepts(
|
|
164
|
+
resolvePathType(invalidExpression.path, targetSchema, (uid) => schemas?.get(uid))
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
return invalidTransform ? `“${invalidTransform}” is not a valid transformation for “${invalidExpression.path}”.` : `“${invalidExpression.path}” is not an available scalar field.`;
|
|
94
168
|
};
|
|
95
169
|
const getAvailableFields = (schema, schemas, prefix = "", ancestors = /* @__PURE__ */ new Set()) => {
|
|
96
170
|
if (!schema) {
|
|
@@ -187,7 +261,7 @@ const SettingsPage = () => {
|
|
|
187
261
|
);
|
|
188
262
|
const updateTemplate = (sourceUid, fieldName, template) => {
|
|
189
263
|
setSettings((current) => {
|
|
190
|
-
const sourceRelations = { ...current.relations[sourceUid]
|
|
264
|
+
const sourceRelations = { ...current.relations[sourceUid] };
|
|
191
265
|
if (template.length === 0) {
|
|
192
266
|
delete sourceRelations[fieldName];
|
|
193
267
|
} else {
|
|
@@ -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-D8leItKF.mjs").then((module) => ({ default: module.SettingsPage })),
|
|
92
92
|
permissions: []
|
|
93
93
|
});
|
|
94
94
|
app.registerPlugin({
|
|
@@ -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-BMwx-BuP.js")).then((module2) => ({ default: module2.SettingsPage })),
|
|
93
93
|
permissions: []
|
|
94
94
|
});
|
|
95
95
|
app.registerPlugin({
|
package/dist/admin/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
exports.default = index.plugin;
|
|
2
|
+
const index = require("../_chunks/index-COuIED98.js");
|
|
3
|
+
module.exports = index.plugin;
|
package/dist/admin/index.mjs
CHANGED
|
@@ -1,14 +1 @@
|
|
|
1
|
-
|
|
2
|
-
type?: string;
|
|
3
|
-
component?: string;
|
|
4
|
-
target?: string;
|
|
5
|
-
targetModel?: string;
|
|
6
|
-
private?: boolean;
|
|
7
|
-
};
|
|
8
|
-
type TemplateSchema = {
|
|
9
|
-
attributes?: Record<string, TemplateAttribute>;
|
|
10
|
-
};
|
|
11
|
-
declare const getPlaceholders: (template: string) => string[] | null;
|
|
12
|
-
declare const validateTemplate: (template: string, attributes: Record<string, TemplateAttribute> | undefined, schemas?: ReadonlyMap<string, TemplateSchema>) => string | null;
|
|
13
|
-
declare const getAvailableFields: (schema: TemplateSchema | undefined, schemas: ReadonlyMap<string, TemplateSchema>, prefix?: string, ancestors?: Set<string>) => string[];
|
|
14
|
-
export { getAvailableFields, getPlaceholders, validateTemplate };
|
|
1
|
+
export { getAvailableFields, getPlaceholders, validateTemplate } from '../../../shared/template';
|