gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-typescript-core] 02/08: input duration


From: gnunet
Subject: [taler-typescript-core] 02/08: input duration
Date: Wed, 15 Jan 2025 22:32:48 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository taler-typescript-core.

commit 411dcd6b32501c1c1c91727b428ae0e46c76460b
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Jan 13 18:07:40 2025 -0300

    input duration
---
 packages/web-util/src/forms/field-types.ts         |   9 +-
 ...ecret.stories.tsx => InputDuration.stories.tsx} |  21 ++-
 .../web-util/src/forms/fields/InputDuration.tsx    | 183 +++++++++++++++++++++
 packages/web-util/src/forms/fields/InputLine.tsx   |   2 +-
 .../src/forms/fields/InputSecret.stories.tsx       |   2 +-
 packages/web-util/src/forms/forms-types.ts         |  15 +-
 packages/web-util/src/forms/forms-utils.ts         |  13 ++
 packages/web-util/src/forms/index.stories.ts       |   1 +
 packages/web-util/src/hooks/useForm.ts             |   2 +-
 9 files changed, 235 insertions(+), 13 deletions(-)

diff --git a/packages/web-util/src/forms/field-types.ts 
b/packages/web-util/src/forms/field-types.ts
index 23fcf5fe7..34696b430 100644
--- a/packages/web-util/src/forms/field-types.ts
+++ b/packages/web-util/src/forms/field-types.ts
@@ -9,9 +9,10 @@ import { InputChoiceStacked } from 
"./fields/InputChoiceStacked.js";
 import { InputFile } from "./fields/InputFile.js";
 import { InputInteger } from "./fields/InputInteger.js";
 import { InputSelectMultiple } from "./fields/InputSelectMultiple.js";
+import { InputDuration } from "./fields/InputDuration.js";
+import { InputSecret } from "./fields/InputSecret.js";
 import { InputSelectOne } from "./fields/InputSelectOne.js";
 import { InputText } from "./fields/InputText.js";
-import { InputSecret } from "./fields/InputSecret.js";
 import { InputTextArea } from "./fields/InputTextArea.js";
 import { InputToggle } from "./fields/InputToggle.js";
 import { Group } from "./Group.js";
@@ -37,6 +38,7 @@ type FieldType<T extends object = any, K extends keyof T = 
any> = {
   secret: Parameters<typeof InputSecret<T, K>>[0];
   toggle: Parameters<typeof InputToggle<T, K>>[0];
   amount: Parameters<typeof InputAmount<T, K>>[0];
+  duration: Parameters<typeof InputDuration<T, K>>[0];
 };
 
 /**
@@ -71,6 +73,10 @@ export type UIFormField =
   | {
       type: "absoluteTimeText";
       properties: FieldType["absoluteTimeText"];
+    }
+  | {
+      type: "duration";
+      properties: FieldType["duration"];
     };
 
 export type FieldComponentFunction<key extends keyof FieldType> = (
@@ -111,4 +117,5 @@ export const UIFormConfiguration: UIFormFieldMap = {
   toggle: InputToggle,
   //@ts-ignore
   amount: InputAmount,
+  duration: InputDuration,
 };
diff --git a/packages/web-util/src/forms/fields/InputSecret.stories.tsx 
b/packages/web-util/src/forms/fields/InputDuration.stories.tsx
similarity index 80%
copy from packages/web-util/src/forms/fields/InputSecret.stories.tsx
copy to packages/web-util/src/forms/fields/InputDuration.stories.tsx
index e143c3af8..8c0983287 100644
--- a/packages/web-util/src/forms/fields/InputSecret.stories.tsx
+++ b/packages/web-util/src/forms/fields/InputDuration.stories.tsx
@@ -19,20 +19,25 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { TranslatedString } from "@gnu-taler/taler-util";
+import { Duration, TranslatedString } from "@gnu-taler/taler-util";
 import * as tests from "../../tests/hook.js";
-import { DefaultForm as TestedComponent } from "../forms-ui.js";
 import { FormDesign, UIHandlerId } from "../forms-types.js";
+import { DefaultForm as TestedComponent } from "../forms-ui.js";
 
 export default {
-  title: "Input secret",
+  title: "Input duration",
 };
 
 type TargetObject = {
-  pwd: string;
+  time: Duration;
 };
 const initial: TargetObject = {
-  pwd: 5,
+  time: Duration.fromSpec({
+    days: 1,
+    hours: 2,
+    minutes: 3,
+    seconds: 4,
+  }),
 };
 
 const design: FormDesign = {
@@ -42,9 +47,9 @@ const design: FormDesign = {
       title: "this is a simple form" as TranslatedString,
       fields: [
         {
-          type: "secret",
-          label: "Password" as TranslatedString,
-          id: "pwd" as UIHandlerId,
+          type: "duration",
+          label: "How long?" as TranslatedString,
+          id: "time" as UIHandlerId,
         },
       ],
     },
diff --git a/packages/web-util/src/forms/fields/InputDuration.tsx 
b/packages/web-util/src/forms/fields/InputDuration.tsx
new file mode 100644
index 000000000..94cd6d6aa
--- /dev/null
+++ b/packages/web-util/src/forms/fields/InputDuration.tsx
@@ -0,0 +1,183 @@
+import { Fragment, VNode, h } from "preact";
+import { useTranslationContext } from "../../index.browser.js";
+import { UIFormProps } from "../FormProvider.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
+import { InputWrapper } from "./InputLine.js";
+import { Duration } from "@gnu-taler/taler-util";
+import { useEffect, useState } from "preact/hooks";
+
+export function InputDuration<T extends object, K extends keyof T>(
+  props: UIFormProps<T, K>,
+): VNode {
+  const { name, placeholder, before, after, converter, disabled } = props;
+  const { i18n } = useTranslationContext();
+  const { value, onChange, state, error } =
+    props.handler ?? noHandlerPropsAndNoContextForField(props.name);
+
+  const sd = Duration.toSpec(value as Duration);
+  const [days, setDays] = useState(sd?.days ?? 0);
+  const [hours, setHours] = useState(sd?.hours ?? 0);
+  const [minutes, setMinutes] = useState(sd?.minutes ?? 0);
+  const [seconds, setSeconds] = useState(sd?.seconds ?? 0);
+
+  useEffect(() => {
+    onChange(
+      Duration.fromSpec({
+        days,
+        hours,
+        minutes,
+        seconds,
+      }),
+    );
+  }, [days, hours, minutes, seconds]);
+  const fromString: (s: string) => any =
+    converter?.fromStringUI ?? defaultFromString;
+  const toString: (s: any) => string = converter?.toStringUI ?? 
defaultToString;
+
+  if (state.hidden) return <div />;
+
+  let clazz =
+    "block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset 
focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 
disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 
disabled:ring-gray-200";
+  // if (before) {
+  //   switch (before.type) {
+  //     case "icon": {
+  //       clazz += " pl-10";
+  //       break;
+  //     }
+  //     case "button": {
+  //       clazz += " rounded-none rounded-r-md ";
+  //       break;
+  //     }
+  //     case "text": {
+  clazz += " min-w-0 flex-1 rounded-r-md rounded-none ";
+  //       break;
+  //     }
+  //   }
+  // }
+  if (after) {
+    switch (after.type) {
+      case "icon": {
+        clazz += " pr-10";
+        break;
+      }
+      case "button": {
+        clazz += " rounded-none rounded-l-md";
+        break;
+      }
+      case "text": {
+        clazz += " min-w-0 flex-1 rounded-l-md rounded-none ";
+        break;
+      }
+    }
+  }
+  const showError = value !== undefined && error;
+  if (showError) {
+    clazz +=
+      " text-red-900 ring-red-300  placeholder:text-red-300 
focus:ring-red-500";
+  } else {
+    clazz +=
+      " text-gray-900 ring-gray-300 placeholder:text-gray-400 
focus:ring-indigo-600";
+  }
+  return (
+    <InputWrapper<T, K>
+      {...props}
+      help={props.help ?? state.help}
+      disabled={disabled ?? false}
+      error={showError ? error : undefined}
+    >
+      <div class="flex flex-col gap-1">
+        <div class="flex">
+          <span class="ml-2 inline-flex items-center rounded-l-md border 
border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+            <i18n.Translate>days</i18n.Translate>
+          </span>
+          <input
+            name={String(name)}
+            type="number"
+            onChange={(e) => {
+              setDays(fromString(e.currentTarget.value));
+            }}
+            placeholder={placeholder ? placeholder : undefined}
+            value={toString(sd?.days) ?? ""}
+            // onBlur={() => {
+            //   onChange(fromString(value as any));
+            // }}
+            // defaultValue={toString(value)}
+            disabled={disabled ?? false}
+            aria-invalid={showError}
+            // aria-describedby="email-error"
+            class={clazz}
+          />
+          <span class="ml-2 inline-flex items-center rounded-l-md border 
border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+            <i18n.Translate>hours</i18n.Translate>
+          </span>
+          <input
+            name={String(name)}
+            type="number"
+            onChange={(e) => {
+              setHours(fromString(e.currentTarget.value));
+            }}
+            placeholder={placeholder ? placeholder : undefined}
+            value={toString(sd?.hours) ?? ""}
+            // onBlur={() => {
+            //   onChange(fromString(value as any));
+            // }}
+            // defaultValue={toString(value)}
+            disabled={disabled ?? false}
+            aria-invalid={showError}
+            // aria-describedby="email-error"
+            class={clazz}
+          />
+        </div>
+        <div class="flex">
+          <span class="ml-2 inline-flex items-center rounded-l-md border 
border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+            <i18n.Translate>minutes</i18n.Translate>
+          </span>
+          <input
+            name={String(name)}
+            type="number"
+            onChange={(e) => {
+              setMinutes(fromString(e.currentTarget.value));
+            }}
+            placeholder={placeholder ? placeholder : undefined}
+            value={toString(sd?.minutes) ?? ""}
+            // onBlur={() => {
+            //   onChange(fromString(value as any));
+            // }}
+            // defaultValue={toString(value)}
+            disabled={disabled ?? false}
+            aria-invalid={showError}
+            // aria-describedby="email-error"
+            class={clazz}
+          />
+          <span class="ml-2 inline-flex items-center rounded-l-md border 
border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+            <i18n.Translate>seconds</i18n.Translate>
+          </span>
+          <input
+            name={String(name)}
+            type="number"
+            onChange={(e) => {
+              setSeconds(fromString(e.currentTarget.value));
+            }}
+            placeholder={placeholder ? placeholder : undefined}
+            value={toString(sd?.seconds) ?? ""}
+            // onBlur={() => {
+            //   onChange(fromString(value as any));
+            // }}
+            // defaultValue={toString(value)}
+            disabled={disabled ?? false}
+            aria-invalid={showError}
+            // aria-describedby="email-error"
+            class={clazz}
+          />
+        </div>
+      </div>
+    </InputWrapper>
+  );
+}
+
+function defaultToString(v: unknown) {
+  return v === undefined ? "" : typeof v !== "object" ? String(v) : "";
+}
+function defaultFromString(v: string) {
+  return v;
+}
diff --git a/packages/web-util/src/forms/fields/InputLine.tsx 
b/packages/web-util/src/forms/fields/InputLine.tsx
index 982fd4670..bbbc871e0 100644
--- a/packages/web-util/src/forms/fields/InputLine.tsx
+++ b/packages/web-util/src/forms/fields/InputLine.tsx
@@ -104,7 +104,7 @@ export function RenderAddon({
   }
 }
 
-function InputWrapper<T extends object, K extends keyof T>({
+export function InputWrapper<T extends object, K extends keyof T>({
   children,
   label,
   tooltip,
diff --git a/packages/web-util/src/forms/fields/InputSecret.stories.tsx 
b/packages/web-util/src/forms/fields/InputSecret.stories.tsx
index e143c3af8..5179b6fbb 100644
--- a/packages/web-util/src/forms/fields/InputSecret.stories.tsx
+++ b/packages/web-util/src/forms/fields/InputSecret.stories.tsx
@@ -32,7 +32,7 @@ type TargetObject = {
   pwd: string;
 };
 const initial: TargetObject = {
-  pwd: 5,
+  pwd: "5",
 };
 
 const design: FormDesign = {
diff --git a/packages/web-util/src/forms/forms-types.ts 
b/packages/web-util/src/forms/forms-types.ts
index d5bcced6c..0da107e34 100644
--- a/packages/web-util/src/forms/forms-types.ts
+++ b/packages/web-util/src/forms/forms-types.ts
@@ -17,7 +17,9 @@ import {
   TranslatedString,
 } from "@gnu-taler/taler-util";
 
-export type FormDesign = DoubleColumnFormDesign | SingleColumnFormDesign;
+export type FormDesign<T = unknown> =
+  | DoubleColumnFormDesign
+  | SingleColumnFormDesign;
 
 /**
  * form with composed by multiple sections
@@ -56,6 +58,7 @@ export type UIFormElementConfig =
   | UIFormFieldInteger
   | UIFormFieldSecret
   | UIFormFieldSelectMultiple
+  | UIFormFieldDuration
   | UIFormFieldSelectOne
   | UIFormFieldText
   | UIFormFieldTextArea
@@ -144,6 +147,10 @@ type UIFormFieldSelectMultiple = {
   allowFreeForm?: boolean;
 } & UIFormFieldBaseConfig;
 
+type UIFormFieldDuration = {
+  type: "duration";
+} & UIFormFieldBaseConfig;
+
 type UIFormFieldSelectOne = {
   type: "selectOne";
   choices: Array<SelectUiChoice>;
@@ -320,6 +327,11 @@ const codecForUiFormFieldSecret = (): 
Codec<UIFormFieldSecret> =>
     .property("type", codecForConstString("secret"))
     .build("UIFormFieldSecret");
 
+const codecForUiFormFieldDuration = (): Codec<UIFormFieldDuration> =>
+  codecForUIFormFieldBaseConfigTemplate<UIFormFieldDuration>()
+    .property("type", codecForConstString("duration"))
+    .build("UiFormFieldDuration");
+
 const codecForUiFormFieldSelectMultiple =
   (): Codec<UIFormFieldSelectMultiple> =>
     codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectMultiple>()
@@ -370,6 +382,7 @@ const codecForUiFormField = (): Codec<UIFormElementConfig> 
=>
     .alternative("integer", codecForUiFormFieldInteger())
     .alternative("secret", codecForUiFormFieldSecret())
     .alternative("selectMultiple", codecForUiFormFieldSelectMultiple())
+    .alternative("duration", codecForUiFormFieldDuration())
     .alternative("selectOne", codecForUiFormFieldSelectOne())
     .alternative("text", codecForUiFormFieldText())
     .alternative("textArea", codecForUiFormFieldTextArea())
diff --git a/packages/web-util/src/forms/forms-utils.ts 
b/packages/web-util/src/forms/forms-utils.ts
index b6797d8d1..dc7d7d22f 100644
--- a/packages/web-util/src/forms/forms-utils.ts
+++ b/packages/web-util/src/forms/forms-utils.ts
@@ -245,6 +245,19 @@ export function convertFormConfigToUiField(
           },
         } as UIFormField;
       }
+      case "duration": {
+        return {
+          type: "duration",
+          properties: {
+            ...converBaseFieldsProps(i18n_, config),
+            ...converInputFieldsProps(
+              form,
+              config,
+              getConverterByFieldType(config.type, config),
+            ),
+          },
+        } as UIFormField;
+      }
       case "toggle": {
         return {
           type: "toggle",
diff --git a/packages/web-util/src/forms/index.stories.ts 
b/packages/web-util/src/forms/index.stories.ts
index 702114f65..5b62c512a 100644
--- a/packages/web-util/src/forms/index.stories.ts
+++ b/packages/web-util/src/forms/index.stories.ts
@@ -11,3 +11,4 @@ export * as a11 from "./fields/InputText.stories.js";
 export * as a12 from "./fields/InputTextArea.stories.js";
 export * as a13 from "./fields/InputToggle.stories.js";
 export * as a14 from "./fields/InputSecret.stories.js";
+export * as a15 from "./fields/InputDuration.stories.js";
diff --git a/packages/web-util/src/hooks/useForm.ts 
b/packages/web-util/src/hooks/useForm.ts
index a669f3007..54cbc06e5 100644
--- a/packages/web-util/src/hooks/useForm.ts
+++ b/packages/web-util/src/hooks/useForm.ts
@@ -110,7 +110,7 @@ function checkAllRequirements<T>(
  * @returns
  */
 export function useForm<T>(
-  design: FormDesign,
+  design: FormDesign<T>,
   initialValue: RecursivePartial<FormValues<T>>,
   check?: (f: RecursivePartial<FormValues<T>>) => FormErrors<T> | undefined,
 ): FormState<T> {

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]