aboutsummaryrefslogtreecommitdiff
path: root/packages/exchange-backoffice-ui/src/forms
diff options
context:
space:
mode:
Diffstat (limited to 'packages/exchange-backoffice-ui/src/forms')
-rw-r--r--packages/exchange-backoffice-ui/src/forms/InputArray.tsx191
-rw-r--r--packages/exchange-backoffice-ui/src/forms/InputFile.tsx99
-rw-r--r--packages/exchange-backoffice-ui/src/forms/forms.ts4
3 files changed, 206 insertions, 88 deletions
diff --git a/packages/exchange-backoffice-ui/src/forms/InputArray.tsx b/packages/exchange-backoffice-ui/src/forms/InputArray.tsx
index 2447c9989..b2905a2a7 100644
--- a/packages/exchange-backoffice-ui/src/forms/InputArray.tsx
+++ b/packages/exchange-backoffice-ui/src/forms/InputArray.tsx
@@ -4,6 +4,72 @@ import { FormProvider } from "./FormProvider.js";
import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
import { useField } from "./useField.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+function Option({
+ label,
+ disabled,
+ isFirst,
+ isLast,
+ isSelected,
+ onClick,
+}: {
+ label: TranslatedString;
+ isFirst?: boolean;
+ isLast?: boolean;
+ isSelected?: boolean;
+ disabled?: boolean;
+ onClick: () => void;
+}): VNode {
+ let clazz = "relative flex border p-4 focus:outline-none disabled:text-grey";
+ if (isFirst) {
+ clazz += " rounded-tl-md rounded-tr-md ";
+ }
+ if (isLast) {
+ clazz += " rounded-bl-md rounded-br-md ";
+ }
+ if (isSelected) {
+ clazz += " z-10 border-indigo-200 bg-indigo-50 ";
+ } else {
+ clazz += " border-gray-200";
+ }
+ if (disabled) {
+ clazz +=
+ " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray";
+ } else {
+ clazz += " cursor-pointer";
+ }
+ return (
+ <label class={clazz}>
+ <input
+ type="radio"
+ name="privacy-setting"
+ checked={isSelected}
+ disabled={disabled}
+ onClick={onClick}
+ class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600"
+ aria-labelledby="privacy-setting-0-label"
+ aria-describedby="privacy-setting-0-description"
+ />
+ <span class="ml-3 flex flex-col">
+ <span
+ id="privacy-setting-0-label"
+ disabled
+ class="block text-sm font-medium"
+ >
+ {label}
+ </span>
+ {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */}
+ {/* <span
+ id="privacy-setting-0-description"
+ class="block text-sm"
+ >
+ This project would be available to anyone who has the link
+ </span> */}
+ </span>
+ </label>
+ );
+}
export function InputArray(
props: {
@@ -17,10 +83,7 @@ export function InputArray(
const [selectedIndex, setSelected] = useState<number | undefined>(undefined);
const selected =
selectedIndex === undefined ? undefined : list[selectedIndex];
- const [subForm, updateSubForm] = useState(selected ?? {});
- useEffect(() => {
- updateSubForm(selected);
- }, [selected]);
+
return (
<div class="sm:col-span-6">
<LabelWithTooltipMaybeRequired
@@ -29,95 +92,47 @@ export function InputArray(
tooltip={tooltip}
/>
- <div class="flex mb-4 items-center pt-3">
- <div class="flex-auto">
- {selectedIndex !== undefined && (
- <button
- type="button"
- onClick={() => {
- setSelected(undefined);
- }}
- class="block rounded-md bg-white px-3 py-2 text-center text-sm font-semibold shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
- >
- Cancel
- </button>
- )}
- </div>
- <div class="flex-none">
- {selectedIndex === undefined && (
- <button
- type="button"
- onClick={() => {
- setSelected(list.length);
- }}
- class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-indigo-500 "
- >
- Add
- </button>
- )}
- </div>
- </div>
<div class="-space-y-px rounded-md bg-white ">
{list.map((v, idx) => {
- const isFirst = idx === 0;
- const isLast = idx === list.length - 1;
- const isSelected = selectedIndex === idx;
- const disabled = selectedIndex !== undefined && !isSelected;
- let clazz =
- "relative flex border p-4 focus:outline-none disabled:text-grey";
- if (isFirst) {
- clazz += " rounded-tl-md rounded-tr-md ";
- }
- if (isLast) {
- clazz += " rounded-bl-md rounded-br-md ";
- }
- if (isSelected) {
- clazz += " z-10 border-indigo-200 bg-indigo-50 ";
- } else {
- clazz += " border-gray-200";
- }
- if (disabled) {
- clazz +=
- " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray";
- } else {
- clazz += " cursor-pointer";
- }
return (
- <label class={clazz}>
- <Fragment>
- <input
- type="radio"
- name="privacy-setting"
- checked={isSelected}
- disabled={disabled}
- onClick={() => setSelected(idx)}
- class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600"
- aria-labelledby="privacy-setting-0-label"
- aria-describedby="privacy-setting-0-description"
- />
- <span class="ml-3 flex flex-col">
- <span
- id="privacy-setting-0-label"
- disabled
- class="block text-sm font-medium"
- >
- {v[labelField]}
- </span>
- {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */}
- {/* <span
- id="privacy-setting-0-description"
- class="block text-sm"
- >
- This project would be available to anyone who has the link
- </span> */}
- </span>
- </Fragment>
- </label>
+ <Option
+ label={v[labelField]}
+ isSelected={selectedIndex === idx}
+ isLast={idx === list.length - 1}
+ disabled={selectedIndex !== undefined && selectedIndex !== idx}
+ isFirst={idx === 0}
+ onClick={() => {
+ setSelected(selectedIndex === idx ? undefined : idx);
+ }}
+ />
);
})}
+ <div class="pt-2">
+ <Option
+ label={"Add..." as TranslatedString}
+ isSelected={selectedIndex === list.length}
+ isLast
+ isFirst
+ disabled={
+ selectedIndex !== undefined && selectedIndex !== list.length
+ }
+ onClick={() => {
+ setSelected(
+ selectedIndex === list.length ? undefined : list.length,
+ );
+ }}
+ />
+ </div>
</div>
{selectedIndex !== undefined && (
- <FormProvider initialValue={subForm} onUpdate={updateSubForm}>
+ <FormProvider
+ initialValue={selected}
+ onUpdate={(v) => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1, v);
+ onChange(newValue);
+ }}
+ >
<div class="px-4 py-6">
<div class="grid grid-cols-1 gap-y-8 ">
<RenderAllFieldsByUiConfig fields={fields} />
@@ -143,7 +158,7 @@ export function InputArray(
</button>
)}
</div>
- <div class="flex-none">
+ {/* <div class="flex-none">
<button
type="button"
onClick={() => {
@@ -156,7 +171,7 @@ export function InputArray(
>
Confirm
</button>
- </div>
+ </div> */}
</div>
)}
</div>
diff --git a/packages/exchange-backoffice-ui/src/forms/InputFile.tsx b/packages/exchange-backoffice-ui/src/forms/InputFile.tsx
new file mode 100644
index 000000000..749c8b264
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/forms/InputFile.tsx
@@ -0,0 +1,99 @@
+import { VNode, h } from "preact";
+import {
+ InputLine,
+ LabelWithTooltipMaybeRequired,
+ UIFormProps,
+} from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputFile(
+ props: { maxBites: number; accept?: string } & UIFormProps<string>,
+): VNode {
+ const {
+ name,
+ label,
+ placeholder,
+ tooltip,
+ required,
+ help,
+ maxBites,
+ accept,
+ } = props;
+ const { value, onChange } = useField<{ [s: string]: string }>(name);
+
+ return (
+ <div class="col-span-full">
+ <LabelWithTooltipMaybeRequired
+ label={label}
+ tooltip={tooltip}
+ required={required}
+ />
+ {!value || !value.startsWith("data:image/") ? (
+ <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1">
+ <div class="text-center">
+ <svg
+ class="mx-auto h-12 w-12 text-gray-300"
+ viewBox="0 0 24 24"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ <div class="my-2 flex text-sm leading-6 text-gray-600">
+ <label
+ for="file-upload"
+ class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
+ >
+ <span>Upload a file</span>
+ <input
+ id="file-upload"
+ name="file-upload"
+ type="file"
+ class="sr-only"
+ accept={accept}
+ onChange={(e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return onChange(undefined!);
+ }
+ if (f[0].size > maxBites) {
+ return onChange(undefined!);
+ }
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = window.btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return onChange(`data:${f[0].type};base64,${b64}` as any);
+ });
+ }}
+ />
+ </label>
+ {/* <p class="pl-1">or drag and drop</p> */}
+ </div>
+ </div>
+ </div>
+ ) : (
+ <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative">
+ <img src={value} class=" h-24 w-full object-cover relative" />
+
+ <div
+ class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer "
+ onClick={() => {
+ onChange(undefined!);
+ }}
+ >
+ Clear
+ </div>
+ </div>
+ )}
+ {help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>}
+ </div>
+ );
+}
diff --git a/packages/exchange-backoffice-ui/src/forms/forms.ts b/packages/exchange-backoffice-ui/src/forms/forms.ts
index 1e59e0cf2..c2cb0799e 100644
--- a/packages/exchange-backoffice-ui/src/forms/forms.ts
+++ b/packages/exchange-backoffice-ui/src/forms/forms.ts
@@ -7,6 +7,7 @@ import { InputChoiceStacked } from "./InputChoice.js";
import { InputArray } from "./InputArray.js";
import { InputSelectMultiple } from "./InputSelectMultiple.js";
import { InputTextArea } from "./InputTextArea.js";
+import { InputFile } from "./InputFile.js";
export type DoubleColumnForm = DoubleColumnFormSection[];
@@ -22,6 +23,7 @@ type DoubleColumnFormSection = {
type FieldType = {
separator: {};
array: Parameters<typeof InputArray>[0];
+ file: Parameters<typeof InputFile>[0];
selectMultiple: Parameters<typeof InputSelectMultiple>[0];
text: Parameters<typeof InputText>[0];
textArea: Parameters<typeof InputTextArea>[0];
@@ -36,6 +38,7 @@ type FieldType = {
export type UIFormField =
| { type: "separator"; props: FieldType["separator"] }
| { type: "array"; props: FieldType["array"] }
+ | { type: "file"; props: FieldType["file"] }
| { type: "selectMultiple"; props: FieldType["selectMultiple"] }
| { type: "text"; props: FieldType["text"] }
| { type: "textArea"; props: FieldType["textArea"] }
@@ -62,6 +65,7 @@ const UIFormConfiguration: UIFormFieldMap = {
separator: Separator,
array: InputArray,
text: InputText,
+ file: InputFile,
textArea: InputTextArea,
date: InputDate,
choiceStacked: InputChoiceStacked,