strapi-relation-names 0.0.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/dist/admin/SettingsPage-BQo5mpAY.mjs +199 -0
  4. package/dist/admin/SettingsPage-C1nFFJjH.js +199 -0
  5. package/dist/admin/de-BBrO1tf0.js +16 -0
  6. package/dist/admin/de-a3KtzZ7N.mjs +16 -0
  7. package/dist/admin/en-8w3y1FSw.mjs +16 -0
  8. package/dist/admin/en-DlY6ZGdo.js +16 -0
  9. package/dist/admin/index-CJO9wtUk.mjs +125 -0
  10. package/dist/admin/index-SsQXjQq0.js +124 -0
  11. package/dist/admin/index.js +4 -0
  12. package/dist/admin/index.mjs +4 -0
  13. package/dist/admin/src/index.d.ts +3 -0
  14. package/dist/admin/src/pages/SettingsPage.d.ts +2 -0
  15. package/dist/admin/src/pluginId.d.ts +1 -0
  16. package/dist/admin/src/types.d.ts +28 -0
  17. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  18. package/dist/admin/src/utils/layout.d.ts +3 -0
  19. package/dist/admin/src/utils/template.d.ts +5 -0
  20. package/dist/server/index.js +564 -0
  21. package/dist/server/index.mjs +564 -0
  22. package/dist/server/src/bootstrap.d.ts +5 -0
  23. package/dist/server/src/config/index.d.ts +12 -0
  24. package/dist/server/src/constants.d.ts +1 -0
  25. package/dist/server/src/content-types/index.d.ts +2 -0
  26. package/dist/server/src/controllers/index.d.ts +9 -0
  27. package/dist/server/src/controllers/settings.d.ts +8 -0
  28. package/dist/server/src/destroy.d.ts +5 -0
  29. package/dist/server/src/index.d.ts +66 -0
  30. package/dist/server/src/middlewares/index.d.ts +2 -0
  31. package/dist/server/src/policies/index.d.ts +2 -0
  32. package/dist/server/src/register.d.ts +5 -0
  33. package/dist/server/src/routes/admin/index.d.ts +12 -0
  34. package/dist/server/src/routes/content-api/index.d.ts +5 -0
  35. package/dist/server/src/routes/index.d.ts +18 -0
  36. package/dist/server/src/services/index.d.ts +19 -0
  37. package/dist/server/src/services/relation-labels.d.ts +14 -0
  38. package/dist/server/src/services/settings.d.ts +11 -0
  39. package/dist/server/src/services/utils/collections.d.ts +1 -0
  40. package/dist/server/src/services/utils/relation-labels.d.ts +27 -0
  41. package/dist/server/src/services/utils/relation-values.d.ts +6 -0
  42. package/dist/server/src/types.d.ts +5 -0
  43. package/package.json +77 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christoph Tupi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # 🔗 strapi-relation-names
2
+
3
+ **Clear and customizable relation labels for Strapi v5**
4
+
5
+ Display Strapi relations using readable labels built from your content fields instead of relying on the default relation name.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/strapi-relation-names)](https://www.npmjs.com/package/strapi-relation-names)
8
+ ![Strapi Version](https://img.shields.io/badge/strapi-v5-blue)
9
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
10
+ ![npm](https://img.shields.io/npm/dt/strapi-relation-names)
11
+
12
+ ---
13
+
14
+ ## ✨ Features
15
+
16
+ - 🏷️ **Custom relation labels**
17
+ - 🧩 Combine multiple fields in a single label
18
+ - ✏️ Add static text around field values
19
+ - 🔁 Supports repeated placeholders
20
+ - 📦 Supports scalar fields such as strings, numbers, booleans, and dates
21
+ - 🛡️ Falls back to Strapi's default relation label when necessary
22
+ - 🔎 Keeps Strapi's existing relation search and sorting behavior
23
+ - 🖥️ Admin-only — no changes to your Content API or stored data
24
+
25
+ ---
26
+
27
+ ## 🚀 Installation
28
+
29
+ Install via npm:
30
+
31
+ ```bash
32
+ npm install strapi-relation-names
33
+ ```
34
+
35
+ or yarn:
36
+
37
+ ```bash
38
+ yarn add strapi-relation-names
39
+ ```
40
+
41
+ or pnpm:
42
+
43
+ ```bash
44
+ pnpm add strapi-relation-names
45
+ ```
46
+
47
+ Enable the plugin:
48
+
49
+ ```javascript
50
+ // config/plugins.{js,ts}
51
+ 'strapi-relation-names': {
52
+ enabled: true,
53
+ },
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Quickstart
59
+
60
+ After installing and enabling the plugin, open the Strapi Admin settings panel:
61
+
62
+ **Settings → Global → Relation Names**
63
+
64
+ Each relation field is displayed together with the scalar fields available on its related content type or component.
65
+
66
+ Create a label using placeholders:
67
+
68
+ ```text
69
+ {firstName} {lastName}
70
+ ```
71
+
72
+ For example, given:
73
+
74
+ ```text
75
+ firstName = John
76
+ lastName = Doe
77
+ ```
78
+
79
+ the relation will be displayed as:
80
+
81
+ ```text
82
+ John Doe
83
+ ```
84
+
85
+ You can also mix placeholders with static text:
86
+
87
+ ```text
88
+ {firstName} {lastName} ({customerNumber})
89
+ ```
90
+
91
+ After saving the settings, the Strapi Admin reloads automatically so open Content Manager views use the updated labels.
92
+
93
+ ---
94
+
95
+ ## ⚙️ Templates
96
+
97
+ Templates use direct field placeholders:
98
+
99
+ ```text
100
+ {fieldName}
101
+ ```
102
+
103
+ Multiple placeholders can be combined:
104
+
105
+ ```text
106
+ {firstName} {lastName}
107
+ ```
108
+
109
+ Placeholders can also be repeated:
110
+
111
+ ```text
112
+ {name} - {name}
113
+ ```
114
+
115
+ Static text can be included anywhere:
116
+
117
+ ```text
118
+ Customer: {firstName} {lastName}
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 🗂️ How It Works
124
+
125
+ The plugin changes how relations are displayed inside the Strapi Admin panel.
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.
132
+
133
+ For example:
134
+
135
+ ```text
136
+ {firstName} {lastName}
137
+ ```
138
+
139
+ with a missing `lastName` becomes:
140
+
141
+ ```text
142
+ John
143
+ ```
144
+
145
+ If the complete rendered label is blank, Strapi automatically falls back to its default relation label.
146
+
147
+ The default label is also used when:
148
+
149
+ - No template is configured
150
+ - The configured template is invalid
151
+ - The rendered template contains no usable value
152
+
153
+ ---
154
+
155
+ ## 🔮 Planned Features
156
+
157
+ - [ ] Live label previews while editing templates
158
+ - [ ] Define default relation templates in plugin configuration
159
+ - [ ] Support nested field paths in templates
160
+ - [ ] Import and export relation-label settings
161
+ - [ ] Conditional labels and configurable fallback rules
162
+
163
+ If you have any feature requests or suggestions, please open a dedicated issue.
164
+
165
+ ## 🛑 Problems
166
+
167
+ If you encounter any issues, please feel free to open an issue on the [GitHub repo](https://github.com/TupiC/strapi-relation-names/issues/new).
168
+
169
+ ## 🛠️ Contributing
170
+
171
+ Contributions are welcome! If you have suggestions or improvements, please open an issue or submit a pull request to the `dev` branch.
@@ -0,0 +1,199 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Main, Box, Typography, Field, TextInput, Flex, Button } from "@strapi/design-system";
3
+ import { useState, useCallback, useEffect, useMemo } from "react";
4
+ import { useIntl } from "react-intl";
5
+ import { useFetchClient } from "@strapi/strapi/admin";
6
+ import { g as getTranslation } from "./index-CJO9wtUk.mjs";
7
+ const FIELD_TYPES = /* @__PURE__ */ new Set([
8
+ "string",
9
+ "text",
10
+ "email",
11
+ "uid",
12
+ "enumeration",
13
+ "integer",
14
+ "biginteger",
15
+ "decimal",
16
+ "float",
17
+ "date",
18
+ "datetime",
19
+ "time",
20
+ "boolean"
21
+ ]);
22
+ const getPlaceholders = (template) => {
23
+ if (!template.trim()) {
24
+ return null;
25
+ }
26
+ const placeholders = [];
27
+ const matches = template.matchAll(/\{([^{}]*)\}/g);
28
+ let cursor = 0;
29
+ for (const match of matches) {
30
+ if (match.index !== cursor && template.slice(cursor, match.index).includes("{")) {
31
+ return null;
32
+ }
33
+ const name = match[1];
34
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
35
+ return null;
36
+ }
37
+ placeholders.push(name);
38
+ cursor = match.index + match[0].length;
39
+ }
40
+ if (template.slice(cursor).includes("{") || template.slice(cursor).includes("}")) {
41
+ return null;
42
+ }
43
+ return placeholders.length > 0 ? placeholders : null;
44
+ };
45
+ const validateTemplate = (template, attributes) => {
46
+ const placeholders = getPlaceholders(template);
47
+ if (!placeholders) {
48
+ return "Use at least one valid {fieldName} placeholder.";
49
+ }
50
+ const invalidField = placeholders.find(
51
+ (field) => !FIELD_TYPES.has(attributes?.[field]?.type ?? "")
52
+ );
53
+ return invalidField ? `“${invalidField}” is not an available scalar field.` : null;
54
+ };
55
+ const PLUGIN_SETTINGS_PATH = "/strapi-relation-names/settings";
56
+ const getRelationTarget = (attribute) => attribute.targetModel ?? attribute.target;
57
+ const SettingsPage = () => {
58
+ const { formatMessage } = useIntl();
59
+ const { get, put } = useFetchClient();
60
+ const [schemas, setSchemas] = useState([]);
61
+ const [settings, setSettings] = useState({
62
+ collections: [],
63
+ relations: {}
64
+ });
65
+ const [isLoading, setIsLoading] = useState(true);
66
+ const [isSaving, setIsSaving] = useState(false);
67
+ const [error, setError] = useState(null);
68
+ const load = useCallback(async () => {
69
+ setIsLoading(true);
70
+ setError(null);
71
+ try {
72
+ const [initResponse, settingsResponse] = await Promise.all([
73
+ get("/content-manager/init"),
74
+ get(PLUGIN_SETTINGS_PATH)
75
+ ]);
76
+ const nextSettings = settingsResponse.data.data;
77
+ const contentTypes = initResponse.data.data.contentTypes.filter(
78
+ (schema) => nextSettings.collections.length === 0 || nextSettings.collections.includes(schema.uid)
79
+ );
80
+ setSchemas([...contentTypes, ...initResponse.data.data.components]);
81
+ setSettings(nextSettings);
82
+ } catch {
83
+ setError(formatMessage({ id: getTranslation("settings.loadError") }));
84
+ } finally {
85
+ setIsLoading(false);
86
+ }
87
+ }, [formatMessage, get]);
88
+ useEffect(() => {
89
+ void load();
90
+ }, [load]);
91
+ const schemaMap = useMemo(
92
+ () => new Map(schemas.map((schema) => [schema.uid, schema])),
93
+ [schemas]
94
+ );
95
+ const rows = useMemo(
96
+ () => schemas.filter((schema) => schema.isDisplayed !== false).flatMap(
97
+ (source) => Object.entries(source.attributes ?? {}).filter(
98
+ ([, attribute]) => attribute.type === "relation" && !attribute.relationType?.toLowerCase().includes("morph")
99
+ ).map(([fieldName, attribute]) => ({
100
+ source,
101
+ fieldName,
102
+ target: getRelationTarget(attribute) ? schemaMap.get(getRelationTarget(attribute)) : void 0
103
+ }))
104
+ ).filter((row) => row.target),
105
+ [schemaMap, schemas]
106
+ );
107
+ const updateTemplate = (sourceUid, fieldName, template) => {
108
+ setSettings((current) => {
109
+ const sourceRelations = { ...current.relations[sourceUid] ?? {} };
110
+ if (template.length === 0) {
111
+ delete sourceRelations[fieldName];
112
+ } else {
113
+ sourceRelations[fieldName] = template;
114
+ }
115
+ const relations = { ...current.relations };
116
+ if (Object.keys(sourceRelations).length === 0) {
117
+ delete relations[sourceUid];
118
+ } else {
119
+ relations[sourceUid] = sourceRelations;
120
+ }
121
+ return { ...current, relations };
122
+ });
123
+ };
124
+ const save = async () => {
125
+ setIsSaving(true);
126
+ setError(null);
127
+ try {
128
+ await put(PLUGIN_SETTINGS_PATH, settings);
129
+ window.location.reload();
130
+ } catch {
131
+ setError(formatMessage({ id: getTranslation("settings.saveError") }));
132
+ setIsSaving(false);
133
+ }
134
+ };
135
+ return /* @__PURE__ */ jsx(Main, { "aria-labelledby": "relation-names-title", children: /* @__PURE__ */ jsxs(Box, { padding: 8, children: [
136
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", id: "relation-names-title", children: formatMessage({ id: getTranslation("settings.title") }) }),
137
+ /* @__PURE__ */ jsx(Box, { paddingTop: 2, paddingBottom: 6, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: formatMessage({ id: getTranslation("settings.description") }) }) }),
138
+ isLoading ? /* @__PURE__ */ jsx(Typography, { children: formatMessage({ id: getTranslation("settings.loading") }) }) : null,
139
+ error ? /* @__PURE__ */ jsx(Typography, { textColor: "danger600", tag: "p", children: error }) : null,
140
+ !isLoading && rows.length === 0 ? /* @__PURE__ */ jsx(Typography, { children: formatMessage({ id: getTranslation("settings.noRelations") }) }) : null,
141
+ !isLoading ? rows.map(({ source, fieldName, target }) => {
142
+ const targetAttributes = target?.attributes ?? {};
143
+ const template = settings.relations[source.uid]?.[fieldName] ?? "";
144
+ const validationError = template ? validateTemplate(template, targetAttributes) : null;
145
+ return /* @__PURE__ */ jsxs(
146
+ Box,
147
+ {
148
+ padding: 4,
149
+ marginBottom: 3,
150
+ background: "neutral0",
151
+ shadow: "tableShadow",
152
+ hasRadius: true,
153
+ children: [
154
+ /* @__PURE__ */ jsxs(Typography, { variant: "beta", tag: "h2", children: [
155
+ source.info?.displayName ?? source.uid,
156
+ " · ",
157
+ fieldName
158
+ ] }),
159
+ /* @__PURE__ */ jsx(Box, { paddingTop: 1, paddingBottom: 3, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: formatMessage(
160
+ { id: getTranslation("settings.target") },
161
+ { target: target?.info?.displayName ?? target?.uid }
162
+ ) }) }),
163
+ /* @__PURE__ */ jsxs(
164
+ Field.Root,
165
+ {
166
+ error: validationError ?? void 0,
167
+ name: `${source.uid}.${fieldName}`,
168
+ children: [
169
+ /* @__PURE__ */ jsx(Field.Label, { children: formatMessage({ id: getTranslation("settings.template") }) }),
170
+ /* @__PURE__ */ jsx(
171
+ TextInput,
172
+ {
173
+ value: template,
174
+ placeholder: "{firstName} {lastName}",
175
+ onChange: (event) => updateTemplate(source.uid, fieldName, event.target.value)
176
+ }
177
+ ),
178
+ /* @__PURE__ */ jsx(Field.Hint, {}),
179
+ /* @__PURE__ */ jsx(Field.Error, {})
180
+ ]
181
+ }
182
+ ),
183
+ /* @__PURE__ */ jsx(Box, { paddingTop: 2, children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
184
+ formatMessage({ id: getTranslation("settings.availableFields") }),
185
+ ":",
186
+ " ",
187
+ Object.keys(targetAttributes).join(", ")
188
+ ] }) })
189
+ ]
190
+ },
191
+ `${source.uid}.${fieldName}`
192
+ );
193
+ }) : null,
194
+ !isLoading && rows.length > 0 ? /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", paddingTop: 4, children: /* @__PURE__ */ jsx(Button, { loading: isSaving, disabled: isSaving, onClick: () => void save(), children: formatMessage({ id: getTranslation("settings.save") }) }) }) : null
195
+ ] }) });
196
+ };
197
+ export {
198
+ SettingsPage
199
+ };
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const designSystem = require("@strapi/design-system");
5
+ const react = require("react");
6
+ const reactIntl = require("react-intl");
7
+ const admin = require("@strapi/strapi/admin");
8
+ const index = require("./index-SsQXjQq0.js");
9
+ const FIELD_TYPES = /* @__PURE__ */ new Set([
10
+ "string",
11
+ "text",
12
+ "email",
13
+ "uid",
14
+ "enumeration",
15
+ "integer",
16
+ "biginteger",
17
+ "decimal",
18
+ "float",
19
+ "date",
20
+ "datetime",
21
+ "time",
22
+ "boolean"
23
+ ]);
24
+ const getPlaceholders = (template) => {
25
+ if (!template.trim()) {
26
+ return null;
27
+ }
28
+ const placeholders = [];
29
+ const matches = template.matchAll(/\{([^{}]*)\}/g);
30
+ let cursor = 0;
31
+ for (const match of matches) {
32
+ if (match.index !== cursor && template.slice(cursor, match.index).includes("{")) {
33
+ return null;
34
+ }
35
+ const name = match[1];
36
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
37
+ return null;
38
+ }
39
+ placeholders.push(name);
40
+ cursor = match.index + match[0].length;
41
+ }
42
+ if (template.slice(cursor).includes("{") || template.slice(cursor).includes("}")) {
43
+ return null;
44
+ }
45
+ return placeholders.length > 0 ? placeholders : null;
46
+ };
47
+ const validateTemplate = (template, attributes) => {
48
+ const placeholders = getPlaceholders(template);
49
+ if (!placeholders) {
50
+ return "Use at least one valid {fieldName} placeholder.";
51
+ }
52
+ const invalidField = placeholders.find(
53
+ (field) => !FIELD_TYPES.has(attributes?.[field]?.type ?? "")
54
+ );
55
+ return invalidField ? `“${invalidField}” is not an available scalar field.` : null;
56
+ };
57
+ const PLUGIN_SETTINGS_PATH = "/strapi-relation-names/settings";
58
+ const getRelationTarget = (attribute) => attribute.targetModel ?? attribute.target;
59
+ const SettingsPage = () => {
60
+ const { formatMessage } = reactIntl.useIntl();
61
+ const { get, put } = admin.useFetchClient();
62
+ const [schemas, setSchemas] = react.useState([]);
63
+ const [settings, setSettings] = react.useState({
64
+ collections: [],
65
+ relations: {}
66
+ });
67
+ const [isLoading, setIsLoading] = react.useState(true);
68
+ const [isSaving, setIsSaving] = react.useState(false);
69
+ const [error, setError] = react.useState(null);
70
+ const load = react.useCallback(async () => {
71
+ setIsLoading(true);
72
+ setError(null);
73
+ try {
74
+ const [initResponse, settingsResponse] = await Promise.all([
75
+ get("/content-manager/init"),
76
+ get(PLUGIN_SETTINGS_PATH)
77
+ ]);
78
+ const nextSettings = settingsResponse.data.data;
79
+ const contentTypes = initResponse.data.data.contentTypes.filter(
80
+ (schema) => nextSettings.collections.length === 0 || nextSettings.collections.includes(schema.uid)
81
+ );
82
+ setSchemas([...contentTypes, ...initResponse.data.data.components]);
83
+ setSettings(nextSettings);
84
+ } catch {
85
+ setError(formatMessage({ id: index.getTranslation("settings.loadError") }));
86
+ } finally {
87
+ setIsLoading(false);
88
+ }
89
+ }, [formatMessage, get]);
90
+ react.useEffect(() => {
91
+ void load();
92
+ }, [load]);
93
+ const schemaMap = react.useMemo(
94
+ () => new Map(schemas.map((schema) => [schema.uid, schema])),
95
+ [schemas]
96
+ );
97
+ const rows = react.useMemo(
98
+ () => schemas.filter((schema) => schema.isDisplayed !== false).flatMap(
99
+ (source) => Object.entries(source.attributes ?? {}).filter(
100
+ ([, attribute]) => attribute.type === "relation" && !attribute.relationType?.toLowerCase().includes("morph")
101
+ ).map(([fieldName, attribute]) => ({
102
+ source,
103
+ fieldName,
104
+ target: getRelationTarget(attribute) ? schemaMap.get(getRelationTarget(attribute)) : void 0
105
+ }))
106
+ ).filter((row) => row.target),
107
+ [schemaMap, schemas]
108
+ );
109
+ const updateTemplate = (sourceUid, fieldName, template) => {
110
+ setSettings((current) => {
111
+ const sourceRelations = { ...current.relations[sourceUid] ?? {} };
112
+ if (template.length === 0) {
113
+ delete sourceRelations[fieldName];
114
+ } else {
115
+ sourceRelations[fieldName] = template;
116
+ }
117
+ const relations = { ...current.relations };
118
+ if (Object.keys(sourceRelations).length === 0) {
119
+ delete relations[sourceUid];
120
+ } else {
121
+ relations[sourceUid] = sourceRelations;
122
+ }
123
+ return { ...current, relations };
124
+ });
125
+ };
126
+ const save = async () => {
127
+ setIsSaving(true);
128
+ setError(null);
129
+ try {
130
+ await put(PLUGIN_SETTINGS_PATH, settings);
131
+ window.location.reload();
132
+ } catch {
133
+ setError(formatMessage({ id: index.getTranslation("settings.saveError") }));
134
+ setIsSaving(false);
135
+ }
136
+ };
137
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Main, { "aria-labelledby": "relation-names-title", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 8, children: [
138
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", tag: "h1", id: "relation-names-title", children: formatMessage({ id: index.getTranslation("settings.title") }) }),
139
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 2, paddingBottom: 6, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: formatMessage({ id: index.getTranslation("settings.description") }) }) }),
140
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { children: formatMessage({ id: index.getTranslation("settings.loading") }) }) : null,
141
+ error ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "danger600", tag: "p", children: error }) : null,
142
+ !isLoading && rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { children: formatMessage({ id: index.getTranslation("settings.noRelations") }) }) : null,
143
+ !isLoading ? rows.map(({ source, fieldName, target }) => {
144
+ const targetAttributes = target?.attributes ?? {};
145
+ const template = settings.relations[source.uid]?.[fieldName] ?? "";
146
+ const validationError = template ? validateTemplate(template, targetAttributes) : null;
147
+ return /* @__PURE__ */ jsxRuntime.jsxs(
148
+ designSystem.Box,
149
+ {
150
+ padding: 4,
151
+ marginBottom: 3,
152
+ background: "neutral0",
153
+ shadow: "tableShadow",
154
+ hasRadius: true,
155
+ children: [
156
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "beta", tag: "h2", children: [
157
+ source.info?.displayName ?? source.uid,
158
+ " · ",
159
+ fieldName
160
+ ] }),
161
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 1, paddingBottom: 3, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: formatMessage(
162
+ { id: index.getTranslation("settings.target") },
163
+ { target: target?.info?.displayName ?? target?.uid }
164
+ ) }) }),
165
+ /* @__PURE__ */ jsxRuntime.jsxs(
166
+ designSystem.Field.Root,
167
+ {
168
+ error: validationError ?? void 0,
169
+ name: `${source.uid}.${fieldName}`,
170
+ children: [
171
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: formatMessage({ id: index.getTranslation("settings.template") }) }),
172
+ /* @__PURE__ */ jsxRuntime.jsx(
173
+ designSystem.TextInput,
174
+ {
175
+ value: template,
176
+ placeholder: "{firstName} {lastName}",
177
+ onChange: (event) => updateTemplate(source.uid, fieldName, event.target.value)
178
+ }
179
+ ),
180
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
181
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {})
182
+ ]
183
+ }
184
+ ),
185
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: [
186
+ formatMessage({ id: index.getTranslation("settings.availableFields") }),
187
+ ":",
188
+ " ",
189
+ Object.keys(targetAttributes).join(", ")
190
+ ] }) })
191
+ ]
192
+ },
193
+ `${source.uid}.${fieldName}`
194
+ );
195
+ }) : null,
196
+ !isLoading && rows.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", paddingTop: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { loading: isSaving, disabled: isSaving, onClick: () => void save(), children: formatMessage({ id: index.getTranslation("settings.save") }) }) }) : null
197
+ ] }) });
198
+ };
199
+ exports.SettingsPage = SettingsPage;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const de = {
4
+ "plugin.name": "Relationsnamen",
5
+ "settings.title": "Relationsnamen",
6
+ "settings.description": "Legen Sie fest, wie verknüpfte Einträge im Content Manager angezeigt werden. Verwenden Sie ein oder mehrere skalare Felder des verknüpften Inhaltstyps, zum Beispiel Vor- und Nachname.",
7
+ "settings.target": "Verknüpfter Inhalt: {target}",
8
+ "settings.template": "Bezeichnungsvorlage",
9
+ "settings.availableFields": "Verfügbare Felder",
10
+ "settings.save": "Einstellungen speichern",
11
+ "settings.loading": "Relationsfelder werden geladen…",
12
+ "settings.noRelations": "Keine Relationsfelder verfügbar.",
13
+ "settings.loadError": "Die Einstellungen für Relationsnamen konnten nicht geladen werden.",
14
+ "settings.saveError": "Die Einstellungen für Relationsnamen konnten nicht gespeichert werden."
15
+ };
16
+ exports.default = de;
@@ -0,0 +1,16 @@
1
+ const de = {
2
+ "plugin.name": "Relationsnamen",
3
+ "settings.title": "Relationsnamen",
4
+ "settings.description": "Legen Sie fest, wie verknüpfte Einträge im Content Manager angezeigt werden. Verwenden Sie ein oder mehrere skalare Felder des verknüpften Inhaltstyps, zum Beispiel Vor- und Nachname.",
5
+ "settings.target": "Verknüpfter Inhalt: {target}",
6
+ "settings.template": "Bezeichnungsvorlage",
7
+ "settings.availableFields": "Verfügbare Felder",
8
+ "settings.save": "Einstellungen speichern",
9
+ "settings.loading": "Relationsfelder werden geladen…",
10
+ "settings.noRelations": "Keine Relationsfelder verfügbar.",
11
+ "settings.loadError": "Die Einstellungen für Relationsnamen konnten nicht geladen werden.",
12
+ "settings.saveError": "Die Einstellungen für Relationsnamen konnten nicht gespeichert werden."
13
+ };
14
+ export {
15
+ de as default
16
+ };
@@ -0,0 +1,16 @@
1
+ const en = {
2
+ "plugin.name": "Relation Names",
3
+ "settings.title": "Relation Names",
4
+ "settings.description": "Choose how related entries are displayed in the Content Manager. Use one or more scalar fields from the related content type, such as a first and last name.",
5
+ "settings.target": "Related content: {target}",
6
+ "settings.template": "Label template",
7
+ "settings.availableFields": "Available fields",
8
+ "settings.save": "Save settings",
9
+ "settings.loading": "Loading relation fields…",
10
+ "settings.noRelations": "No relation fields are available.",
11
+ "settings.loadError": "Relation Names settings could not be loaded.",
12
+ "settings.saveError": "Relation Names settings could not be saved."
13
+ };
14
+ export {
15
+ en as default
16
+ };
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const en = {
4
+ "plugin.name": "Relation Names",
5
+ "settings.title": "Relation Names",
6
+ "settings.description": "Choose how related entries are displayed in the Content Manager. Use one or more scalar fields from the related content type, such as a first and last name.",
7
+ "settings.target": "Related content: {target}",
8
+ "settings.template": "Label template",
9
+ "settings.availableFields": "Available fields",
10
+ "settings.save": "Save settings",
11
+ "settings.loading": "Loading relation fields…",
12
+ "settings.noRelations": "No relation fields are available.",
13
+ "settings.loadError": "Relation Names settings could not be loaded.",
14
+ "settings.saveError": "Relation Names settings could not be saved."
15
+ };
16
+ exports.default = en;