@tunghtml/strapi-plugin-composite-field 1.2.7 → 1.2.9

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
@@ -141,6 +141,7 @@ Enable **Auto-generate** in Advanced settings to automatically update the compos
141
141
  - If you manually edit the composite field, it will preserve your changes
142
142
  - When any source field changes, auto-generation resumes with the new values
143
143
  - This prevents accidental overwrites while maintaining automatic updates
144
+ - Opening an existing entry keeps its saved value: the field is only regenerated after a source field changes, so the entry is not marked as modified on load
144
145
 
145
146
  ### Space-Only Separator
146
147
 
@@ -175,6 +176,19 @@ Result: Panel Discussion - John Doe
175
176
 
176
177
  ## Changelog
177
178
 
179
+ ### v1.2.9 (2026-10-19)
180
+
181
+ **🔧 Technical:**
182
+
183
+ - Value formatting and joining moved out of the component, regexes use `RegExp.exec`
184
+
185
+ ### v1.2.8 (2026-10-19)
186
+
187
+ **🐛 Bug Fixes:**
188
+
189
+ - Opening an existing entry no longer regenerates the composite value, which marked the form as modified and rewrote the stored value on the next save
190
+ - Loading another entry or resetting the form takes the loaded value as the new starting point
191
+
178
192
  ### v1.2.4 (2026-02-05)
179
193
 
180
194
  **🎯 Improvements:**
@@ -5,6 +5,57 @@ import get from 'lodash/get';
5
5
  import React from 'react';
6
6
  import { useIntl } from 'react-intl';
7
7
 
8
+ const ISO_DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})(T[\d:.]+Z?)?$/;
9
+ const TIME_REGEX = /^(\d{2}):(\d{2}):(\d{2})(\.\d{3})?$/;
10
+
11
+ // Format a source value for display (especially time/date)
12
+ const formatFieldValue = (val) => {
13
+ if (val === null || val === undefined) return '';
14
+
15
+ // If it's a Date object
16
+ if (val instanceof Date) {
17
+ return val.toLocaleTimeString([], {
18
+ hour: '2-digit',
19
+ minute: '2-digit',
20
+ hour12: false,
21
+ });
22
+ }
23
+
24
+ const stringVal = String(val);
25
+
26
+ // ISO date (YYYY-MM-DDTHH:MM:SS.sssZ or YYYY-MM-DD) => MM/DD/YYYY
27
+ const dateMatch = ISO_DATE_REGEX.exec(stringVal);
28
+ if (dateMatch) {
29
+ const [, year, month, day] = dateMatch;
30
+ return `${month}/${day}/${year}`;
31
+ }
32
+
33
+ // Time (HH:MM:SS.mmm or HH:MM:SS) => HH:MM
34
+ const timeMatch = TIME_REGEX.exec(stringVal);
35
+ if (timeMatch) {
36
+ return `${timeMatch[1]}:${timeMatch[2]}`;
37
+ }
38
+
39
+ return stringVal;
40
+ };
41
+
42
+ // Text of one source value: relations/objects use their name, title or label
43
+ const toDisplayText = (rawValue) => {
44
+ if (rawValue === null || rawValue === undefined) return '';
45
+ if (typeof rawValue === 'object' && !(rawValue instanceof Date)) {
46
+ return String(rawValue.name || rawValue.title || rawValue.label || '');
47
+ }
48
+ return formatFieldValue(rawValue);
49
+ };
50
+
51
+ // A space-only separator joins with single spaces, other separators get spaces around them
52
+ const joinParts = (parts, separator) => {
53
+ const cleanSeparator = separator.trim();
54
+ return cleanSeparator === ''
55
+ ? parts.join(' ').trim()
56
+ : parts.join(` ${cleanSeparator} `).trim();
57
+ };
58
+
8
59
  const CompositeInput = (props) => {
9
60
  const {
10
61
  attribute,
@@ -28,10 +79,15 @@ const CompositeInput = (props) => {
28
79
  const [isManuallyEdited, setIsManuallyEdited] = React.useState(false);
29
80
 
30
81
  const lastGeneratedVal = React.useRef(value || '');
31
- const lastWatchedValues = React.useRef([]);
82
+ // Source values the field was last generated from; null until the form has loaded
83
+ const lastWatchedValues = React.useRef(null);
32
84
 
33
85
  React.useEffect(() => {
34
86
  const validValue = value || '';
87
+ // A value this field did not produce (another entry loaded, form reset) is a new baseline
88
+ if (validValue !== lastGeneratedVal.current) {
89
+ lastWatchedValues.current = null;
90
+ }
35
91
  setLocalValue(validValue);
36
92
  lastGeneratedVal.current = validValue;
37
93
  // Reset manual edit flag when value changes from outside (e.g., form reset)
@@ -64,43 +120,6 @@ const CompositeInput = (props) => {
64
120
  }
65
121
  };
66
122
 
67
- // Helper function to format values (especially time/date)
68
- const formatFieldValue = React.useCallback((val) => {
69
- if (val === null || val === undefined) return '';
70
-
71
- // If it's a Date object
72
- if (val instanceof Date) {
73
- return val.toLocaleTimeString([], {
74
- hour: '2-digit',
75
- minute: '2-digit',
76
- hour12: false,
77
- });
78
- }
79
-
80
- const stringVal = String(val);
81
-
82
- // Check if value is ISO date format (YYYY-MM-DDTHH:MM:SS.sssZ or YYYY-MM-DD)
83
- const isoDateRegex = /^(\d{4})-(\d{2})-(\d{2})(T[\d:.]+Z?)?$/;
84
- const dateMatch = stringVal.match(isoDateRegex);
85
-
86
- if (dateMatch) {
87
- // Return in MM/DD/YYYY format
88
- const [, year, month, day] = dateMatch;
89
- return `${month}/${day}/${year}`;
90
- }
91
-
92
- // Check if value is time format (HH:MM:SS.mmm or HH:MM:SS)
93
- const timeRegex = /^(\d{2}):(\d{2}):(\d{2})(\.\d{3})?$/;
94
- const timeMatch = stringVal.match(timeRegex);
95
-
96
- if (timeMatch) {
97
- // Return only HH:MM
98
- return `${timeMatch[1]}:${timeMatch[2]}`;
99
- }
100
-
101
- return stringVal;
102
- }, []);
103
-
104
123
  const handleGenerate = React.useCallback(() => {
105
124
  // Calculate parent path to resolve relative field names
106
125
  const parts = name.split('.');
@@ -108,44 +127,15 @@ const CompositeInput = (props) => {
108
127
  parts.pop(); // Remove current field name
109
128
  const parentPath = parts.join('.');
110
129
 
111
- const generatedParts = [];
130
+ const generatedParts = fields
131
+ .map((fieldName) => {
132
+ // Resolve full path for the target field
133
+ const fullPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
134
+ return toDisplayText(get(values, fullPath));
135
+ })
136
+ .filter(Boolean);
112
137
 
113
- fields.forEach((fieldName) => {
114
- // Resolve full path for the target field
115
- const fullPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
116
-
117
- // Get raw value from form values
118
- let rawValue = get(values, fullPath);
119
-
120
- // Handle different value types
121
- if (rawValue !== null && rawValue !== undefined) {
122
- let stringValue = '';
123
-
124
- // Handle relations/objects
125
- if (typeof rawValue === 'object' && !(rawValue instanceof Date)) {
126
- if (rawValue.name) stringValue = String(rawValue.name);
127
- else if (rawValue.title) stringValue = String(rawValue.title);
128
- else if (rawValue.label) stringValue = String(rawValue.label);
129
- } else {
130
- stringValue = formatFieldValue(rawValue);
131
- }
132
-
133
- if (stringValue) {
134
- generatedParts.push(stringValue);
135
- }
136
- }
137
- });
138
-
139
- // Handle separator logic - if separator is just a space, don't add extra spaces
140
- const cleanSeparator = separator.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
- }
138
+ const result = joinParts(generatedParts, separator);
149
139
 
150
140
  if (result !== lastGeneratedVal.current) {
151
141
  setLocalValue(result);
@@ -157,7 +147,7 @@ const CompositeInput = (props) => {
157
147
  onChange({ target: { name, value: result, type: 'text' } });
158
148
  }
159
149
  }
160
- }, [fields, separator, onChange, name, values, formatFieldValue]);
150
+ }, [fields, separator, onChange, name, values]);
161
151
 
162
152
  // Watch only the relevant field values to trigger auto-generation
163
153
  const watchedValues = React.useMemo(() => {
@@ -177,13 +167,19 @@ const CompositeInput = (props) => {
177
167
 
178
168
  // Check if watched values actually changed
179
169
  const currentWatchedValues = JSON.stringify(watchedValues);
180
- const lastWatchedValuesStr = JSON.stringify(lastWatchedValues.current);
170
+
171
+ // Keep the saved value when an entry opens: regenerating it there marks the form as
172
+ // modified and overwrites values written by the server on the next save
173
+ if (lastWatchedValues.current === null) {
174
+ lastWatchedValues.current = currentWatchedValues;
175
+ return;
176
+ }
181
177
 
182
178
  // Only auto-generate if:
183
179
  // 1. Values actually changed
184
180
  // 2. User hasn't manually edited the field
185
- if (currentWatchedValues !== lastWatchedValuesStr) {
186
- lastWatchedValues.current = watchedValues;
181
+ if (currentWatchedValues !== lastWatchedValues.current) {
182
+ lastWatchedValues.current = currentWatchedValues;
187
183
 
188
184
  if (!isManuallyEdited) {
189
185
  const timeoutId = setTimeout(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tunghtml/strapi-plugin-composite-field",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
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",