summaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx')
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx204
1 files changed, 204 insertions, 0 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
new file mode 100644
index 000000000..9956a6427
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
@@ -0,0 +1,204 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import emptyImage from "../../assets/empty.png";
+import { FormErrors, FormProvider } from "./FormProvider.js";
+import { InputWithAddon } from "./InputWithAddon.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+type Entity = {
+ id: string,
+ description: string;
+ image?: string;
+ extra?: string;
+};
+
+export interface Props<T extends Entity> {
+ selected?: T;
+ onChange: (p?: T) => void;
+ label: TranslatedString;
+ list: T[];
+ withImage?: boolean;
+}
+
+interface Search {
+ name: string;
+}
+
+export function InputSearchOnList<T extends Entity>({
+ selected,
+ onChange,
+ label,
+ list,
+ withImage,
+}: Props<T>): VNode {
+ const [nameForm, setNameForm] = useState<Partial<Search>>({
+ name: "",
+ });
+
+ const errors: FormErrors<Search> = {
+ name: undefined,
+ };
+ const { i18n } = useTranslationContext();
+
+ if (selected) {
+ return (
+ <article class="media">
+ {withImage &&
+ <figure class="media-left">
+ <p class="image is-128x128">
+ <img src={selected.image ? selected.image : emptyImage} />
+ </p>
+ </figure>
+ }
+ <div class="media-content">
+ <div class="content">
+ <p class="media-meta">
+ <i18n.Translate>ID</i18n.Translate>: <b>{selected.id}</b>
+ </p>
+ <p>
+ <i18n.Translate>Description</i18n.Translate>:{" "}
+ {selected.description}
+ </p>
+ <div class="buttons is-right mt-5">
+ <button
+ class="button is-info"
+ onClick={() => onChange(undefined)}
+ >
+ clear
+ </button>
+ </div>
+ </div>
+ </div>
+ </article>
+ );
+ }
+
+ return (
+ <FormProvider<Search>
+ errors={errors}
+ object={nameForm}
+ valueHandler={setNameForm}
+ >
+ <InputWithAddon<Search>
+ name="name"
+ label={label}
+ tooltip={i18n.str`enter description or id`}
+ addonAfter={
+ <span class="icon">
+ <i class="mdi mdi-magnify" />
+ </span>
+ }
+ >
+ <div>
+ <DropdownList
+ name={nameForm.name}
+ list={list}
+ onSelect={(p) => {
+ setNameForm({ name: "" });
+ onChange(p);
+ }}
+ withImage={!!withImage}
+ />
+ </div>
+ </InputWithAddon>
+ </FormProvider>
+ );
+}
+
+interface DropdownListProps<T extends Entity> {
+ name?: string;
+ onSelect: (p: T) => void;
+ list: T[];
+ withImage: boolean;
+}
+
+function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) {
+ const { i18n } = useTranslationContext();
+ if (!name) {
+ /* FIXME
+ this BR is added to occupy the space that will be added when the
+ dropdown appears
+ */
+ return (
+ <div>
+ <br />
+ </div>
+ );
+ }
+ const filtered = list.filter(
+ (p) => p.id.includes(name) || p.description.includes(name),
+ );
+
+ return (
+ <div class="dropdown is-active">
+ <div
+ class="dropdown-menu"
+ id="dropdown-menu"
+ role="menu"
+ style={{ minWidth: "20rem" }}
+ >
+ <div class="dropdown-content">
+ {!filtered.length ? (
+ <div class="dropdown-item">
+ <i18n.Translate>
+ no match found with that description or id
+ </i18n.Translate>
+ </div>
+ ) : (
+ filtered.map((p) => (
+ <div
+ key={p.id}
+ class="dropdown-item"
+ onClick={() => onSelect(p)}
+ style={{ cursor: "pointer" }}
+ >
+ <article class="media">
+ {withImage &&
+ <div class="media-left">
+ <div class="image" style={{ minWidth: 64 }}>
+ <img
+ src={p.image ? p.image : emptyImage}
+ style={{ width: 64, height: 64 }}
+ />
+ </div>
+ </div>
+ }
+ <div class="media-content">
+ <div class="content">
+ <p>
+ <strong>{p.id}</strong> {p.extra !== undefined ? <small>{p.extra}</small> : undefined}
+ <br />
+ {p.description}
+ </p>
+ </div>
+ </div>
+ </article>
+ </div>
+ ))
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}