@startsimpli/ui 0.4.34 → 0.4.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/ui",
3
- "version": "0.4.34",
3
+ "version": "0.4.35",
4
4
  "description": "Shared UI components package for StartSimpli applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -33,8 +33,8 @@
33
33
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
34
34
  "react": "^18.0.0 || ^19.0.0",
35
35
  "react-dom": "^18.0.0 || ^19.0.0",
36
- "@startsimpli/auth": "0.4.28",
37
- "@startsimpli/api": "0.5.35"
36
+ "@startsimpli/api": "0.5.35",
37
+ "@startsimpli/auth": "0.4.28"
38
38
  },
39
39
  "peerDependenciesMeta": {
40
40
  "@startsimpli/api": {
@@ -225,6 +225,46 @@ export function SchemaTypeBuilder({
225
225
  />
226
226
  </div>
227
227
  )}
228
+
229
+ {(attr.dataType === 'number' || attr.dataType === 'integer') && (
230
+ <div className="grid gap-3 sm:col-span-4 sm:grid-cols-2">
231
+ <div className="space-y-1.5">
232
+ <Label htmlFor={`attr-min-${i}`}>Minimum (optional)</Label>
233
+ <Input
234
+ id={`attr-min-${i}`}
235
+ type="number"
236
+ value={attr.min}
237
+ onChange={(e) => updateAttr(i, { min: e.target.value })}
238
+ placeholder="e.g. 0"
239
+ />
240
+ </div>
241
+ <div className="space-y-1.5">
242
+ <Label htmlFor={`attr-max-${i}`}>Maximum (optional)</Label>
243
+ <Input
244
+ id={`attr-max-${i}`}
245
+ type="number"
246
+ value={attr.max}
247
+ onChange={(e) => updateAttr(i, { max: e.target.value })}
248
+ placeholder="e.g. 100"
249
+ />
250
+ </div>
251
+ </div>
252
+ )}
253
+
254
+ {(attr.dataType === 'text' || attr.dataType === 'longtext') && (
255
+ <div className="space-y-1.5 sm:col-span-4">
256
+ <Label htmlFor={`attr-regex-${i}`}>Pattern (regex, optional)</Label>
257
+ <Input
258
+ id={`attr-regex-${i}`}
259
+ value={attr.regex}
260
+ onChange={(e) => updateAttr(i, { regex: e.target.value })}
261
+ placeholder="e.g. [^@]+@[^@]+\.[^@]+"
262
+ />
263
+ <p className="text-xs text-gray-500">
264
+ Values must fully match this pattern.
265
+ </p>
266
+ </div>
267
+ )}
228
268
  </div>
229
269
  ))}
230
270
 
@@ -21,22 +21,23 @@ describe('toKey', () => {
21
21
  })
22
22
 
23
23
  describe('blankAttribute', () => {
24
- it('defaults to a non-required text field with no choices', () => {
24
+ it('defaults to a non-required text field with no choices/bounds/regex', () => {
25
25
  expect(blankAttribute()).toEqual({
26
26
  name: '',
27
27
  dataType: 'text',
28
28
  required: false,
29
29
  choices: '',
30
+ min: '',
31
+ max: '',
32
+ regex: '',
30
33
  })
31
34
  })
32
35
  })
33
36
 
34
37
  describe('draftToAttributePayloads', () => {
35
38
  const text = (over: Partial<DraftAttribute> = {}): DraftAttribute => ({
39
+ ...blankAttribute(),
36
40
  name: 'email',
37
- dataType: 'text',
38
- required: false,
39
- choices: '',
40
41
  ...over,
41
42
  })
42
43
 
@@ -74,6 +75,39 @@ describe('draftToAttributePayloads', () => {
74
75
  config: {},
75
76
  })
76
77
  })
78
+
79
+ it('builds numeric config.min/max from the min/max inputs (number/integer)', () => {
80
+ const [payload] = draftToAttributePayloads(5, [
81
+ text({ name: 'qty', dataType: 'integer', min: '1', max: '10' }),
82
+ ])
83
+ expect(payload).toMatchObject({
84
+ name: 'qty',
85
+ dataType: 'integer',
86
+ config: { min: 1, max: 10 },
87
+ })
88
+ })
89
+
90
+ it('sets only the numeric bounds that are provided (blank min/max omitted)', () => {
91
+ const [payload] = draftToAttributePayloads(5, [
92
+ text({ name: 'score', dataType: 'number', min: '0', max: '' }),
93
+ ])
94
+ expect(payload.config).toEqual({ min: 0 })
95
+ })
96
+
97
+ it('builds text config.regex from the regex input (text/longtext)', () => {
98
+ const [payload] = draftToAttributePayloads(5, [
99
+ text({ name: 'email', dataType: 'text', regex: '[^@]+@[^@]+' }),
100
+ ])
101
+ expect(payload.config).toEqual({ regex: '[^@]+@[^@]+' })
102
+ })
103
+
104
+ it('keeps config keys scoped to the dataType (no cross-contamination)', () => {
105
+ // text reads only regex; a stray min/max/choices must not leak into config.
106
+ const [payload] = draftToAttributePayloads(5, [
107
+ text({ name: 'note', dataType: 'text', min: '1', max: '9', choices: 'a,b' }),
108
+ ])
109
+ expect(payload.config).toEqual({})
110
+ })
77
111
  })
78
112
 
79
113
  describe('DATA_TYPES', () => {
@@ -37,10 +37,16 @@ export interface DraftAttribute {
37
37
  required: boolean
38
38
  /** Comma-separated choices, only used when dataType === 'enum'. */
39
39
  choices: string
40
+ /** Lower bound, only used when dataType is number/integer (blank = none). */
41
+ min: string
42
+ /** Upper bound, only used when dataType is number/integer (blank = none). */
43
+ max: string
44
+ /** Validation pattern, only used when dataType is text/longtext (blank = none). */
45
+ regex: string
40
46
  }
41
47
 
42
48
  export function blankAttribute(): DraftAttribute {
43
- return { name: '', dataType: 'text', required: false, choices: '' }
49
+ return { name: '', dataType: 'text', required: false, choices: '', min: '', max: '', regex: '' }
44
50
  }
45
51
 
46
52
  /** Slugify a label into a stable, lowercase key the backend stores. */
@@ -73,12 +79,22 @@ export function draftToAttributePayloads(
73
79
  for (const attr of drafts) {
74
80
  const name = attr.name.trim()
75
81
  if (!name) continue
82
+ // Config keys are scoped to the dataType + match what the backend enforces
83
+ // (enum: choices, number/integer: min/max, text/longtext: regex). Numeric
84
+ // bounds + regex are omitted when blank; an enum always carries its choices.
76
85
  const config: Record<string, unknown> = {}
77
86
  if (attr.dataType === 'enum') {
78
87
  config.choices = attr.choices
79
88
  .split(',')
80
89
  .map((c) => c.trim())
81
90
  .filter(Boolean)
91
+ } else if (attr.dataType === 'number' || attr.dataType === 'integer') {
92
+ const min = Number(attr.min)
93
+ const max = Number(attr.max)
94
+ if (attr.min.trim() !== '' && !Number.isNaN(min)) config.min = min
95
+ if (attr.max.trim() !== '' && !Number.isNaN(max)) config.max = max
96
+ } else if (attr.dataType === 'text' || attr.dataType === 'longtext') {
97
+ if (attr.regex.trim() !== '') config.regex = attr.regex.trim()
82
98
  }
83
99
  out.push({
84
100
  entityType: entityTypeId,