@tunghtml/strapi-plugin-composite-field 1.2.8 → 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 +6 -0
- package/admin/src/components/CompositeInput.jsx +60 -75
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -176,6 +176,12 @@ Result: Panel Discussion - John Doe
|
|
|
176
176
|
|
|
177
177
|
## Changelog
|
|
178
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
|
+
|
|
179
185
|
### v1.2.8 (2026-10-19)
|
|
180
186
|
|
|
181
187
|
**🐛 Bug Fixes:**
|
|
@@ -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,
|
|
@@ -69,43 +120,6 @@ const CompositeInput = (props) => {
|
|
|
69
120
|
}
|
|
70
121
|
};
|
|
71
122
|
|
|
72
|
-
// Helper function to format values (especially time/date)
|
|
73
|
-
const formatFieldValue = React.useCallback((val) => {
|
|
74
|
-
if (val === null || val === undefined) return '';
|
|
75
|
-
|
|
76
|
-
// If it's a Date object
|
|
77
|
-
if (val instanceof Date) {
|
|
78
|
-
return val.toLocaleTimeString([], {
|
|
79
|
-
hour: '2-digit',
|
|
80
|
-
minute: '2-digit',
|
|
81
|
-
hour12: false,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const stringVal = String(val);
|
|
86
|
-
|
|
87
|
-
// Check if value is ISO date format (YYYY-MM-DDTHH:MM:SS.sssZ or YYYY-MM-DD)
|
|
88
|
-
const isoDateRegex = /^(\d{4})-(\d{2})-(\d{2})(T[\d:.]+Z?)?$/;
|
|
89
|
-
const dateMatch = stringVal.match(isoDateRegex);
|
|
90
|
-
|
|
91
|
-
if (dateMatch) {
|
|
92
|
-
// Return in MM/DD/YYYY format
|
|
93
|
-
const [, year, month, day] = dateMatch;
|
|
94
|
-
return `${month}/${day}/${year}`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Check if value is time format (HH:MM:SS.mmm or HH:MM:SS)
|
|
98
|
-
const timeRegex = /^(\d{2}):(\d{2}):(\d{2})(\.\d{3})?$/;
|
|
99
|
-
const timeMatch = stringVal.match(timeRegex);
|
|
100
|
-
|
|
101
|
-
if (timeMatch) {
|
|
102
|
-
// Return only HH:MM
|
|
103
|
-
return `${timeMatch[1]}:${timeMatch[2]}`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return stringVal;
|
|
107
|
-
}, []);
|
|
108
|
-
|
|
109
123
|
const handleGenerate = React.useCallback(() => {
|
|
110
124
|
// Calculate parent path to resolve relative field names
|
|
111
125
|
const parts = name.split('.');
|
|
@@ -113,44 +127,15 @@ const CompositeInput = (props) => {
|
|
|
113
127
|
parts.pop(); // Remove current field name
|
|
114
128
|
const parentPath = parts.join('.');
|
|
115
129
|
|
|
116
|
-
const generatedParts =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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);
|
|
121
137
|
|
|
122
|
-
|
|
123
|
-
let rawValue = get(values, fullPath);
|
|
124
|
-
|
|
125
|
-
// Handle different value types
|
|
126
|
-
if (rawValue !== null && rawValue !== undefined) {
|
|
127
|
-
let stringValue = '';
|
|
128
|
-
|
|
129
|
-
// Handle relations/objects
|
|
130
|
-
if (typeof rawValue === 'object' && !(rawValue instanceof Date)) {
|
|
131
|
-
if (rawValue.name) stringValue = String(rawValue.name);
|
|
132
|
-
else if (rawValue.title) stringValue = String(rawValue.title);
|
|
133
|
-
else if (rawValue.label) stringValue = String(rawValue.label);
|
|
134
|
-
} else {
|
|
135
|
-
stringValue = formatFieldValue(rawValue);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (stringValue) {
|
|
139
|
-
generatedParts.push(stringValue);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Handle separator logic - if separator is just a space, don't add extra spaces
|
|
145
|
-
const cleanSeparator = separator.trim();
|
|
146
|
-
let result;
|
|
147
|
-
if (cleanSeparator === '') {
|
|
148
|
-
// If separator is just spaces, use single space
|
|
149
|
-
result = generatedParts.join(' ').trim();
|
|
150
|
-
} else {
|
|
151
|
-
// For other separators, add spaces around them
|
|
152
|
-
result = generatedParts.join(` ${cleanSeparator} `).trim();
|
|
153
|
-
}
|
|
138
|
+
const result = joinParts(generatedParts, separator);
|
|
154
139
|
|
|
155
140
|
if (result !== lastGeneratedVal.current) {
|
|
156
141
|
setLocalValue(result);
|
|
@@ -162,7 +147,7 @@ const CompositeInput = (props) => {
|
|
|
162
147
|
onChange({ target: { name, value: result, type: 'text' } });
|
|
163
148
|
}
|
|
164
149
|
}
|
|
165
|
-
}, [fields, separator, onChange, name, values
|
|
150
|
+
}, [fields, separator, onChange, name, values]);
|
|
166
151
|
|
|
167
152
|
// Watch only the relevant field values to trigger auto-generation
|
|
168
153
|
const watchedValues = React.useMemo(() => {
|
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.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",
|