Browse Source

fix(dashboard): Fix UX of number inputs

Do not switch to `0` when deleting a number value
Michael Bromley 3 months ago
parent
commit
6645c66376

+ 2 - 8
packages/dashboard/src/app/routes/_authenticated/_global-settings/global-settings.tsx

@@ -1,9 +1,9 @@
+import { NumberInput } from '@/vdb/components/data-input/number-input.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
 import { LanguageSelector } from '@/vdb/components/shared/language-selector.js';
 import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { Input } from '@/vdb/components/ui/input.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import { extendDetailFormQuery } from '@/vdb/framework/document-extension/extend-detail-form-query.js';
@@ -136,13 +136,7 @@ function GlobalSettingsPage() {
                                     by product variants.
                                 </Trans>
                             }
-                            render={({ field }) => (
-                                <Input
-                                    value={field.value ?? []}
-                                    onChange={e => field.onChange(Number(e.target.valueAsNumber))}
-                                    type="number"
-                                />
-                            )}
+                            render={({ field }) => <NumberInput {...field} />}
                         />
                         <FormFieldWrapper
                             control={form.control}

+ 2 - 9
packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx

@@ -1,4 +1,5 @@
 import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
+import { NumberInput } from '@/vdb/components/data-input/number-input.js';
 import { AssignedFacetValues } from '@/vdb/components/shared/assigned-facet-values.js';
 import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
@@ -401,15 +402,7 @@ function ProductVariantDetailPage() {
                                     control={form.control}
                                     name={`stockLevels.${index}.stockOnHand`}
                                     label={stockLabel}
-                                    render={({ field }) => (
-                                        <Input
-                                            type="number"
-                                            value={field.value}
-                                            onChange={e => {
-                                                field.onChange(e.target.valueAsNumber);
-                                            }}
-                                        />
-                                    )}
+                                    render={({ field }) => <NumberInput {...field} value={field.value} />}
                                 />
                                 <div>
                                     <FormItem>

+ 3 - 14
packages/dashboard/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx

@@ -1,4 +1,5 @@
 import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
+import { NumberInput } from '@/vdb/components/data-input/number-input.js';
 import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
@@ -202,25 +203,13 @@ function PromotionDetailPage() {
                             control={form.control}
                             name="perCustomerUsageLimit"
                             label={<Trans>Per customer usage limit</Trans>}
-                            render={({ field }) => (
-                                <Input
-                                    type="number"
-                                    value={field.value ?? ''}
-                                    onChange={e => field.onChange(e.target.valueAsNumber)}
-                                />
-                            )}
+                            render={({ field }) => <NumberInput {...field} value={field.value ?? ''} />}
                         />
                         <FormFieldWrapper
                             control={form.control}
                             name="usageLimit"
                             label={<Trans>Usage limit</Trans>}
-                            render={({ field }) => (
-                                <Input
-                                    type="number"
-                                    value={field.value ?? ''}
-                                    onChange={e => field.onChange(e.target.valueAsNumber)}
-                                />
-                            )}
+                            render={({ field }) => <NumberInput {...field} value={field.value ?? ''} />}
                         />
                     </DetailFormGrid>
                 </PageBlock>

+ 1 - 1
packages/dashboard/src/lib/components/data-input/number-input.tsx

@@ -24,7 +24,7 @@ export function NumberInput({ fieldDef, onChange, ...fieldProps }: Readonly<Dash
         if (readOnly) return;
         const numValue = e.target.valueAsNumber;
         if (Number.isNaN(numValue)) {
-            onChange(e.target.value);
+            onChange(null);
         } else {
             onChange(e.target.valueAsNumber);
         }

+ 1 - 1
packages/dashboard/src/lib/components/shared/configurable-operation-input.tsx

@@ -144,7 +144,7 @@ export function interpolateDescription(
         (substring: string, argName: string) => {
             const normalizedArgName = argName.toLowerCase();
             const value = values.find(v => v.name === normalizedArgName)?.value;
-            if (value == null) {
+            if (value == null || value === '') {
                 return '_';
             }
             let formatted = value;

+ 8 - 1
packages/dashboard/src/lib/framework/form-engine/value-transformers.ts

@@ -29,9 +29,16 @@ export const nativeValueTransformer: ValueTransformer = {
  */
 export const jsonStringValueTransformer: ValueTransformer = {
     parse: (value: string, fieldDef: ConfigurableFieldDef) => {
-        if (!value) {
+        if (value === undefined) {
             return getDefaultValue(fieldDef);
         }
+        // This case arises often when the administrator is actively editing
+        // values and clears out the input. At that point, we don't want to suddenly
+        // switch to the default value otherwise it results in poor UX, e.g. pressing
+        // backspace to delete a number would result in `0` suddenly appearing as the value.
+        if (value === '') {
+            return value;
+        }
 
         try {
             // For JSON string mode, parse the string to get the native value

+ 12 - 15
packages/dashboard/src/lib/framework/page/detail-page.tsx

@@ -5,8 +5,8 @@ import { Checkbox } from '@/vdb/components/ui/checkbox.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
-import { Trans } from '@lingui/react/macro';
 import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import { Trans } from '@lingui/react/macro';
 import { AnyRoute, useNavigate } from '@tanstack/react-router';
 import { ResultOf, VariablesOf } from 'gql.tada';
 import { toast } from 'sonner';
@@ -16,6 +16,7 @@ import {
     getOperationVariablesFields,
 } from '../document-introspection/get-document-structure.js';
 
+import { NumberInput } from '@/vdb/components/data-input/number-input.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { FormControl } from '@/vdb/components/ui/form.js';
 import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
@@ -108,11 +109,7 @@ function FieldInputRenderer<
         case 'Float':
             return (
                 <FormControl>
-                    <Input
-                        type="number"
-                        value={field.value}
-                        onChange={e => field.onChange(e.target.valueAsNumber)}
-                    />
+                    <NumberInput {...field} />
                 </FormControl>
             );
         case 'DateTime':
@@ -152,15 +149,15 @@ export function DetailPage<
     C extends TypedDocumentNode<any, any>,
     U extends TypedDocumentNode<any, any>,
 >({
-      pageId,
-      route,
-      entityName: passedEntityName,
-      queryDocument,
-      createDocument,
-      updateDocument,
-      setValuesForUpdate,
-      title,
-  }: DetailPageProps<T, C, U>) {
+    pageId,
+    route,
+    entityName: passedEntityName,
+    queryDocument,
+    createDocument,
+    updateDocument,
+    setValuesForUpdate,
+    title,
+}: DetailPageProps<T, C, U>) {
     const params = route.useParams();
     const creatingNewEntity = params.id === NEW_ENTITY_PATH;
     const navigate = useNavigate();