@tunghtml/strapi-plugin-composite-field 1.2.3 → 1.2.5
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 +43 -7
- package/admin/src/components/CompositeInput.jsx +41 -11
- package/package.json +6 -2
- package/strapi-admin.js +47 -60
package/README.md
CHANGED
|
@@ -11,10 +11,10 @@ A Strapi v5 plugin that creates composite display fields by combining multiple t
|
|
|
11
11
|
- 🔗 **Multiple Fields**: Combine any text/string/enum fields
|
|
12
12
|
- 🔄 **Generate Button**: Click icon to generate composite value
|
|
13
13
|
- ⚡ **Auto-Generate**: Automatically update when source fields change
|
|
14
|
-
- 🔒 **Editable Control**: Make field read-only or editable
|
|
15
14
|
- 📝 **Easy Config**: Textarea input (one field per line)
|
|
16
15
|
- ⚙️ **Custom Separator**: Configure separator (default: " - ")
|
|
17
|
-
- ✨ **
|
|
16
|
+
- ✨ **Smart Spacing**: Automatically handles spacing for separators
|
|
17
|
+
- 🎯 **Manual Edit Protection**: Preserves manual edits until source fields change
|
|
18
18
|
- 🎨 **Clean UI**: Matches Strapi admin design system
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
@@ -28,7 +28,7 @@ Then add the plugin to your `config/plugins.js`:
|
|
|
28
28
|
```javascript
|
|
29
29
|
module.exports = {
|
|
30
30
|
// ...
|
|
31
|
-
|
|
31
|
+
'composite-field': {
|
|
32
32
|
enabled: true,
|
|
33
33
|
},
|
|
34
34
|
};
|
|
@@ -128,18 +128,28 @@ Result: Tung Le
|
|
|
128
128
|
|
|
129
129
|
| Option | Type | Default | Description |
|
|
130
130
|
| -------------- | -------- | ------- | ------------------------------------------------ |
|
|
131
|
-
| `editable` | checkbox | `true` | Allow manual editing of the composite field |
|
|
132
131
|
| `autoGenerate` | checkbox | `false` | Automatically generate when source fields change |
|
|
133
132
|
|
|
134
133
|
## Advanced Usage
|
|
135
134
|
|
|
136
135
|
### Auto-Generate Mode
|
|
137
136
|
|
|
138
|
-
Enable **Auto-generate** in Advanced settings to automatically update the composite field when any source field changes.
|
|
137
|
+
Enable **Auto-generate** in Advanced settings to automatically update the composite field when any source field changes.
|
|
139
138
|
|
|
140
|
-
|
|
139
|
+
**Smart Manual Edit Protection**: When auto-generate is enabled, the plugin intelligently handles manual edits:
|
|
141
140
|
|
|
142
|
-
|
|
141
|
+
- If you manually edit the composite field, it will preserve your changes
|
|
142
|
+
- When any source field changes, auto-generation resumes with the new values
|
|
143
|
+
- This prevents accidental overwrites while maintaining automatic updates
|
|
144
|
+
|
|
145
|
+
### Space-Only Separator
|
|
146
|
+
|
|
147
|
+
When using a space `" "` as the separator, the plugin automatically uses a single space without adding extra spaces around it. For other separators like `-` or `|`, spaces are added around them for better readability.
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
|
|
151
|
+
- Separator: `" "` → Result: `John Doe`
|
|
152
|
+
- Separator: `-` → Result: `John - Doe`
|
|
143
153
|
|
|
144
154
|
### Enum Field Support
|
|
145
155
|
|
|
@@ -163,6 +173,28 @@ Result: Panel Discussion - John Doe
|
|
|
163
173
|
- Relation fields are not supported (use simple fields only)
|
|
164
174
|
- Auto-generate uses a 300ms debounce to avoid excessive updates
|
|
165
175
|
|
|
176
|
+
## Changelog
|
|
177
|
+
|
|
178
|
+
### v1.2.4 (2026-02-05)
|
|
179
|
+
|
|
180
|
+
**🎯 Improvements:**
|
|
181
|
+
|
|
182
|
+
- **Smart Manual Edit Protection**: Auto-generate now respects manual edits and only resumes when source fields change
|
|
183
|
+
- **Improved Separator Logic**: Space-only separators now use single space without extra padding
|
|
184
|
+
- **Removed Editable Option**: Simplified configuration by removing the unnecessary editable toggle
|
|
185
|
+
|
|
186
|
+
**🐛 Bug Fixes:**
|
|
187
|
+
|
|
188
|
+
- Fixed auto-generate overwriting manual edits immediately
|
|
189
|
+
- Fixed excessive spacing when using space as separator
|
|
190
|
+
- Improved change detection for more reliable auto-generation
|
|
191
|
+
|
|
192
|
+
**🔧 Technical:**
|
|
193
|
+
|
|
194
|
+
- Added `isManuallyEdited` state tracking
|
|
195
|
+
- Enhanced `watchedValues` change detection
|
|
196
|
+
- Optimized re-render performance
|
|
197
|
+
|
|
166
198
|
## Requirements
|
|
167
199
|
|
|
168
200
|
- Strapi v5.0.0 or higher
|
|
@@ -184,3 +216,7 @@ MIT
|
|
|
184
216
|
## Author
|
|
185
217
|
|
|
186
218
|
Tung Le - [@finnwasabi](https://github.com/finnwasabi)
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
Built by **Tung Le**. More Strapi plugins and case studies at [tungln.is-a.dev](https://tungln.is-a.dev).
|
|
@@ -25,18 +25,21 @@ const CompositeInput = (props) => {
|
|
|
25
25
|
const { values } = form;
|
|
26
26
|
|
|
27
27
|
const [localValue, setLocalValue] = React.useState(value || '');
|
|
28
|
+
const [isManuallyEdited, setIsManuallyEdited] = React.useState(false);
|
|
28
29
|
|
|
29
30
|
const lastGeneratedVal = React.useRef(value || '');
|
|
31
|
+
const lastWatchedValues = React.useRef([]);
|
|
30
32
|
|
|
31
33
|
React.useEffect(() => {
|
|
32
34
|
const validValue = value || '';
|
|
33
35
|
setLocalValue(validValue);
|
|
34
36
|
lastGeneratedVal.current = validValue;
|
|
37
|
+
// Reset manual edit flag when value changes from outside (e.g., form reset)
|
|
38
|
+
setIsManuallyEdited(false);
|
|
35
39
|
}, [value]);
|
|
36
40
|
|
|
37
41
|
const fieldsConfig = attribute?.options?.fields || '';
|
|
38
42
|
const separator = attribute?.options?.separator || ' - ';
|
|
39
|
-
const editable = attribute?.options?.editable !== false;
|
|
40
43
|
const autoGenerate = attribute?.options?.autoGenerate === true;
|
|
41
44
|
|
|
42
45
|
// Parse fields
|
|
@@ -54,6 +57,8 @@ const CompositeInput = (props) => {
|
|
|
54
57
|
const newValue = e.target.value;
|
|
55
58
|
setLocalValue(newValue);
|
|
56
59
|
lastGeneratedVal.current = newValue;
|
|
60
|
+
// Mark as manually edited when user types
|
|
61
|
+
setIsManuallyEdited(true);
|
|
57
62
|
if (onChange) {
|
|
58
63
|
onChange({ target: { name, value: newValue, type: 'text' } });
|
|
59
64
|
}
|
|
@@ -131,13 +136,22 @@ const CompositeInput = (props) => {
|
|
|
131
136
|
}
|
|
132
137
|
});
|
|
133
138
|
|
|
134
|
-
//
|
|
139
|
+
// Handle separator logic - if separator is just a space, don't add extra spaces
|
|
135
140
|
const cleanSeparator = separator.trim();
|
|
136
|
-
|
|
141
|
+
let result;
|
|
142
|
+
if (cleanSeparator === '') {
|
|
143
|
+
// If separator is just spaces, use single space
|
|
144
|
+
result = generatedParts.join(' ').trim();
|
|
145
|
+
} else {
|
|
146
|
+
// For other separators, add spaces around them
|
|
147
|
+
result = generatedParts.join(` ${cleanSeparator} `).trim();
|
|
148
|
+
}
|
|
137
149
|
|
|
138
150
|
if (result !== lastGeneratedVal.current) {
|
|
139
151
|
setLocalValue(result);
|
|
140
152
|
lastGeneratedVal.current = result;
|
|
153
|
+
// Reset manual edit flag when auto-generating
|
|
154
|
+
setIsManuallyEdited(false);
|
|
141
155
|
|
|
142
156
|
if (onChange) {
|
|
143
157
|
onChange({ target: { name, value: result, type: 'text' } });
|
|
@@ -161,12 +175,29 @@ const CompositeInput = (props) => {
|
|
|
161
175
|
React.useEffect(() => {
|
|
162
176
|
if (!autoGenerate || fields.length === 0) return;
|
|
163
177
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
// Check if watched values actually changed
|
|
179
|
+
const currentWatchedValues = JSON.stringify(watchedValues);
|
|
180
|
+
const lastWatchedValuesStr = JSON.stringify(lastWatchedValues.current);
|
|
181
|
+
|
|
182
|
+
// Only auto-generate if:
|
|
183
|
+
// 1. Values actually changed
|
|
184
|
+
// 2. User hasn't manually edited the field
|
|
185
|
+
if (currentWatchedValues !== lastWatchedValuesStr) {
|
|
186
|
+
lastWatchedValues.current = watchedValues;
|
|
187
|
+
|
|
188
|
+
if (!isManuallyEdited) {
|
|
189
|
+
const timeoutId = setTimeout(() => {
|
|
190
|
+
handleGenerate();
|
|
191
|
+
}, 300); // Debounce
|
|
192
|
+
|
|
193
|
+
return () => clearTimeout(timeoutId);
|
|
194
|
+
} else {
|
|
195
|
+
// If manually edited, reset the flag when source fields change
|
|
196
|
+
// This allows auto-generation to resume after source field changes
|
|
197
|
+
setIsManuallyEdited(false);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}, [autoGenerate, fields, handleGenerate, watchedValues, isManuallyEdited]);
|
|
170
201
|
|
|
171
202
|
if (!props) {
|
|
172
203
|
return null;
|
|
@@ -192,7 +223,7 @@ const CompositeInput = (props) => {
|
|
|
192
223
|
type="text"
|
|
193
224
|
value={localValue}
|
|
194
225
|
onChange={handleChange}
|
|
195
|
-
disabled={disabled
|
|
226
|
+
disabled={disabled}
|
|
196
227
|
placeholder={
|
|
197
228
|
autoGenerate
|
|
198
229
|
? 'Auto-generated from fields'
|
|
@@ -242,7 +273,6 @@ const CompositeInput = (props) => {
|
|
|
242
273
|
<Typography variant="pi" textColor="neutral600">
|
|
243
274
|
Combines: {fields.join(', ')}
|
|
244
275
|
{autoGenerate && ' (auto-generated)'}
|
|
245
|
-
{!editable && ' (read-only)'}
|
|
246
276
|
</Typography>
|
|
247
277
|
</Field.Hint>
|
|
248
278
|
)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tunghtml/strapi-plugin-composite-field",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
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",
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
"url": "git+https://github.com/finnwasabi/strapi-plugin-composite-field.git"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
|
-
"author":
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "Tung Le",
|
|
23
|
+
"email": "nhattung1511@gmail.com",
|
|
24
|
+
"url": "https://tungln.is-a.dev"
|
|
25
|
+
},
|
|
22
26
|
"files": [
|
|
23
27
|
"admin",
|
|
24
28
|
"server",
|
package/strapi-admin.js
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
import React from
|
|
1
|
+
import React from 'react';
|
|
2
2
|
|
|
3
3
|
const CompositeIcon = () =>
|
|
4
4
|
React.createElement(
|
|
5
|
-
|
|
5
|
+
'div',
|
|
6
6
|
{
|
|
7
7
|
style: {
|
|
8
|
-
display:
|
|
9
|
-
justifyContent:
|
|
10
|
-
alignItems:
|
|
11
|
-
width:
|
|
12
|
-
height:
|
|
13
|
-
borderRadius:
|
|
14
|
-
border:
|
|
15
|
-
background:
|
|
16
|
-
fontSize:
|
|
17
|
-
fontWeight:
|
|
18
|
-
color:
|
|
8
|
+
display: 'flex',
|
|
9
|
+
justifyContent: 'center',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
width: '32px',
|
|
12
|
+
height: '24px',
|
|
13
|
+
borderRadius: '4px',
|
|
14
|
+
border: '1px solid #b8e1ff',
|
|
15
|
+
background: '#eafbff',
|
|
16
|
+
fontSize: '14px',
|
|
17
|
+
fontWeight: '600',
|
|
18
|
+
color: '#0c75af',
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
|
-
|
|
21
|
+
'Aa'
|
|
22
22
|
);
|
|
23
23
|
|
|
24
24
|
export default {
|
|
25
25
|
register(app) {
|
|
26
26
|
app.customFields.register({
|
|
27
|
-
name:
|
|
28
|
-
pluginId:
|
|
29
|
-
type:
|
|
27
|
+
name: 'composite',
|
|
28
|
+
pluginId: 'composite-field',
|
|
29
|
+
type: 'string',
|
|
30
30
|
icon: CompositeIcon,
|
|
31
31
|
intlLabel: {
|
|
32
|
-
id:
|
|
33
|
-
defaultMessage:
|
|
32
|
+
id: 'composite-field.label',
|
|
33
|
+
defaultMessage: 'Composite',
|
|
34
34
|
},
|
|
35
35
|
intlDescription: {
|
|
36
|
-
id:
|
|
37
|
-
defaultMessage:
|
|
36
|
+
id: 'composite-field.description',
|
|
37
|
+
defaultMessage: 'Combine multiple fields into one display value',
|
|
38
38
|
},
|
|
39
39
|
components: {
|
|
40
|
-
Input: async () => import(
|
|
40
|
+
Input: async () => import('./admin/src/components/CompositeInput'),
|
|
41
41
|
},
|
|
42
42
|
options: {
|
|
43
43
|
base: [
|
|
44
44
|
{
|
|
45
45
|
sectionTitle: {
|
|
46
|
-
id:
|
|
47
|
-
defaultMessage:
|
|
46
|
+
id: 'composite-field.section.title',
|
|
47
|
+
defaultMessage: 'Settings',
|
|
48
48
|
},
|
|
49
49
|
items: [
|
|
50
50
|
{
|
|
51
|
-
name:
|
|
52
|
-
type:
|
|
51
|
+
name: 'options.fields',
|
|
52
|
+
type: 'textarea',
|
|
53
53
|
intlLabel: {
|
|
54
|
-
id:
|
|
55
|
-
defaultMessage:
|
|
54
|
+
id: 'composite-field.fields.label',
|
|
55
|
+
defaultMessage: 'Fields to combine',
|
|
56
56
|
},
|
|
57
57
|
description: {
|
|
58
|
-
id:
|
|
58
|
+
id: 'composite-field.fields.description',
|
|
59
59
|
defaultMessage:
|
|
60
|
-
|
|
60
|
+
'Enter field names (one per line). Only text/string fields are supported.',
|
|
61
61
|
},
|
|
62
62
|
placeholder: {
|
|
63
|
-
id:
|
|
64
|
-
defaultMessage:
|
|
63
|
+
id: 'composite-field.fields.placeholder',
|
|
64
|
+
defaultMessage: 'salutation\nfirstName\nlastName\nemail',
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
|
-
name:
|
|
69
|
-
type:
|
|
68
|
+
name: 'options.separator',
|
|
69
|
+
type: 'text',
|
|
70
70
|
intlLabel: {
|
|
71
|
-
id:
|
|
72
|
-
defaultMessage:
|
|
71
|
+
id: 'composite-field.separator.label',
|
|
72
|
+
defaultMessage: 'Separator',
|
|
73
73
|
},
|
|
74
74
|
description: {
|
|
75
|
-
id:
|
|
75
|
+
id: 'composite-field.separator.description',
|
|
76
76
|
defaultMessage: 'Separator between fields (default: " - ")',
|
|
77
77
|
},
|
|
78
78
|
placeholder: {
|
|
79
|
-
id:
|
|
80
|
-
defaultMessage:
|
|
79
|
+
id: 'composite-field.separator.placeholder',
|
|
80
|
+
defaultMessage: ' - ',
|
|
81
81
|
},
|
|
82
82
|
},
|
|
83
83
|
],
|
|
@@ -86,34 +86,21 @@ export default {
|
|
|
86
86
|
advanced: [
|
|
87
87
|
{
|
|
88
88
|
sectionTitle: {
|
|
89
|
-
id:
|
|
90
|
-
defaultMessage:
|
|
89
|
+
id: 'composite-field.advanced.title',
|
|
90
|
+
defaultMessage: 'Settings',
|
|
91
91
|
},
|
|
92
92
|
items: [
|
|
93
93
|
{
|
|
94
|
-
name:
|
|
95
|
-
type:
|
|
94
|
+
name: 'options.autoGenerate',
|
|
95
|
+
type: 'checkbox',
|
|
96
96
|
intlLabel: {
|
|
97
|
-
id:
|
|
98
|
-
defaultMessage:
|
|
97
|
+
id: 'composite-field.autoGenerate.label',
|
|
98
|
+
defaultMessage: 'Auto-generate',
|
|
99
99
|
},
|
|
100
100
|
description: {
|
|
101
|
-
id:
|
|
101
|
+
id: 'composite-field.autoGenerate.description',
|
|
102
102
|
defaultMessage:
|
|
103
|
-
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: "options.autoGenerate",
|
|
108
|
-
type: "checkbox",
|
|
109
|
-
intlLabel: {
|
|
110
|
-
id: "composite-field.autoGenerate.label",
|
|
111
|
-
defaultMessage: "Auto-generate",
|
|
112
|
-
},
|
|
113
|
-
description: {
|
|
114
|
-
id: "composite-field.autoGenerate.description",
|
|
115
|
-
defaultMessage:
|
|
116
|
-
"Automatically generate value when source fields change",
|
|
103
|
+
'Automatically generate value when source fields change',
|
|
117
104
|
},
|
|
118
105
|
},
|
|
119
106
|
],
|