aboutsummaryrefslogtreecommitdiff
path: root/packages/exchange-backoffice-ui/src/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'packages/exchange-backoffice-ui/src/handlers')
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/Group.tsx41
-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.tsx8
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx136
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/Separator.tsx26
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/forms.ts17
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,
};