@tunghtml/strapi-plugin-composite-field 1.2.3 → 1.2.4

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 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
- - ✨ **Auto Spacing**: Automatically adds spaces around separator
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
- "composite-field": {
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. This is useful for fields that should always reflect the current values.
137
+ Enable **Auto-generate** in Advanced settings to automatically update the composite field when any source field changes.
139
138
 
140
- ### Read-Only Mode
139
+ **Smart Manual Edit Protection**: When auto-generate is enabled, the plugin intelligently handles manual edits:
141
140
 
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.
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
@@ -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
- // Ensure separator has spaces around it
139
+ // Handle separator logic - if separator is just a space, don't add extra spaces
135
140
  const cleanSeparator = separator.trim();
136
- const result = generatedParts.join(` ${cleanSeparator} `).trim();
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
- const timeoutId = setTimeout(() => {
165
- handleGenerate();
166
- }, 300); // Debounce
167
-
168
- return () => clearTimeout(timeoutId);
169
- }, [autoGenerate, fields, handleGenerate, watchedValues, name]);
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 || !editable}
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",
3
+ "version": "1.2.4",
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",
package/strapi-admin.js CHANGED
@@ -1,83 +1,83 @@
1
- import React from "react";
1
+ import React from 'react';
2
2
 
3
3
  const CompositeIcon = () =>
4
4
  React.createElement(
5
- "div",
5
+ 'div',
6
6
  {
7
7
  style: {
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",
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
- "Aa"
21
+ 'Aa'
22
22
  );
23
23
 
24
24
  export default {
25
25
  register(app) {
26
26
  app.customFields.register({
27
- name: "composite",
28
- pluginId: "composite-field",
29
- type: "string",
27
+ name: 'composite',
28
+ pluginId: 'composite-field',
29
+ type: 'string',
30
30
  icon: CompositeIcon,
31
31
  intlLabel: {
32
- id: "composite-field.label",
33
- defaultMessage: "Composite",
32
+ id: 'composite-field.label',
33
+ defaultMessage: 'Composite',
34
34
  },
35
35
  intlDescription: {
36
- id: "composite-field.description",
37
- defaultMessage: "Combine multiple fields into one display value",
36
+ id: 'composite-field.description',
37
+ defaultMessage: 'Combine multiple fields into one display value',
38
38
  },
39
39
  components: {
40
- Input: async () => import("./admin/src/components/CompositeInput"),
40
+ Input: async () => import('./admin/src/components/CompositeInput'),
41
41
  },
42
42
  options: {
43
43
  base: [
44
44
  {
45
45
  sectionTitle: {
46
- id: "composite-field.section.title",
47
- defaultMessage: "Settings",
46
+ id: 'composite-field.section.title',
47
+ defaultMessage: 'Settings',
48
48
  },
49
49
  items: [
50
50
  {
51
- name: "options.fields",
52
- type: "textarea",
51
+ name: 'options.fields',
52
+ type: 'textarea',
53
53
  intlLabel: {
54
- id: "composite-field.fields.label",
55
- defaultMessage: "Fields to combine",
54
+ id: 'composite-field.fields.label',
55
+ defaultMessage: 'Fields to combine',
56
56
  },
57
57
  description: {
58
- id: "composite-field.fields.description",
58
+ id: 'composite-field.fields.description',
59
59
  defaultMessage:
60
- "Enter field names (one per line). Only text/string fields are supported.",
60
+ 'Enter field names (one per line). Only text/string fields are supported.',
61
61
  },
62
62
  placeholder: {
63
- id: "composite-field.fields.placeholder",
64
- defaultMessage: "salutation\nfirstName\nlastName\nemail",
63
+ id: 'composite-field.fields.placeholder',
64
+ defaultMessage: 'salutation\nfirstName\nlastName\nemail',
65
65
  },
66
66
  },
67
67
  {
68
- name: "options.separator",
69
- type: "text",
68
+ name: 'options.separator',
69
+ type: 'text',
70
70
  intlLabel: {
71
- id: "composite-field.separator.label",
72
- defaultMessage: "Separator",
71
+ id: 'composite-field.separator.label',
72
+ defaultMessage: 'Separator',
73
73
  },
74
74
  description: {
75
- id: "composite-field.separator.description",
75
+ id: 'composite-field.separator.description',
76
76
  defaultMessage: 'Separator between fields (default: " - ")',
77
77
  },
78
78
  placeholder: {
79
- id: "composite-field.separator.placeholder",
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: "composite-field.advanced.title",
90
- defaultMessage: "Settings",
89
+ id: 'composite-field.advanced.title',
90
+ defaultMessage: 'Settings',
91
91
  },
92
92
  items: [
93
93
  {
94
- name: "options.editable",
95
- type: "checkbox",
94
+ name: 'options.autoGenerate',
95
+ type: 'checkbox',
96
96
  intlLabel: {
97
- id: "composite-field.editable.label",
98
- defaultMessage: "Editable",
97
+ id: 'composite-field.autoGenerate.label',
98
+ defaultMessage: 'Auto-generate',
99
99
  },
100
100
  description: {
101
- id: "composite-field.editable.description",
101
+ id: 'composite-field.autoGenerate.description',
102
102
  defaultMessage:
103
- "Allow manual editing of the composite field value",
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
  ],