@byline/admin 3.20.4 → 3.21.0

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.
@@ -110,6 +110,8 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
110
110
  id: htmlId
111
111
  });
112
112
  case 'integer':
113
+ case 'float':
114
+ case 'decimal':
113
115
  return /*#__PURE__*/ jsx(NumericalField, {
114
116
  field: hideLabel ? {
115
117
  ...field,
@@ -6,13 +6,15 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import type { CounterField, DecimalField, FieldComponentSlots, FloatField, IntegerField } from '@byline/core';
9
+ type NumericalValue = string | number | null;
9
10
  export declare const NumericalField: ({ field, value, defaultValue, onChange, id, path, components, }: {
10
11
  field: IntegerField | FloatField | DecimalField | CounterField;
11
12
  value?: string | number | null;
12
13
  defaultValue?: string | number | null;
13
- onChange?: (value: string) => void;
14
+ onChange?: (value: NumericalValue) => void;
14
15
  id?: string;
15
16
  path?: string;
16
17
  /** Optional UI component slot overrides from the admin config. */
17
18
  components?: FieldComponentSlots;
18
19
  }) => import("react").JSX.Element;
20
+ export {};
@@ -1,13 +1,39 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { normalizeNumericValue } from "@byline/core";
2
4
  import { Input } from "@byline/ui/react";
3
5
  import { useFieldError, useFieldValue } from "../../forms/form-context.js";
6
+ const COMPLETE_NUMERIC_RE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
4
7
  const NumericalField = ({ field, value, defaultValue, onChange, id, path, components })=>{
5
8
  const fieldPath = path ?? field.name;
6
9
  const fieldError = useFieldError(fieldPath);
7
10
  const fieldValue = useFieldValue(fieldPath);
8
- const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
11
+ const incomingValue = void 0 !== value ? value : void 0 !== fieldValue ? fieldValue : defaultValue ?? null;
9
12
  const htmlId = id ?? fieldPath;
10
- const displayValue = null == incomingValue ? '' : String(incomingValue);
13
+ const canonicalDisplay = null == incomingValue ? '' : String(incomingValue);
14
+ const [displayValue, setDisplayValue] = useState(canonicalDisplay);
15
+ const [isEditing, setIsEditing] = useState(false);
16
+ useEffect(()=>{
17
+ if (!isEditing) setDisplayValue(canonicalDisplay);
18
+ }, [
19
+ canonicalDisplay,
20
+ isEditing
21
+ ]);
22
+ const canonicalize = (nextValue)=>{
23
+ if (null == nextValue || 'string' == typeof nextValue && '' === nextValue.trim()) return null;
24
+ if ('counter' === field.type) return;
25
+ if ('string' == typeof nextValue && !COMPLETE_NUMERIC_RE.test(nextValue.trim())) return;
26
+ try {
27
+ return normalizeNumericValue(field.type, nextValue, fieldPath);
28
+ } catch {
29
+ return;
30
+ }
31
+ };
32
+ const commit = (nextValue)=>{
33
+ const canonical = canonicalize(nextValue);
34
+ if (void 0 !== canonical) onChange?.(canonical);
35
+ return canonical;
36
+ };
11
37
  const slots = components;
12
38
  const CustomLabel = slots?.Label;
13
39
  const CustomHelpText = slots?.HelpText;
@@ -35,12 +61,14 @@ const NumericalField = ({ field, value, defaultValue, onChange, id, path, compon
35
61
  const renderInput = ()=>{
36
62
  if (CustomField) return /*#__PURE__*/ jsx(CustomField, {
37
63
  ...slotBaseProps,
38
- onChange: (v)=>onChange?.(v),
64
+ onChange: (nextValue)=>commit(nextValue),
39
65
  defaultValue: defaultValue,
40
66
  placeholder: field.placeholder
41
67
  });
42
68
  return /*#__PURE__*/ jsx(Input, {
43
- type: "number",
69
+ type: "text",
70
+ inputMode: 'integer' === field.type || 'counter' === field.type ? 'numeric' : 'decimal',
71
+ step: 'integer' === field.type || 'counter' === field.type ? 1 : 'any',
44
72
  id: htmlId,
45
73
  name: field.name,
46
74
  label: suppressInputLabel ? void 0 : field.label,
@@ -48,7 +76,16 @@ const NumericalField = ({ field, value, defaultValue, onChange, id, path, compon
48
76
  readOnly: field.readOnly,
49
77
  helpText: suppressInputHelpText ? void 0 : field.helpText,
50
78
  value: displayValue,
51
- onChange: (e)=>onChange?.(e.target.value),
79
+ onFocus: ()=>setIsEditing(true),
80
+ onChange: (e)=>{
81
+ setDisplayValue(e.target.value);
82
+ commit(e.target.value);
83
+ },
84
+ onBlur: ()=>{
85
+ setIsEditing(false);
86
+ const canonical = commit(displayValue);
87
+ setDisplayValue(void 0 === canonical ? canonicalDisplay : canonical?.toString() ?? '');
88
+ },
52
89
  error: null != fieldError,
53
90
  errorText: fieldError
54
91
  });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/admin",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "3.20.4",
5
+ "version": "3.21.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -155,10 +155,10 @@
155
155
  "react-diff-viewer-continued": "^4.2.2",
156
156
  "uuid": "^14.0.1",
157
157
  "zod": "^4.4.3",
158
- "@byline/auth": "3.20.4",
159
- "@byline/core": "3.20.4",
160
- "@byline/ui": "3.20.4",
161
- "@byline/i18n": "3.20.4"
158
+ "@byline/auth": "3.21.0",
159
+ "@byline/ui": "3.21.0",
160
+ "@byline/i18n": "3.21.0",
161
+ "@byline/core": "3.21.0"
162
162
  },
163
163
  "peerDependencies": {
164
164
  "react": "^19.0.0",
@@ -195,6 +195,8 @@ export const FieldRenderer = ({
195
195
  />
196
196
  )
197
197
  case 'integer':
198
+ case 'float':
199
+ case 'decimal':
198
200
  return (
199
201
  <NumericalField
200
202
  field={hideLabel ? { ...field, label: undefined } : field}
@@ -6,6 +6,8 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
 
9
+ import { useEffect, useState } from 'react'
10
+
9
11
  import type {
10
12
  CounterField,
11
13
  DecimalField,
@@ -14,10 +16,15 @@ import type {
14
16
  FloatField,
15
17
  IntegerField,
16
18
  } from '@byline/core'
19
+ import { normalizeNumericValue } from '@byline/core'
17
20
  import { Input } from '@byline/ui/react'
18
21
 
19
22
  import { useFieldError, useFieldValue } from '../../forms/form-context'
20
23
 
24
+ type NumericalValue = string | number | null
25
+
26
+ const COMPLETE_NUMERIC_RE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/
27
+
21
28
  export const NumericalField = ({
22
29
  field,
23
30
  value,
@@ -30,7 +37,7 @@ export const NumericalField = ({
30
37
  field: IntegerField | FloatField | DecimalField | CounterField
31
38
  value?: string | number | null
32
39
  defaultValue?: string | number | null
33
- onChange?: (value: string) => void
40
+ onChange?: (value: NumericalValue) => void
34
41
  id?: string
35
42
  path?: string
36
43
  /** Optional UI component slot overrides from the admin config. */
@@ -38,11 +45,36 @@ export const NumericalField = ({
38
45
  }) => {
39
46
  const fieldPath = path ?? field.name
40
47
  const fieldError = useFieldError(fieldPath)
41
- const fieldValue = useFieldValue<string | number | undefined>(fieldPath)
42
- const incomingValue = value ?? fieldValue ?? defaultValue ?? ''
48
+ const fieldValue = useFieldValue<NumericalValue | undefined>(fieldPath)
49
+ const incomingValue =
50
+ value !== undefined ? value : fieldValue !== undefined ? fieldValue : (defaultValue ?? null)
43
51
  const htmlId = id ?? fieldPath
44
- const displayValue =
45
- incomingValue === undefined || incomingValue === null ? '' : String(incomingValue)
52
+ const canonicalDisplay = incomingValue == null ? '' : String(incomingValue)
53
+ const [displayValue, setDisplayValue] = useState(canonicalDisplay)
54
+ const [isEditing, setIsEditing] = useState(false)
55
+
56
+ useEffect(() => {
57
+ if (!isEditing) setDisplayValue(canonicalDisplay)
58
+ }, [canonicalDisplay, isEditing])
59
+
60
+ const canonicalize = (nextValue: NumericalValue): NumericalValue | undefined => {
61
+ if (nextValue == null || (typeof nextValue === 'string' && nextValue.trim() === '')) return null
62
+ if (field.type === 'counter') return undefined
63
+ if (typeof nextValue === 'string' && !COMPLETE_NUMERIC_RE.test(nextValue.trim())) {
64
+ return undefined
65
+ }
66
+ try {
67
+ return normalizeNumericValue(field.type, nextValue, fieldPath)
68
+ } catch {
69
+ return undefined
70
+ }
71
+ }
72
+
73
+ const commit = (nextValue: NumericalValue) => {
74
+ const canonical = canonicalize(nextValue)
75
+ if (canonical !== undefined) onChange?.(canonical)
76
+ return canonical
77
+ }
46
78
 
47
79
  // Custom component slots (from admin config)
48
80
  const slots = components
@@ -79,7 +111,7 @@ export const NumericalField = ({
79
111
  return (
80
112
  <CustomField
81
113
  {...slotBaseProps}
82
- onChange={(v: any) => onChange?.(v)}
114
+ onChange={(nextValue: NumericalValue) => commit(nextValue)}
83
115
  defaultValue={defaultValue}
84
116
  placeholder={field.placeholder}
85
117
  />
@@ -87,7 +119,9 @@ export const NumericalField = ({
87
119
  }
88
120
  return (
89
121
  <Input
90
- type="number"
122
+ type="text"
123
+ inputMode={field.type === 'integer' || field.type === 'counter' ? 'numeric' : 'decimal'}
124
+ step={field.type === 'integer' || field.type === 'counter' ? 1 : 'any'}
91
125
  id={htmlId}
92
126
  name={field.name}
93
127
  label={suppressInputLabel ? undefined : field.label}
@@ -95,7 +129,18 @@ export const NumericalField = ({
95
129
  readOnly={field.readOnly}
96
130
  helpText={suppressInputHelpText ? undefined : field.helpText}
97
131
  value={displayValue}
98
- onChange={(e) => onChange?.(e.target.value)}
132
+ onFocus={() => setIsEditing(true)}
133
+ onChange={(e) => {
134
+ setDisplayValue(e.target.value)
135
+ commit(e.target.value)
136
+ }}
137
+ onBlur={() => {
138
+ setIsEditing(false)
139
+ const canonical = commit(displayValue)
140
+ setDisplayValue(
141
+ canonical === undefined ? canonicalDisplay : (canonical?.toString() ?? '')
142
+ )
143
+ }}
99
144
  error={fieldError != null}
100
145
  errorText={fieldError}
101
146
  />