diff options
Diffstat (limited to 'packages/exchange-backoffice-ui/src/handlers')
| -rw-r--r-- | packages/exchange-backoffice-ui/src/handlers/Group.tsx | 41 | ||||
| -rw-r--r-- | packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx (renamed from packages/exchange-backoffice-ui/src/handlers/InputChoice.tsx) | 0 | ||||
| -rw-r--r-- | packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx | 8 | ||||
| -rw-r--r-- | packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx | 136 | ||||
| -rw-r--r-- | packages/exchange-backoffice-ui/src/handlers/Separator.tsx | 26 | ||||
| -rw-r--r-- | packages/exchange-backoffice-ui/src/handlers/forms.ts | 17 | 
6 files changed, 220 insertions, 8 deletions
diff --git a/packages/exchange-backoffice-ui/src/handlers/Group.tsx b/packages/exchange-backoffice-ui/src/handlers/Group.tsx new file mode 100644 index 000000000..04af0647b --- /dev/null +++ b/packages/exchange-backoffice-ui/src/handlers/Group.tsx @@ -0,0 +1,41 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { VNode, h } from "preact"; +import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; +import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; + +interface Props { +  before?: TranslatedString; +  after?: TranslatedString; +  tooltipBefore?: TranslatedString; +  tooltipAfter?: TranslatedString; +  fields: UIFormField[]; +} + +export function Group({ +  before, +  after, +  tooltipAfter, +  tooltipBefore, +  fields, +}: Props): VNode { +  return ( +    <div class="sm:col-span-6 p-4 rounded-lg border-r-2 border-2 bg-gray-50"> +      <div class="pb-4"> +        {before && ( +          <LabelWithTooltipMaybeRequired +            label={before} +            tooltip={tooltipBefore} +          /> +        )} +      </div> +      <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> +        <RenderAllFieldsByUiConfig fields={fields} /> +      </div> +      <div class="pt-4"> +        {after && ( +          <LabelWithTooltipMaybeRequired label={after} tooltip={tooltipAfter} /> +        )} +      </div> +    </div> +  ); +} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputChoice.tsx b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx index b19a0d82e..b19a0d82e 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputChoice.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx diff --git a/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx b/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx index 05733fe19..dd85453e5 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx @@ -1,5 +1,5 @@  import { Fragment, VNode, h } from "preact"; -import { Choice } from "./InputChoice.js"; +import { Choice } from "./InputChoiceStacked.js";  import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";  import { useField } from "./useField.js";  import { useState } from "preact/hooks"; @@ -8,9 +8,10 @@ export function InputSelectMultiple(    props: {      choices: Choice[];      unique?: boolean; +    max?: number;    } & UIFormProps<Array<string>>,  ): VNode { -  const { name, label, choices, placeholder, tooltip, required, unique } = +  const { name, label, choices, placeholder, tooltip, required, unique, max } =      props;    const { value, onChange } = useField<{ [s: string]: Array<string> }>(name); @@ -113,6 +114,9 @@ export function InputSelectMultiple(                      if (unique && list.indexOf(v.value) !== -1) {                        return;                      } +                    if (max !== undefined && list.length >= max) { +                      return; +                    }                      const newValue = [...list];                      newValue.splice(0, 0, v.value);                      onChange(newValue); diff --git a/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx b/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx new file mode 100644 index 000000000..a4ed1ba1c --- /dev/null +++ b/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx @@ -0,0 +1,136 @@ +import { Fragment, VNode, h } from "preact"; +import { Choice } from "./InputChoiceStacked.js"; +import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; +import { useField } from "./useField.js"; +import { useState } from "preact/hooks"; + +export function InputSelectOne( +  props: { +    choices: Choice[]; +  } & UIFormProps<Array<string>>, +): VNode { +  const { name, label, choices, placeholder, tooltip, required } = props; +  const { value, onChange } = useField<{ [s: string]: string | undefined }>( +    name, +  ); + +  const [filter, setFilter] = useState<string | undefined>(undefined); +  const regex = new RegExp(`.*${filter}.*`, "i"); +  const choiceMap = choices.reduce((prev, curr) => { +    return { ...prev, [curr.value]: curr.label }; +  }, {} as Record<string, string>); + +  const filteredChoices = +    filter === undefined +      ? undefined +      : choices.filter((v) => { +          return regex.test(v.label); +        }); +  return ( +    <div class="sm:col-span-6"> +      <LabelWithTooltipMaybeRequired +        label={label} +        required={required} +        tooltip={tooltip} +      /> +      {value ? ( +        <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 font-medium text-gray-600"> +          {choiceMap[value]} +          <button +            type="button" +            onClick={() => { +              onChange(undefined); +            }} +            class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" +          > +            <span class="sr-only">Remove</span> +            <svg +              viewBox="0 0 14 14" +              class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75" +            > +              <path d="M4 4l6 6m0-6l-6 6" /> +            </svg> +            <span class="absolute -inset-1"></span> +          </button> +        </span> +      ) : ( +        <div class="relative mt-2"> +          <input +            id="combobox" +            type="text" +            value={filter ?? ""} +            onChange={(e) => { +              setFilter(e.currentTarget.value); +            }} +            placeholder={placeholder} +            class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" +            role="combobox" +            aria-controls="options" +            aria-expanded="false" +          /> +          <button +            type="button" +            onClick={() => { +              setFilter(filter === undefined ? "" : undefined); +            }} +            class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" +          > +            <svg +              class="h-5 w-5 text-gray-400" +              viewBox="0 0 20 20" +              fill="currentColor" +              aria-hidden="true" +            > +              <path +                fill-rule="evenodd" +                d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" +                clip-rule="evenodd" +              /> +            </svg> +          </button> + +          {filteredChoices !== undefined && ( +            <ul +              class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" +              id="options" +              role="listbox" +            > +              {filteredChoices.map((v, idx) => { +                return ( +                  <li +                    class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600" +                    id="option-0" +                    role="option" +                    onClick={() => { +                      setFilter(undefined); +                      onChange(v.value); +                    }} + +                    // tabindex="-1" +                  > +                    {/* <!-- Selected: "font-semibold" --> */} +                    <span class="block truncate">{v.label}</span> + +                    {/* <!-- +          Checkmark, only display for selected option. + +          Active: "text-white", Not Active: "text-indigo-600" +        --> */} +                  </li> +                ); +              })} + +              {/* <!-- +        Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation. + +        Active: "text-white bg-indigo-600", Not Active: "text-gray-900" +      --> */} + +              {/* <!-- More items... --> */} +            </ul> +          )} +        </div> +      )} +    </div> +  ); +} diff --git a/packages/exchange-backoffice-ui/src/handlers/Separator.tsx b/packages/exchange-backoffice-ui/src/handlers/Separator.tsx new file mode 100644 index 000000000..5fa25c3ca --- /dev/null +++ b/packages/exchange-backoffice-ui/src/handlers/Separator.tsx @@ -0,0 +1,26 @@ +import { VNode, h } from "preact"; +import { +  InputLine, +  LabelWithTooltipMaybeRequired, +  UIFormProps, +} from "./InputLine.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; + +interface Props { +  label: TranslatedString; +  tooltip?: TranslatedString; +  help?: TranslatedString; +} + +export function Separator({ label, tooltip, help }: Props): VNode { +  return ( +    <div class="sm:col-span-6"> +      <LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} /> +      {help && ( +        <p class="mt-2 text-sm text-gray-500" id="email-description"> +          {help} +        </p> +      )} +    </div> +  ); +} diff --git a/packages/exchange-backoffice-ui/src/handlers/forms.ts b/packages/exchange-backoffice-ui/src/handlers/forms.ts index b5d1a2b20..418754919 100644 --- a/packages/exchange-backoffice-ui/src/handlers/forms.ts +++ b/packages/exchange-backoffice-ui/src/handlers/forms.ts @@ -3,11 +3,14 @@ import { InputText } from "./InputText.js";  import { InputDate } from "./InputDate.js";  import { InputInteger } from "./InputInteger.js";  import { h as create, Fragment, VNode } from "preact"; -import { InputChoiceStacked } from "./InputChoice.js"; +import { InputChoiceStacked } from "./InputChoiceStacked.js";  import { InputArray } from "./InputArray.js";  import { InputSelectMultiple } from "./InputSelectMultiple.js";  import { InputTextArea } from "./InputTextArea.js";  import { InputFile } from "./InputFile.js"; +import { Separator } from "./Separator.js"; +import { Group } from "./Group.js"; +import { InputSelectOne } from "./InputSelectOne.js";  export type DoubleColumnForm = DoubleColumnFormSection[]; @@ -21,9 +24,11 @@ type DoubleColumnFormSection = {   * Constrain the type with the ui props   */  type FieldType = { -  separator: {}; +  group: Parameters<typeof Group>[0]; +  separator: Parameters<typeof Separator>[0];    array: Parameters<typeof InputArray>[0];    file: Parameters<typeof InputFile>[0]; +  selectOne: Parameters<typeof InputSelectOne>[0];    selectMultiple: Parameters<typeof InputSelectMultiple>[0];    text: Parameters<typeof InputText>[0];    textArea: Parameters<typeof InputTextArea>[0]; @@ -36,9 +41,11 @@ type FieldType = {   * List all the form fields so typescript can type-check the form instance   */  export type UIFormField = +  | { type: "group"; props: FieldType["group"] }    | { type: "separator"; props: FieldType["separator"] }    | { type: "array"; props: FieldType["array"] }    | { type: "file"; props: FieldType["file"] } +  | { type: "selectOne"; props: FieldType["selectOne"] }    | { type: "selectMultiple"; props: FieldType["selectMultiple"] }    | { type: "text"; props: FieldType["text"] }    | { type: "textArea"; props: FieldType["textArea"] } @@ -54,14 +61,11 @@ type UIFormFieldMap = {    [key in keyof FieldType]: FieldComponentFunction<key>;  }; -function Separator(): VNode { -  return create("div", {}); -} -  /**   * Maps input type with component implementation   */  const UIFormConfiguration: UIFormFieldMap = { +  group: Group,    separator: Separator,    array: InputArray,    text: InputText, @@ -70,6 +74,7 @@ const UIFormConfiguration: UIFormFieldMap = {    date: InputDate,    choiceStacked: InputChoiceStacked,    integer: InputInteger, +  selectOne: InputSelectOne,    selectMultiple: InputSelectMultiple,  };  | 
