@tunghtml/strapi-plugin-composite-field 1.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 +38 -2
- package/dist/_chunks/{CompositeInput-DZSaB0Rv.mjs → CompositeInput-Cs0WHHpL.mjs} +52 -6
- package/dist/_chunks/{CompositeInput-BsQuuQxB.js → CompositeInput-cwqJYLtS.js} +52 -6
- package/dist/admin/index.js +35 -2
- package/dist/admin/index.mjs +35 -2
- package/dist/server/index.js +0 -4
- package/dist/server/index.mjs +0 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,8 +8,10 @@ A Strapi v5 plugin that creates composite display fields by combining multiple t
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- 🔗 **Multiple Fields**: Combine any text/string fields
|
|
11
|
+
- 🔗 **Multiple Fields**: Combine any text/string/enum fields
|
|
12
12
|
- 🔄 **Generate Button**: Click icon to generate composite value
|
|
13
|
+
- ⚡ **Auto-Generate**: Automatically update when source fields change
|
|
14
|
+
- 🔒 **Editable Control**: Make field read-only or editable
|
|
13
15
|
- 📝 **Easy Config**: Textarea input (one field per line)
|
|
14
16
|
- ⚙️ **Custom Separator**: Configure separator (default: " - ")
|
|
15
17
|
- ✨ **Auto Spacing**: Automatically adds spaces around separator
|
|
@@ -115,17 +117,51 @@ Result: Tung Le
|
|
|
115
117
|
|
|
116
118
|
## Configuration Options
|
|
117
119
|
|
|
120
|
+
### Base Settings
|
|
121
|
+
|
|
118
122
|
| Option | Type | Default | Description |
|
|
119
123
|
| ----------- | -------- | ------- | ------------------------------------- |
|
|
120
124
|
| `fields` | textarea | - | Field names to combine (one per line) |
|
|
121
125
|
| `separator` | text | `" - "` | Separator between fields |
|
|
122
126
|
|
|
127
|
+
### Advanced Settings
|
|
128
|
+
|
|
129
|
+
| Option | Type | Default | Description |
|
|
130
|
+
| -------------- | -------- | ------- | ------------------------------------------------ |
|
|
131
|
+
| `editable` | checkbox | `true` | Allow manual editing of the composite field |
|
|
132
|
+
| `autoGenerate` | checkbox | `false` | Automatically generate when source fields change |
|
|
133
|
+
|
|
134
|
+
## Advanced Usage
|
|
135
|
+
|
|
136
|
+
### Auto-Generate Mode
|
|
137
|
+
|
|
138
|
+
Enable **Auto-generate** in Advanced settings to automatically update the composite field when any source field changes. This is useful for fields that should always reflect the current values.
|
|
139
|
+
|
|
140
|
+
### Read-Only Mode
|
|
141
|
+
|
|
142
|
+
Disable **Editable** in Advanced settings to make the composite field read-only. Users can only generate values using the button or auto-generate, but cannot manually edit the field.
|
|
143
|
+
|
|
144
|
+
### Enum Field Support
|
|
145
|
+
|
|
146
|
+
The plugin now supports enumeration fields! Simply include the enum field name in your fields list, and the plugin will automatically extract the selected value.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
Fields:
|
|
152
|
+
meetingType (enum)
|
|
153
|
+
name
|
|
154
|
+
|
|
155
|
+
Result: Panel Discussion - John Doe
|
|
156
|
+
```
|
|
157
|
+
|
|
123
158
|
## Notes
|
|
124
159
|
|
|
125
|
-
-
|
|
160
|
+
- Supports text, string, and enumeration fields
|
|
126
161
|
- Automatically adds spaces around separator for clean output
|
|
127
162
|
- Empty fields are skipped automatically
|
|
128
163
|
- Relation fields are not supported (use simple fields only)
|
|
164
|
+
- Auto-generate uses a 300ms debounce to avoid excessive updates
|
|
129
165
|
|
|
130
166
|
## Requirements
|
|
131
167
|
|
|
@@ -434,6 +434,8 @@ const CompositeInput = (props) => {
|
|
|
434
434
|
}, [value]);
|
|
435
435
|
const fieldsConfig = attribute?.options?.fields || "";
|
|
436
436
|
const separator = attribute?.options?.separator || " - ";
|
|
437
|
+
const editable = attribute?.options?.editable !== false;
|
|
438
|
+
const autoGenerate = attribute?.options?.autoGenerate === true;
|
|
437
439
|
let fields = [];
|
|
438
440
|
if (typeof fieldsConfig === "string") {
|
|
439
441
|
fields = fieldsConfig.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
@@ -445,7 +447,7 @@ const CompositeInput = (props) => {
|
|
|
445
447
|
onChange({ target: { name, value: newValue, type: "text" } });
|
|
446
448
|
}
|
|
447
449
|
};
|
|
448
|
-
const handleGenerate = () => {
|
|
450
|
+
const handleGenerate = React__default.useCallback(() => {
|
|
449
451
|
const formData = {};
|
|
450
452
|
fields.forEach((fieldPath) => {
|
|
451
453
|
const input = document.querySelector(
|
|
@@ -453,6 +455,22 @@ const CompositeInput = (props) => {
|
|
|
453
455
|
);
|
|
454
456
|
if (input && input.value) {
|
|
455
457
|
formData[fieldPath] = input.value;
|
|
458
|
+
} else {
|
|
459
|
+
const enumSelect = document.querySelector(
|
|
460
|
+
`[data-strapi-field-name="${fieldPath}"] select`
|
|
461
|
+
);
|
|
462
|
+
if (enumSelect && enumSelect.value) {
|
|
463
|
+
formData[fieldPath] = enumSelect.value;
|
|
464
|
+
}
|
|
465
|
+
const enumButton = document.querySelector(
|
|
466
|
+
`[data-strapi-field-name="${fieldPath}"] button[role="combobox"]`
|
|
467
|
+
);
|
|
468
|
+
if (enumButton) {
|
|
469
|
+
const selectedText = enumButton.textContent?.trim();
|
|
470
|
+
if (selectedText && selectedText !== "Select...") {
|
|
471
|
+
formData[fieldPath] = selectedText;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
456
474
|
}
|
|
457
475
|
});
|
|
458
476
|
const parts = [];
|
|
@@ -468,7 +486,33 @@ const CompositeInput = (props) => {
|
|
|
468
486
|
if (onChange) {
|
|
469
487
|
onChange({ target: { name, value: result, type: "text" } });
|
|
470
488
|
}
|
|
471
|
-
};
|
|
489
|
+
}, [fields, separator, onChange, name]);
|
|
490
|
+
React__default.useEffect(() => {
|
|
491
|
+
if (!autoGenerate || fields.length === 0) return;
|
|
492
|
+
const handleFieldChange = () => {
|
|
493
|
+
const timeoutId = setTimeout(() => {
|
|
494
|
+
handleGenerate();
|
|
495
|
+
}, 300);
|
|
496
|
+
return () => clearTimeout(timeoutId);
|
|
497
|
+
};
|
|
498
|
+
const listeners = [];
|
|
499
|
+
fields.forEach((fieldPath) => {
|
|
500
|
+
const elements = document.querySelectorAll(
|
|
501
|
+
`input[name="${fieldPath}"], textarea[name="${fieldPath}"], select[name="${fieldPath}"], [data-strapi-field-name="${fieldPath}"] select, [data-strapi-field-name="${fieldPath}"] button`
|
|
502
|
+
);
|
|
503
|
+
elements.forEach((element) => {
|
|
504
|
+
element.addEventListener("change", handleFieldChange);
|
|
505
|
+
element.addEventListener("input", handleFieldChange);
|
|
506
|
+
listeners.push({ element, handler: handleFieldChange });
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
return () => {
|
|
510
|
+
listeners.forEach(({ element, handler }) => {
|
|
511
|
+
element.removeEventListener("change", handler);
|
|
512
|
+
element.removeEventListener("input", handler);
|
|
513
|
+
});
|
|
514
|
+
};
|
|
515
|
+
}, [autoGenerate, fields, handleGenerate]);
|
|
472
516
|
return /* @__PURE__ */ jsx(
|
|
473
517
|
Field.Root,
|
|
474
518
|
{
|
|
@@ -486,12 +530,12 @@ const CompositeInput = (props) => {
|
|
|
486
530
|
type: "text",
|
|
487
531
|
value: localValue,
|
|
488
532
|
onChange: handleChange,
|
|
489
|
-
disabled,
|
|
490
|
-
placeholder: "Click button to generate",
|
|
533
|
+
disabled: disabled || !editable,
|
|
534
|
+
placeholder: autoGenerate ? "Auto-generated from fields" : "Click button to generate",
|
|
491
535
|
style: { paddingRight: "40px" }
|
|
492
536
|
}
|
|
493
537
|
),
|
|
494
|
-
/* @__PURE__ */ jsx(
|
|
538
|
+
!autoGenerate && /* @__PURE__ */ jsx(
|
|
495
539
|
"div",
|
|
496
540
|
{
|
|
497
541
|
style: {
|
|
@@ -531,7 +575,9 @@ const CompositeInput = (props) => {
|
|
|
531
575
|
] }),
|
|
532
576
|
fields.length > 0 && /* @__PURE__ */ jsx(Field.Hint, { children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
|
|
533
577
|
"Combines: ",
|
|
534
|
-
fields.join(", ")
|
|
578
|
+
fields.join(", "),
|
|
579
|
+
autoGenerate && " (auto-generated)",
|
|
580
|
+
!editable && " (read-only)"
|
|
535
581
|
] }) }),
|
|
536
582
|
/* @__PURE__ */ jsx(Field.Error, {})
|
|
537
583
|
] })
|
|
@@ -453,6 +453,8 @@ const CompositeInput = (props) => {
|
|
|
453
453
|
}, [value]);
|
|
454
454
|
const fieldsConfig = attribute?.options?.fields || "";
|
|
455
455
|
const separator = attribute?.options?.separator || " - ";
|
|
456
|
+
const editable = attribute?.options?.editable !== false;
|
|
457
|
+
const autoGenerate = attribute?.options?.autoGenerate === true;
|
|
456
458
|
let fields = [];
|
|
457
459
|
if (typeof fieldsConfig === "string") {
|
|
458
460
|
fields = fieldsConfig.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
@@ -464,7 +466,7 @@ const CompositeInput = (props) => {
|
|
|
464
466
|
onChange({ target: { name, value: newValue, type: "text" } });
|
|
465
467
|
}
|
|
466
468
|
};
|
|
467
|
-
const handleGenerate = () => {
|
|
469
|
+
const handleGenerate = React__namespace.default.useCallback(() => {
|
|
468
470
|
const formData = {};
|
|
469
471
|
fields.forEach((fieldPath) => {
|
|
470
472
|
const input = document.querySelector(
|
|
@@ -472,6 +474,22 @@ const CompositeInput = (props) => {
|
|
|
472
474
|
);
|
|
473
475
|
if (input && input.value) {
|
|
474
476
|
formData[fieldPath] = input.value;
|
|
477
|
+
} else {
|
|
478
|
+
const enumSelect = document.querySelector(
|
|
479
|
+
`[data-strapi-field-name="${fieldPath}"] select`
|
|
480
|
+
);
|
|
481
|
+
if (enumSelect && enumSelect.value) {
|
|
482
|
+
formData[fieldPath] = enumSelect.value;
|
|
483
|
+
}
|
|
484
|
+
const enumButton = document.querySelector(
|
|
485
|
+
`[data-strapi-field-name="${fieldPath}"] button[role="combobox"]`
|
|
486
|
+
);
|
|
487
|
+
if (enumButton) {
|
|
488
|
+
const selectedText = enumButton.textContent?.trim();
|
|
489
|
+
if (selectedText && selectedText !== "Select...") {
|
|
490
|
+
formData[fieldPath] = selectedText;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
475
493
|
}
|
|
476
494
|
});
|
|
477
495
|
const parts = [];
|
|
@@ -487,7 +505,33 @@ const CompositeInput = (props) => {
|
|
|
487
505
|
if (onChange) {
|
|
488
506
|
onChange({ target: { name, value: result, type: "text" } });
|
|
489
507
|
}
|
|
490
|
-
};
|
|
508
|
+
}, [fields, separator, onChange, name]);
|
|
509
|
+
React__namespace.default.useEffect(() => {
|
|
510
|
+
if (!autoGenerate || fields.length === 0) return;
|
|
511
|
+
const handleFieldChange = () => {
|
|
512
|
+
const timeoutId = setTimeout(() => {
|
|
513
|
+
handleGenerate();
|
|
514
|
+
}, 300);
|
|
515
|
+
return () => clearTimeout(timeoutId);
|
|
516
|
+
};
|
|
517
|
+
const listeners = [];
|
|
518
|
+
fields.forEach((fieldPath) => {
|
|
519
|
+
const elements = document.querySelectorAll(
|
|
520
|
+
`input[name="${fieldPath}"], textarea[name="${fieldPath}"], select[name="${fieldPath}"], [data-strapi-field-name="${fieldPath}"] select, [data-strapi-field-name="${fieldPath}"] button`
|
|
521
|
+
);
|
|
522
|
+
elements.forEach((element) => {
|
|
523
|
+
element.addEventListener("change", handleFieldChange);
|
|
524
|
+
element.addEventListener("input", handleFieldChange);
|
|
525
|
+
listeners.push({ element, handler: handleFieldChange });
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
return () => {
|
|
529
|
+
listeners.forEach(({ element, handler }) => {
|
|
530
|
+
element.removeEventListener("change", handler);
|
|
531
|
+
element.removeEventListener("input", handler);
|
|
532
|
+
});
|
|
533
|
+
};
|
|
534
|
+
}, [autoGenerate, fields, handleGenerate]);
|
|
491
535
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
492
536
|
designSystem.Field.Root,
|
|
493
537
|
{
|
|
@@ -505,12 +549,12 @@ const CompositeInput = (props) => {
|
|
|
505
549
|
type: "text",
|
|
506
550
|
value: localValue,
|
|
507
551
|
onChange: handleChange,
|
|
508
|
-
disabled,
|
|
509
|
-
placeholder: "Click button to generate",
|
|
552
|
+
disabled: disabled || !editable,
|
|
553
|
+
placeholder: autoGenerate ? "Auto-generated from fields" : "Click button to generate",
|
|
510
554
|
style: { paddingRight: "40px" }
|
|
511
555
|
}
|
|
512
556
|
),
|
|
513
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
557
|
+
!autoGenerate && /* @__PURE__ */ jsxRuntime.jsx(
|
|
514
558
|
"div",
|
|
515
559
|
{
|
|
516
560
|
style: {
|
|
@@ -550,7 +594,9 @@ const CompositeInput = (props) => {
|
|
|
550
594
|
] }),
|
|
551
595
|
fields.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: [
|
|
552
596
|
"Combines: ",
|
|
553
|
-
fields.join(", ")
|
|
597
|
+
fields.join(", "),
|
|
598
|
+
autoGenerate && " (auto-generated)",
|
|
599
|
+
!editable && " (read-only)"
|
|
554
600
|
] }) }),
|
|
555
601
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {})
|
|
556
602
|
] })
|
package/dist/admin/index.js
CHANGED
|
@@ -37,7 +37,7 @@ const index = {
|
|
|
37
37
|
defaultMessage: "Combine multiple fields into one display value"
|
|
38
38
|
},
|
|
39
39
|
components: {
|
|
40
|
-
Input: async () => Promise.resolve().then(() => require("../_chunks/CompositeInput-
|
|
40
|
+
Input: async () => Promise.resolve().then(() => require("../_chunks/CompositeInput-cwqJYLtS.js"))
|
|
41
41
|
},
|
|
42
42
|
options: {
|
|
43
43
|
base: [
|
|
@@ -82,7 +82,40 @@ const index = {
|
|
|
82
82
|
]
|
|
83
83
|
}
|
|
84
84
|
],
|
|
85
|
-
advanced: [
|
|
85
|
+
advanced: [
|
|
86
|
+
{
|
|
87
|
+
sectionTitle: {
|
|
88
|
+
id: "composite-field.advanced.title",
|
|
89
|
+
defaultMessage: "Settings"
|
|
90
|
+
},
|
|
91
|
+
items: [
|
|
92
|
+
{
|
|
93
|
+
name: "options.editable",
|
|
94
|
+
type: "checkbox",
|
|
95
|
+
intlLabel: {
|
|
96
|
+
id: "composite-field.editable.label",
|
|
97
|
+
defaultMessage: "Editable"
|
|
98
|
+
},
|
|
99
|
+
description: {
|
|
100
|
+
id: "composite-field.editable.description",
|
|
101
|
+
defaultMessage: "Allow manual editing of the composite field value"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "options.autoGenerate",
|
|
106
|
+
type: "checkbox",
|
|
107
|
+
intlLabel: {
|
|
108
|
+
id: "composite-field.autoGenerate.label",
|
|
109
|
+
defaultMessage: "Auto-generate"
|
|
110
|
+
},
|
|
111
|
+
description: {
|
|
112
|
+
id: "composite-field.autoGenerate.description",
|
|
113
|
+
defaultMessage: "Automatically generate value when source fields change"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
]
|
|
86
119
|
}
|
|
87
120
|
});
|
|
88
121
|
},
|
package/dist/admin/index.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const index = {
|
|
|
34
34
|
defaultMessage: "Combine multiple fields into one display value"
|
|
35
35
|
},
|
|
36
36
|
components: {
|
|
37
|
-
Input: async () => import("../_chunks/CompositeInput-
|
|
37
|
+
Input: async () => import("../_chunks/CompositeInput-Cs0WHHpL.mjs")
|
|
38
38
|
},
|
|
39
39
|
options: {
|
|
40
40
|
base: [
|
|
@@ -79,7 +79,40 @@ const index = {
|
|
|
79
79
|
]
|
|
80
80
|
}
|
|
81
81
|
],
|
|
82
|
-
advanced: [
|
|
82
|
+
advanced: [
|
|
83
|
+
{
|
|
84
|
+
sectionTitle: {
|
|
85
|
+
id: "composite-field.advanced.title",
|
|
86
|
+
defaultMessage: "Settings"
|
|
87
|
+
},
|
|
88
|
+
items: [
|
|
89
|
+
{
|
|
90
|
+
name: "options.editable",
|
|
91
|
+
type: "checkbox",
|
|
92
|
+
intlLabel: {
|
|
93
|
+
id: "composite-field.editable.label",
|
|
94
|
+
defaultMessage: "Editable"
|
|
95
|
+
},
|
|
96
|
+
description: {
|
|
97
|
+
id: "composite-field.editable.description",
|
|
98
|
+
defaultMessage: "Allow manual editing of the composite field value"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "options.autoGenerate",
|
|
103
|
+
type: "checkbox",
|
|
104
|
+
intlLabel: {
|
|
105
|
+
id: "composite-field.autoGenerate.label",
|
|
106
|
+
defaultMessage: "Auto-generate"
|
|
107
|
+
},
|
|
108
|
+
description: {
|
|
109
|
+
id: "composite-field.autoGenerate.description",
|
|
110
|
+
defaultMessage: "Automatically generate value when source fields change"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
]
|
|
83
116
|
}
|
|
84
117
|
});
|
|
85
118
|
},
|
package/dist/server/index.js
CHANGED
|
@@ -3,15 +3,11 @@ function getDefaultExportFromCjs(x) {
|
|
|
3
3
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
|
|
4
4
|
}
|
|
5
5
|
var register$1 = ({ strapi }) => {
|
|
6
|
-
console.log("🔌 Composite Field Plugin: Registering custom field...");
|
|
7
6
|
strapi.customFields.register({
|
|
8
7
|
name: "composite",
|
|
9
8
|
plugin: "composite-field",
|
|
10
9
|
type: "string"
|
|
11
10
|
});
|
|
12
|
-
console.log(
|
|
13
|
-
"✅ Composite Field Plugin: Custom field registered successfully"
|
|
14
|
-
);
|
|
15
11
|
};
|
|
16
12
|
const register = register$1;
|
|
17
13
|
var src = {
|
package/dist/server/index.mjs
CHANGED
|
@@ -2,15 +2,11 @@ function getDefaultExportFromCjs(x) {
|
|
|
2
2
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
|
|
3
3
|
}
|
|
4
4
|
var register$1 = ({ strapi }) => {
|
|
5
|
-
console.log("🔌 Composite Field Plugin: Registering custom field...");
|
|
6
5
|
strapi.customFields.register({
|
|
7
6
|
name: "composite",
|
|
8
7
|
plugin: "composite-field",
|
|
9
8
|
type: "string"
|
|
10
9
|
});
|
|
11
|
-
console.log(
|
|
12
|
-
"✅ Composite Field Plugin: Custom field registered successfully"
|
|
13
|
-
);
|
|
14
10
|
};
|
|
15
11
|
const register = register$1;
|
|
16
12
|
var src = {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tunghtml/strapi-plugin-composite-field",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A Strapi v5 plugin to create composite display fields by combining multiple text fields",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A Strapi v5 plugin to create composite display fields by combining multiple text, string, and enum fields with auto-generate support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strapi",
|
|
7
7
|
"strapi-plugin",
|