diff options
| author | Sebastian <sebasjm@gmail.com> | 2023-10-06 10:38:09 -0300 | 
|---|---|---|
| committer | Sebastian <sebasjm@gmail.com> | 2023-10-06 10:38:23 -0300 | 
| commit | 98013322db6b912ac50d72353dbd8b5f7fbc0435 (patch) | |
| tree | 184b99e08c69f131fe26628bbb106633d7d66013 /packages/merchant-backoffice-ui | |
| parent | 97d7be7503168f4f3bbd05905d32aa76ca1636b2 (diff) | |
backoffice ui
Diffstat (limited to 'packages/merchant-backoffice-ui')
26 files changed, 256 insertions, 232 deletions
| diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json index 66e03a65c..23b258792 100644 --- a/packages/merchant-backoffice-ui/package.json +++ b/packages/merchant-backoffice-ui/package.json @@ -1,7 +1,7 @@  {    "private": true,    "name": "@gnu-taler/merchant-backoffice-ui", -  "version": "0.1.0", +  "version": "0.9.3-dev.27",    "license": "AGPL-3.0-or-later",    "type": "module",    "scripts": { diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index f0a7de53b..4210192ae 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -64,7 +64,7 @@ function ApplicationStatusRoutes(): VNode {    const result = useBackendConfig();    const { i18n } = useTranslationContext(); -  const { currency, version } = result.ok +  const { currency, version } = result.ok && result.data      ? result.data      : { currency: "unknown", version: "unknown" };    const ctx = useMemo(() => ({ currency, version }), [currency, version]); diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx index f5372db8d..95be49c9d 100644 --- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx @@ -80,7 +80,7 @@ import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";  export enum InstancePaths {    error = "/error", -  server = "/server", +  settings = "/settings",    token = "/token",    bank_list = "/bank", @@ -118,7 +118,7 @@ export enum InstancePaths {    validators_update = "/validators/:vid/update",    validators_new = "/validators/new", -  settings = "/interface", +  interface = "/interface",  }  // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -255,7 +255,7 @@ export function InstanceRoutes({          instance={id}          admin={admin}          onShowSettings={() => { -          route(InstancePaths.settings) +          route(InstancePaths.interface)          }}          path={path}          onLogout={clearTokenAndGoToRoot} @@ -320,7 +320,7 @@ export function InstanceRoutes({           * Update instance page           */}          <Route -          path={InstancePaths.server} +          path={InstancePaths.settings}            component={InstanceUpdatePage}            onBack={() => {              route(`/`); @@ -353,7 +353,7 @@ export function InstanceRoutes({            path={InstancePaths.inventory_list}            component={ProductListPage}            onUnauthorized={LoginPageAccessDenied} -          onLoadError={ServerErrorRedirectTo(InstancePaths.server)} +          onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}            onCreate={() => {              route(InstancePaths.inventory_new);            }} @@ -392,7 +392,7 @@ export function InstanceRoutes({            path={InstancePaths.bank_list}            component={BankAccountListPage}            onUnauthorized={LoginPageAccessDenied} -          onLoadError={ServerErrorRedirectTo(InstancePaths.server)} +          onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}            onCreate={() => {              route(InstancePaths.bank_new);            }} @@ -437,7 +437,7 @@ export function InstanceRoutes({              route(InstancePaths.order_details.replace(":oid", id));            }}            onUnauthorized={LoginPageAccessDenied} -          onLoadError={ServerErrorRedirectTo(InstancePaths.server)} +          onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}            onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}          />          <Route @@ -468,7 +468,7 @@ export function InstanceRoutes({            component={TransferListPage}            onUnauthorized={LoginPageAccessDenied}            onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} -          onLoadError={ServerErrorRedirectTo(InstancePaths.server)} +          onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}            onCreate={() => {              route(InstancePaths.transfers_new);            }} @@ -491,7 +491,7 @@ export function InstanceRoutes({            component={WebhookListPage}            onUnauthorized={LoginPageAccessDenied}            onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} -          onLoadError={ServerErrorRedirectTo(InstancePaths.server)} +          onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}            onCreate={() => {              route(InstancePaths.webhooks_new);            }} @@ -530,7 +530,7 @@ export function InstanceRoutes({            component={ValidatorListPage}            onUnauthorized={LoginPageAccessDenied}            onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} -          onLoadError={ServerErrorRedirectTo(InstancePaths.server)} +          onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}            onCreate={() => {              route(InstancePaths.validators_new);            }} @@ -569,7 +569,7 @@ export function InstanceRoutes({            component={TemplateListPage}            onUnauthorized={LoginPageAccessDenied}            onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} -          onLoadError={ServerErrorRedirectTo(InstancePaths.server)} +          onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}            onCreate={() => {              route(InstancePaths.templates_new);            }} @@ -638,7 +638,7 @@ export function InstanceRoutes({            component={ReservesListPage}            onUnauthorized={LoginPageAccessDenied}            onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} -          onLoadError={ServerErrorRedirectTo(InstancePaths.server)} +          onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}            onSelect={(id: string) => {              route(InstancePaths.reserves_details.replace(":rid", id));            }} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx index 34feec202..d7b490a5d 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx @@ -107,8 +107,9 @@ export function InputWithAddon<T>({            {error && <p class="help is-danger">{error}</p>}            <span class="has-text-grey">{help}</span>          </div> -        {side} +        {expand ? <div>{side}</div> : side}        </div> +            </div>    );  } diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx new file mode 100644 index 000000000..2ff23dfd3 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx @@ -0,0 +1,59 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; + +export function JumpToElementById({ testIfExist, onSelect, palceholder, description }: { palceholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<any>, onSelect: (id: string) => void }): VNode { +  const { i18n } = useTranslationContext() + +  const [error, setError] = useState<string | undefined>( +    undefined, +  ); + +  const [id, setId] = useState<string>() +  async function check(currentId: string | undefined): Promise<void> { +    if (!currentId) { +      setError(i18n.str`missing id`); +      return; +    } +    try { +      await testIfExist(currentId); +      onSelect(currentId); +      setError(undefined); +    } catch { +      setError(i18n.str`not found`); +    } +  } + +  return <div class="level"> +    <div class="level-left"> +      <div class="level-item"> +        <div class="field has-addons"> +          <div class="control"> +            <input +              class={error ? "input is-danger" : "input"} +              type="text" +              value={id ?? ""} +              onChange={(e) => setId(e.currentTarget.value)} +              placeholder={palceholder} +            /> +            {error && <p class="help is-danger">{error}</p>} +          </div> +          <span +            class="has-tooltip-bottom" +            data-tooltip={description} +          > +            <button +              class="button" +              onClick={(e) => check(id)} +            > +              <span class="icon"> +                <i class="mdi mdi-arrow-right" /> +              </span> +            </button> +          </span> +        </div> +      </div> +    </div> +  </div> +} diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx index 402134096..6905cb4d0 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -177,12 +177,12 @@ export function Sidebar({                  </a>                </li>                <li> -                <a href={"/server"} class="has-icon"> +                <a href={"/settings"} class="has-icon">                    <span class="icon">                      <i class="mdi mdi-square-edit-outline" />                    </span>                    <span class="menu-item-label"> -                    <i18n.Translate>Server</i18n.Translate> +                    <i18n.Translate>Settings</i18n.Translate>                    </span>                  </a>                </li> diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx index b8ac2c9ab..0e881d7a5 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx @@ -24,7 +24,7 @@ import { Sidebar } from "./SideBar.js";  function getInstanceTitle(path: string, id: string): string {    switch (path) { -    case InstancePaths.server: +    case InstancePaths.settings:        return `${id}: Settings`;      case InstancePaths.order_list:        return `${id}: Orders`; @@ -64,9 +64,7 @@ function getInstanceTitle(path: string, id: string): string {        return `${id}: Templates`;      case InstancePaths.templates_use:        return `${id}: Use template`; -    case InstancePaths.settings: -      return `${id}: Interface`; -    case InstancePaths.settings: +    case InstancePaths.interface:        return `${id}: Interface`;      default:        return ""; diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts index eaeede103..4515f0557 100644 --- a/packages/merchant-backoffice-ui/src/hooks/backend.ts +++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts @@ -91,7 +91,7 @@ const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;  const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;  export function useBackendConfig(): HttpResponse< -  MerchantBackend.VersionResponse, +  MerchantBackend.VersionResponse | undefined,    RequestError<MerchantBackend.ErrorDetail>  > {    const { request } = useBackendBaseRequest(); @@ -340,6 +340,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {        if (refunded !== undefined) params.refunded = refunded;        if (wired !== undefined) params.wired = wired;        if (date_s !== undefined) params.date_s = date_s; +      if (delta === 0) { +        //in this case we can already assume the response  +        //and avoid network +        return Promise.resolve({ +          ok: true, +          data: {orders:[]} as T, +        }) +      }        return requestHandler<T>(baseUrl, endpoint, { params, token });      },      [baseUrl, token], @@ -385,6 +393,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {        const params: any = {};        if (payto_uri !== undefined) params.payto_uri = payto_uri;        if (verified !== undefined) params.verified = verified; +      if (delta === 0) { +        //in this case we can already assume the response  +        //and avoid network +        return Promise.resolve({ +          ok: true, +          data: {transfers:[]} as T, +        }) +      }        if (delta !== undefined) {          params.limit = delta;        } @@ -403,6 +419,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {      ): Promise<HttpResponseOk<T>> {        const [endpoint, position, delta] = args        const params: any = {}; +      if (delta === 0) { +        //in this case we can already assume the response  +        //and avoid network +        return Promise.resolve({ +          ok: true, +          data: {templates:[]} as T, +        }) +      }        if (delta !== undefined) {          params.limit = delta;        } @@ -421,6 +445,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {      ): Promise<HttpResponseOk<T>> {        const [endpoint, position, delta] = args        const params: any = {}; +      if (delta === 0) { +        //in this case we can already assume the response  +        //and avoid network +        return Promise.resolve({ +          ok: true, +          data: {webhooks:[]} as T, +        }) +      }        if (delta !== undefined) {          params.limit = delta;        } diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts index 498e4eb78..3c8ef15ed 100644 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ b/packages/merchant-backoffice-ui/src/hooks/index.ts @@ -32,7 +32,13 @@ const calculateRootPath = () => {        ? window.location.origin + window.location.pathname        : "/"; -  return rootPath.replace("webui/",""); +  /** +   * By default, merchant backend serves the html content +   * from the /webui root. This should cover most of the  +   * cases and the rootPath will be the merchant backend +   * URL where the instances are +   */ +  return rootPath.replace("/webui/", "");  };  const loginTokenCodec = buildCodecForObject<LoginToken>() diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts index 56cdd3046..ee8728cc8 100644 --- a/packages/merchant-backoffice-ui/src/hooks/templates.ts +++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts @@ -82,10 +82,19 @@ export function useTemplateAPI(): TemplateAPI {      return res;    }; +  const testTemplateExist = async ( +    templateId: string, +  ): Promise<HttpResponseOk<void>> => { +    const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", }); +    return res; +  }; + +    return {      createTemplate,      updateTemplate,      deleteTemplate, +    testTemplateExist,      createOrderFromTemplate,    };  } @@ -98,6 +107,9 @@ export interface TemplateAPI {      id: string,      data: MerchantBackend.Template.TemplatePatchDetails,    ) => Promise<HttpResponseOk<void>>; +  testTemplateExist: ( +    id: string +  ) => Promise<HttpResponseOk<void>>;    deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>;    createOrderFromTemplate: (      id: string, @@ -119,11 +131,11 @@ export function useInstanceTemplates(  > {    const { templateFetcher } = useBackendInstanceRequest(); -  // const [pageBefore, setPageBefore] = useState(1); +  const [pageBefore, setPageBefore] = useState(1);    const [pageAfter, setPageAfter] = useState(1);    const totalAfter = pageAfter * PAGE_SIZE; -  // const totalBefore = args?.position !== undefined ? pageBefore * PAGE_SIZE : 0; +  const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0;    /**     * FIXME: this can be cleaned up a little @@ -131,20 +143,20 @@ export function useInstanceTemplates(     * the logic of double query should be inside the orderFetch so from the hook perspective and cache     * is just one query and one error status     */ -  // const { -  //   data: beforeData, -  //   error: beforeError, -  //   isValidating: loadingBefore, -  // } = useSWR<HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, HttpError>( -  //   [ -  //     `/private/templates`, -  //     token, -  //     url, -  //     args?.position, -  //     totalBefore, -  //   ], -  //   templateFetcher, -  // ); +  const { +    data: beforeData, +    error: beforeError, +    isValidating: loadingBefore, +  } = useSWR< +    HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, +    RequestError<MerchantBackend.ErrorDetail>>( +      [ +        `/private/templates`, +        args?.position, +        totalBefore, +      ], +      templateFetcher, +    );    const {      data: afterData,      error: afterError, @@ -155,9 +167,13 @@ export function useInstanceTemplates(    >([`/private/templates`, args?.position, -totalAfter], templateFetcher);    //this will save last result -  // const [lastBefore, setLastBefore] = useState< -  //   HttpResponse<MerchantBackend.Template.TemplateSummaryResponse, MerchantBackend.ErrorDetail> -  // >({ loading: true }); +  const [lastBefore, setLastBefore] = useState< +    HttpResponse< +      MerchantBackend.Template.TemplateSummaryResponse, +      MerchantBackend.ErrorDetail +    > +  >({ loading: true }); +    const [lastAfter, setLastAfter] = useState<      HttpResponse<        MerchantBackend.Template.TemplateSummaryResponse, @@ -166,19 +182,18 @@ export function useInstanceTemplates(    >({ loading: true });    useEffect(() => {      if (afterData) setLastAfter(afterData); -    // if (beforeData) setLastBefore(beforeData); -  }, [afterData /*, beforeData*/]); +    if (beforeData) setLastBefore(beforeData); +  }, [afterData, beforeData]); -  // if (beforeError) return beforeError; +  if (beforeError) return beforeError.cause;    if (afterError) return afterError.cause;    // if the query returns less that we ask, then we have reach the end or beginning    const isReachingEnd =      afterData && afterData.data.templates.length < totalAfter; -  const isReachingStart = false; -  // args?.position === undefined -  // || -  // (beforeData && beforeData.data.templates.length < totalBefore); +  const isReachingStart = args?.position === undefined +    || +    (beforeData && beforeData.data.templates.length < totalBefore);    const pagination = {      isReachingEnd, @@ -188,37 +203,36 @@ export function useInstanceTemplates(        if (afterData.data.templates.length < MAX_RESULT_SIZE) {          setPageAfter(pageAfter + 1);        } else { -        const from = `${ -          afterData.data.templates[afterData.data.templates.length - 1] -            .template_id -        }`; +        const from = `${afterData.data.templates[afterData.data.templates.length - 1] +          .template_id +          }`;          if (from && updatePosition) updatePosition(from);        }      },      loadMorePrev: () => { -      // if (!beforeData || isReachingStart) return; -      // if (beforeData.data.templates.length < MAX_RESULT_SIZE) { -      //   setPageBefore(pageBefore + 1); -      // } else if (beforeData) { -      //   const from = `${beforeData.data.templates[beforeData.data.templates.length - 1] -      //     .template_id -      //     }`; -      //   if (from && updatePosition) updatePosition(from); -      // } +      if (!beforeData || isReachingStart) return; +      if (beforeData.data.templates.length < MAX_RESULT_SIZE) { +        setPageBefore(pageBefore + 1); +      } else if (beforeData) { +        const from = `${beforeData.data.templates[beforeData.data.templates.length - 1] +          .template_id +          }`; +        if (from && updatePosition) updatePosition(from); +      }      },    }; -  const templates = !afterData ? [] : (afterData || lastAfter).data.templates; -  // const templates = -  //   !beforeData || !afterData -  //     ? [] -  //     : (beforeData || lastBefore).data.templates -  //       .slice() -  //       .reverse() -  //       .concat((afterData || lastAfter).data.templates); -  if (loadingAfter /* || loadingBefore */) +  // const templates = !afterData ? [] : (afterData || lastAfter).data.templates; +  const templates = +    !beforeData || !afterData +      ? [] +      : (beforeData || lastBefore).data.templates +        .slice() +        .reverse() +        .concat((afterData || lastAfter).data.templates); +  if (loadingAfter || loadingBefore)      return { loading: true, data: { templates } }; -  if (/*beforeData &&*/ afterData) { +  if (beforeData && afterData) {      return { ok: true, data: { templates }, ...pagination };    }    return { loading: true }; diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts index c91fff8b4..ad6bf96e2 100644 --- a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts +++ b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts @@ -93,9 +93,11 @@ export function useInstanceWebhooks(  > {    const { webhookFetcher } = useBackendInstanceRequest(); +  const [pageBefore, setPageBefore] = useState(1);    const [pageAfter, setPageAfter] = useState(1);    const totalAfter = pageAfter * PAGE_SIZE; +  const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0;    const {      data: afterData, @@ -120,7 +122,7 @@ export function useInstanceWebhooks(    const isReachingEnd =      afterData && afterData.data.webhooks.length < totalAfter; -  const isReachingStart = false; +  const isReachingStart = true;    const pagination = {      isReachingEnd, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx index c29a6fa6e..9f80719a1 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx @@ -21,7 +21,7 @@  import { useTranslationContext } from "@gnu-taler/web-util/browser";  import { format } from "date-fns"; -import { h, VNode } from "preact"; +import { h, VNode, Fragment } from "preact";  import { useState } from "preact/hooks";  import { DatePicker } from "../../../../components/picker/DatePicker.js";  import { MerchantBackend, WithId } from "../../../../declaration.js"; @@ -29,8 +29,6 @@ import { CardTable } from "./Table.js";  import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";  export interface ListPageProps { -  errorOrderId: string | undefined; -    onShowAll: () => void;    onShowNotPaid: () => void;    onShowPaid: () => void; @@ -56,17 +54,18 @@ export interface ListPageProps {    onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;    onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; -  onSearchOrderById: (id: string) => void;    onCreate: () => void;  }  export function ListPage({ +  hasMoreAfter, +  hasMoreBefore, +  onLoadMoreAfter, +  onLoadMoreBefore,    orders, -  errorOrderId,    isAllActive,    onSelectOrder,    onRefundOrder, -  onSearchOrderById,    jumpToDate,    onCopyURL,    onShowAll, @@ -86,42 +85,10 @@ export function ListPage({    const { i18n } = useTranslationContext();    const dateTooltip = i18n.str`select date to show nearby orders`;    const [pickDate, setPickDate] = useState(false); -  const [orderId, setOrderId] = useState<string>("");    const [settings] = useSettings();    return ( -    <section class="section is-main-section"> -      <div class="level"> -        <div class="level-left"> -          <div class="level-item"> -            <div class="field has-addons"> -              <div class="control"> -                <input -                  class={errorOrderId ? "input is-danger" : "input"} -                  type="text" -                  value={orderId} -                  onChange={(e) => setOrderId(e.currentTarget.value)} -                  placeholder={i18n.str`order id`} -                /> -                {errorOrderId && <p class="help is-danger">{errorOrderId}</p>} -              </div> -              <span -                class="has-tooltip-bottom" -                data-tooltip={i18n.str`jump to order with the given order ID`} -              > -                <button -                  class="button" -                  onClick={(e) => onSearchOrderById(orderId)} -                > -                  <span class="icon"> -                    <i class="mdi mdi-arrow-right" /> -                  </span> -                </button> -              </span> -            </div> -          </div> -        </div> -      </div> +    <Fragment>        <div class="columns">          <div class="column is-two-thirds">            <div class="tabs" style={{ overflow: "inherit" }}> @@ -249,7 +216,11 @@ export function ListPage({          onCopyURL={onCopyURL}          onSelect={onSelectOrder}          onRefund={onRefundOrder} +        hasMoreAfter={hasMoreAfter} +        hasMoreBefore={hasMoreBefore} +        onLoadMoreAfter={onLoadMoreAfter} +        onLoadMoreBefore={onLoadMoreBefore}        /> -    </section> +    </Fragment>    );  } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx index 608c9b20d..b2806bb79 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -140,10 +140,9 @@ function Table({    const [settings] = useSettings();    return (      <div class="table-container"> -      {onLoadMoreBefore && ( +      {hasMoreBefore && (          <button            class="button is-fullwidth" -          disabled={!hasMoreBefore}            onClick={onLoadMoreBefore}          >            <i18n.Translate>load newer orders</i18n.Translate> @@ -218,10 +217,9 @@ function Table({            })}          </tbody>        </table> -      {onLoadMoreAfter && ( +      {hasMoreAfter && (          <button            class="button is-fullwidth" -          disabled={!hasMoreAfter}            onClick={onLoadMoreAfter}          >            <i18n.Translate>load older orders</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx index 48f77e3d3..34c7d348a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -39,6 +39,7 @@ import { Notification } from "../../../../utils/types.js";  import { ListPage } from "./ListPage.js";  import { RefundModal } from "./Table.js";  import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";  interface Props {    onUnauthorized: () => VNode; @@ -69,9 +70,6 @@ export default function OrderList({    const [notif, setNotif] = useState<Notification | undefined>(undefined);    const { i18n } = useTranslationContext(); -  const [errorOrderId, setErrorOrderId] = useState<string | undefined>( -    undefined, -  );    if (result.loading) return <Loading />;    if (!result.ok) { @@ -100,24 +98,17 @@ export default function OrderList({        ? "is-active"        : ""; -  async function testIfOrderExistAndSelect(orderId: string): Promise<void> { -    if (!orderId) { -      setErrorOrderId(i18n.str`Enter an order id`); -      return; -    } -    try { -      await getPaymentURL(orderId); -      onSelect(orderId); -      setErrorOrderId(undefined); -    } catch { -      setErrorOrderId(i18n.str`order not found`); -    } -  } -    return ( -    <Fragment> +    <section class="section is-main-section">        <NotificationCard notification={notif} /> +      <JumpToElementById +        testIfExist={getPaymentURL}  +        onSelect={onSelect} +        description={i18n.str`jump to order with the given product ID`} +        palceholder={i18n.str`order id`} +      /> +        <ListPage          orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))}          onLoadMoreBefore={result.loadMorePrev} @@ -126,7 +117,6 @@ export default function OrderList({          hasMoreAfter={!result.isReachingEnd}          onSelectOrder={(order) => onSelect(order.id)}          onRefundOrder={(value) => setOrderToBeRefunded(value)} -        errorOrderId={errorOrderId}          isAllActive={isAllActive}          isNotWiredActive={isNotWiredActive}          isWiredActive={isWiredActive} @@ -138,7 +128,6 @@ export default function OrderList({            getPaymentURL(id).then((resp) => copyToClipboard(resp.data))          }          onCreate={onCreate} -        onSearchOrderById={testIfOrderExistAndSelect}          onSelectDate={setNewDate}          onShowAll={() => setFilter({})}          onShowNotPaid={() => setFilter({ paid: "no" })} @@ -190,7 +179,7 @@ export default function OrderList({            }}          />        )} -    </Fragment> +    </section>    );  } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx index db73217ed..275f855cb 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -244,7 +244,10 @@ function Table({                      }                      style={{ cursor: "pointer" }}                    > +                    <span style={{"whiteSpace":"nowrap"}}> +                      {i.total_sold} {i.unit} +                    </span>                    </td>                    <td class="is-actions-cell right-sticky">                      <div class="buttons is-right"> @@ -341,7 +344,7 @@ function FastProductWithInfiniteStockUpdateForm({          <div class="buttons mt-5"> -          <button class="button " onClick={onCancel}> +          <button class="button mt-5" onClick={onCancel}>              <i18n.Translate>Clone</i18n.Translate>            </button>          </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx index 274a7c2ea..942b5d0ac 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -37,6 +37,7 @@ import { Notification } from "../../../../utils/types.js";  import { CardTable } from "./Table.js";  import { HttpStatusCode } from "@gnu-taler/taler-util";  import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";  interface Props {    onUnauthorized: () => VNode; @@ -74,60 +75,17 @@ export default function ProductList({        return onNotFound();      return onLoadError(result);    } -  const [errorId, setErrorId] = useState<string | undefined>( -    undefined, -  ); - -  const [productId, setProductId] = useState<string>() -  async function testIfProductExistAndSelect(orderId: string | undefined): Promise<void> { -    if (!orderId) { -      setErrorId(i18n.str`Enter a product id`); -      return; -    } -    try { -      await getProduct(orderId); -      onSelect(orderId); -      setErrorId(undefined); -    } catch { -      setErrorId(i18n.str`product not found`); -    } -  }    return (      <section class="section is-main-section">        <NotificationCard notification={notif} /> -      <div class="level"> -        <div class="level-left"> -          <div class="level-item"> -            <div class="field has-addons"> -              <div class="control"> -                <input -                  class={errorId ? "input is-danger" : "input"} -                  type="text" -                  value={productId ?? ""} -                  onChange={(e) => setProductId(e.currentTarget.value)} -                  placeholder={i18n.str`product id`} -                /> -                {errorId && <p class="help is-danger">{errorId}</p>} -              </div> -              <span -                class="has-tooltip-bottom" -                data-tooltip={i18n.str`jump to product with the given product ID`} -              > -                <button -                  class="button" -                  onClick={(e) => testIfProductExistAndSelect(productId)} -                > -                  <span class="icon"> -                    <i class="mdi mdi-arrow-right" /> -                  </span> -                </button> -              </span> -            </div> -          </div> -        </div> -      </div> +      <JumpToElementById +        testIfExist={getProduct}  +        onSelect={onSelect} +        description={i18n.str`jump to product with the given product ID`} +        palceholder={i18n.str`product id`} +      />        <CardTable          instances={result.data} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx index ab1d5ef91..bf6062c34 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx @@ -49,7 +49,6 @@ export function ListPage({    const { i18n } = useTranslationContext();    return ( -    <section class="section is-main-section">        <CardTable          templates={templates.map((o) => ({            ...o, @@ -65,6 +64,5 @@ export function ListPage({          onLoadMoreAfter={onLoadMoreAfter}          hasMoreAfter={!onLoadMoreAfter}        /> -    </section>    );  } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx index 3bea9abe8..9fdf4ead9 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx @@ -136,11 +136,10 @@ function Table({    const { i18n } = useTranslationContext();    return (      <div class="table-container"> -      {onLoadMoreBefore && ( +      {hasMoreBefore && (          <button            class="button is-fullwidth"            data-tooltip={i18n.str`load more templates before the first one`} -          disabled={!hasMoreBefore}            onClick={onLoadMoreBefore}          >            <i18n.Translate>load newer templates</i18n.Translate> @@ -188,7 +187,7 @@ function Table({                        data-tooltip={i18n.str`use template to create new order`}                        onClick={() => onNewOrder(i)}                      > -                      New order +                      Use template                      </button>                      <button                        class="button is-info is-small has-tooltip-left" @@ -204,11 +203,10 @@ function Table({            })}          </tbody>        </table> -      {onLoadMoreAfter && ( +      {hasMoreAfter && (          <button            class="button is-fullwidth"            data-tooltip={i18n.str`load more templates after the last one`} -          disabled={!hasMoreAfter}            onClick={onLoadMoreAfter}          >            <i18n.Translate>load older templates</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx index 3c9bb231c..b9767442f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -35,8 +35,9 @@ import {  } from "../../../../hooks/templates.js";  import { Notification } from "../../../../utils/types.js";  import { ListPage } from "./ListPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";  import { ConfirmModal } from "../../../../components/modal/index.js"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";  interface Props {    onUnauthorized: () => VNode; @@ -60,7 +61,7 @@ export default function ListTemplates({    const [position, setPosition] = useState<string | undefined>(undefined);    const { i18n } = useTranslationContext();    const [notif, setNotif] = useState<Notification | undefined>(undefined); -  const { deleteTemplate } = useTemplateAPI(); +  const { deleteTemplate, testTemplateExist } = useTemplateAPI();    const result = useInstanceTemplates({ position }, (id) => setPosition(id));    const [deleting, setDeleting] =      useState<MerchantBackend.Template.TemplateEntry | null>(null); @@ -81,9 +82,16 @@ export default function ListTemplates({    }    return ( -    <Fragment> +    <section class="section is-main-section">        <NotificationCard notification={notif} /> +      <JumpToElementById +        testIfExist={testTemplateExist}  +        onSelect={onSelect} +        description={i18n.str`jump to template with the given template ID`} +        palceholder={i18n.str`template id`} +      /> +        <ListPage          templates={result.data.templates}          onLoadMoreBefore={ @@ -139,6 +147,6 @@ export default function ListTemplates({            </p>          </ConfirmModal>        )} -    </Fragment> +    </section>    );  } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx index db2533ecc..02b12c4c2 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx @@ -73,7 +73,7 @@ export function ListPage({            >              <InputSelector                name="payto_uri" -              label={i18n.str`Address`} +              label={i18n.str`Account URI`}                values={accounts}                placeholder={i18n.str`Select one account`}                tooltip={i18n.str`filter by account address`} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx index 1c464cbc7..b6b1cf328 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -125,11 +125,10 @@ function Table({    const [settings] = useSettings();    return (      <div class="table-container"> -      {onLoadMoreBefore && ( +      {hasMoreBefore && (          <button            class="button is-fullwidth"            data-tooltip={i18n.str`load more transfers before the first one`} -          disabled={!hasMoreBefore}            onClick={onLoadMoreBefore}          >            <i18n.Translate>load newer transfers</i18n.Translate> @@ -198,11 +197,10 @@ function Table({            })}          </tbody>        </table> -      {onLoadMoreAfter && ( +      {hasMoreAfter && (          <button            class="button is-fullwidth"            data-tooltip={i18n.str`load more transfer after the last one`} -          disabled={!hasMoreAfter}            onClick={onLoadMoreAfter}          >            <i18n.Translate>load older transfers</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx index 1bc1673ba..0fdbb9bc3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -21,7 +21,7 @@  import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";  import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks";  import { Loading } from "../../../../components/exception/loading.js";  import { MerchantBackend } from "../../../../declaration.js";  import { useInstanceDetails } from "../../../../hooks/instance.js"; @@ -47,7 +47,6 @@ export default function ListTransfer({    onCreate,    onNotFound,  }: Props): VNode { -  const [form, setForm] = useState<Form>({ payto_uri: "" });    const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s });    const [position, setPosition] = useState<string | undefined>(undefined); @@ -56,6 +55,14 @@ export default function ListTransfer({    const accounts = !instance.ok      ? []      : instance.data.accounts.map((a) => a.payto_uri); +  const [form, setForm] = useState<Form>({ payto_uri: "" }); + +  const shoulUseDefaultAccount = accounts.length === 1 +  useEffect(() => { +    if (shoulUseDefaultAccount) { +      setForm({...form, payto_uri: accounts[0]}) +    } +  }, [shoulUseDefaultAccount])    const isVerifiedTransfers = form.verified === "yes";    const isNonVerifiedTransfers = form.verified === "no"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx index cebc1ade6..4e813d777 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx @@ -118,6 +118,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {                {state.otp_algorithm && state.otp_algorithm > 0 ? (                  <Fragment>                    <InputWithAddon<Entity> +                    expand                      name="otp_key"                      label={i18n.str`Device key`}                      inputType={showKey ? "text" : "password"} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx index b639a6134..0c28027fe 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx @@ -126,11 +126,10 @@ function Table({    const { i18n } = useTranslationContext();    return (      <div class="table-container"> -      {onLoadMoreBefore && ( +      {hasMoreBefore && (          <button            class="button is-fullwidth"            data-tooltip={i18n.str`load more devices before the first one`} -          disabled={!hasMoreBefore}            onClick={onLoadMoreBefore}          >            <i18n.Translate>load newer devices</i18n.Translate> @@ -180,11 +179,10 @@ function Table({            })}          </tbody>        </table> -      {onLoadMoreAfter && ( +      {hasMoreAfter && (          <button            class="button is-fullwidth"            data-tooltip={i18n.str`load more devices after the last one`} -          disabled={!hasMoreAfter}            onClick={onLoadMoreAfter}          >            <i18n.Translate>load older devices</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx index 124ced1f1..42a179d2c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx @@ -126,11 +126,10 @@ function Table({    const { i18n } = useTranslationContext();    return (      <div class="table-container"> -      {onLoadMoreBefore && ( +      {hasMoreBefore && (          <button            class="button is-fullwidth"            data-tooltip={i18n.str`load more webhooks before the first one`} -          disabled={!hasMoreBefore}            onClick={onLoadMoreBefore}          >            <i18n.Translate>load newer webhooks</i18n.Translate> @@ -187,11 +186,10 @@ function Table({            })}          </tbody>        </table> -      {onLoadMoreAfter && ( +      {hasMoreAfter && (          <button            class="button is-fullwidth"            data-tooltip={i18n.str`load more webhooks after the last one`} -          disabled={!hasMoreAfter}            onClick={onLoadMoreAfter}          >            <i18n.Translate>load older webhooks</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx index a9e3c3a1b..e37ef4bef 100644 --- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -26,31 +26,15 @@ import { useBackendContext } from "../../context/backend.js";  import { useInstanceContext } from "../../context/instance.js";  import { AccessToken, LoginToken } from "../../declaration.js";  import { useCredentialsChecker } from "../../hooks/backend.js"; -import { useBackendURL } from "../../hooks/index.js";  interface Props {    onConfirm: (token: LoginToken | undefined) => void;  } -function getTokenValuePart(t: string): string { -  if (!t) return t; -  const match = /secret-token:(.*)/.exec(t); -  if (!match || !match[1]) return ""; -  return match[1]; -} -  function normalizeToken(r: string): AccessToken {    return `secret-token:${r}` as AccessToken;  } -function cleanUp(s: string): string { -  let result = s; -  if (result.indexOf("webui/") !== -1) { -    result = result.substring(0, result.indexOf("webui/")); -  } -  return result; -} -  export function LoginPage({ onConfirm }: Props): VNode {    const { url: backendURL, changeBackend, resetBackend } = useBackendContext();    const { admin, id } = useInstanceContext(); @@ -245,11 +229,14 @@ function AsyncButton({ onClick, disabled, type = "", children }: { type?: string  export function ConnectionPage({ onConfirm }: { onConfirm: (s: string) => void }): VNode {    const { url: backendURL } = useBackendContext() -  const [url, setURL] = useState(cleanUp(backendURL)); +  const [error, setError] = useState<string>(); +  const [url, setURL] = useState(backendURL ?? "");    const { i18n } = useTranslationContext();    async function doConnect() { -    onConfirm(url) +    const withHttp = url.startsWith("http") ? url : "https://" + url +     +    onConfirm(withHttp)    }    return ( | 
