11import { z } from 'zod' ;
2+ import { SystemIdentifierSchema } from '../shared/identifiers.zod' ;
23
34/**
45 * Field Type Enum
@@ -38,10 +39,25 @@ export type FieldType = z.infer<typeof FieldType>;
3839
3940/**
4041 * Select Option Schema
42+ *
43+ * Defines option values for select/picklist fields.
44+ *
45+ * **CRITICAL RULE**: The `value` field is a machine identifier that gets stored in the database.
46+ * It MUST be lowercase to avoid case-sensitivity issues in queries and comparisons.
47+ *
48+ * @example Good
49+ * { label: 'New', value: 'new' }
50+ * { label: 'In Progress', value: 'in_progress' }
51+ * { label: 'Closed Won', value: 'closed_won' }
52+ *
53+ * @example Bad (will be rejected)
54+ * { label: 'New', value: 'New' } // uppercase
55+ * { label: 'In Progress', value: 'In Progress' } // spaces and uppercase
56+ * { label: 'Closed Won', value: 'Closed_Won' } // mixed case
4157 */
4258export const SelectOptionSchema = z . object ( {
43- label : z . string ( ) . describe ( 'Display label' ) ,
44- value : z . string ( ) . describe ( 'Stored value' ) ,
59+ label : z . string ( ) . describe ( 'Display label (human-readable, any case allowed) ' ) ,
60+ value : SystemIdentifierSchema . describe ( 'Stored value (lowercase machine identifier) ' ) ,
4561 color : z . string ( ) . optional ( ) . describe ( 'Color code for badges/charts' ) ,
4662 default : z . boolean ( ) . optional ( ) . describe ( 'Is default option' ) ,
4763} ) ;
@@ -230,13 +246,28 @@ export const Field = {
230246 /**
231247 * Select field helper with backward-compatible API
232248 *
233- * @example Old API (array first)
249+ * Automatically converts option values to lowercase to enforce naming conventions.
250+ *
251+ * @example Old API (array first) - auto-converts to lowercase
234252 * Field.select(['High', 'Low'], { label: 'Priority' })
253+ * // Results in: [{ label: 'High', value: 'high' }, { label: 'Low', value: 'low' }]
235254 *
236- * @example New API (config object)
255+ * @example New API (config object) - enforces lowercase
237256 * Field.select({ options: [{label: 'High', value: 'high'}], label: 'Priority' })
257+ *
258+ * @example Multi-word values - converts to snake_case
259+ * Field.select(['In Progress', 'Closed Won'], { label: 'Status' })
260+ * // Results in: [{ label: 'In Progress', value: 'in_progress' }, { label: 'Closed Won', value: 'closed_won' }]
238261 */
239262 select : ( optionsOrConfig : SelectOption [ ] | string [ ] | FieldInput & { options : SelectOption [ ] | string [ ] } , config ?: FieldInput ) => {
263+ // Helper function to convert string to lowercase snake_case
264+ const toSnakeCase = ( str : string ) : string => {
265+ return str
266+ . toLowerCase ( )
267+ . replace ( / \s + / g, '_' ) // Replace spaces with underscores
268+ . replace ( / [ ^ a - z 0 - 9 _ . ] / g, '' ) ; // Remove invalid characters
269+ } ;
270+
240271 // Support both old and new signatures:
241272 // Old: Field.select(['a', 'b'], { label: 'X' })
242273 // New: Field.select({ options: [{label: 'A', value: 'a'}], label: 'X' })
@@ -245,11 +276,19 @@ export const Field = {
245276
246277 if ( Array . isArray ( optionsOrConfig ) ) {
247278 // Old signature: array as first param
248- options = optionsOrConfig . map ( o => typeof o === 'string' ? { label : o , value : o } : o ) ;
279+ options = optionsOrConfig . map ( o =>
280+ typeof o === 'string'
281+ ? { label : o , value : toSnakeCase ( o ) } // Auto-convert string to snake_case
282+ : { ...o , value : o . value . toLowerCase ( ) } // Ensure value is lowercase
283+ ) ;
249284 finalConfig = config || { } ;
250285 } else {
251286 // New signature: config object with options
252- options = ( optionsOrConfig . options || [ ] ) . map ( o => typeof o === 'string' ? { label : o , value : o } : o ) ;
287+ options = ( optionsOrConfig . options || [ ] ) . map ( o =>
288+ typeof o === 'string'
289+ ? { label : o , value : toSnakeCase ( o ) } // Auto-convert string to snake_case
290+ : { ...o , value : o . value . toLowerCase ( ) } // Ensure value is lowercase
291+ ) ;
253292 // Remove options from config to avoid confusion
254293 const { options : _ , ...restConfig } = optionsOrConfig ;
255294 finalConfig = restConfig ;
0 commit comments