gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] branch master updated: some fixes


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated: some fixes
Date: Tue, 27 Apr 2021 14:58:55 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository merchant-backoffice.

The following commit(s) were added to refs/heads/master by this push:
     new c9b0e41  some fixes
c9b0e41 is described below

commit c9b0e41116d3347c844ac00bda401e84d67ffd4d
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Apr 27 09:57:09 2021 -0300

    some fixes
---
 CHANGELOG.md                                       |   1 +
 packages/frontend/.gitignore                       |   1 +
 packages/frontend/.storybook/main.js               |   9 +-
 packages/frontend/.storybook/preview.js            |   1 +
 packages/frontend/package.json                     |  12 +-
 packages/frontend/src/InstanceRoutes.tsx           |  60 ++--
 .../src/components/form/InputSearchProduct.tsx     |  34 ++-
 .../src/components/form/InputSecured.stories.tsx   |  10 +-
 .../frontend/src/components/form/InputStock.tsx    |  16 +-
 .../src/components/product/ProductForm.tsx         |   9 +-
 .../src/components/product/ProductList.tsx         |   9 +-
 packages/frontend/src/hooks/backend.ts             |  36 ++-
 packages/frontend/src/messages/en.po               |   3 +
 .../paths/instance/orders/create/CreatePage.tsx    |   6 +-
 .../orders/create/InventoryProductForm.tsx         |  39 +--
 .../orders/create/NonInventoryProductForm.tsx      |  88 +++++-
 .../src/paths/instance/orders/create/index.tsx     |   8 +-
 .../paths/instance/orders/details/DetailPage.tsx   |  30 +-
 .../src/paths/instance/orders/details/Timeline.tsx |  27 +-
 .../paths/instance/products/create/CreatePage.tsx  |   2 +-
 .../src/paths/instance/products/create/index.tsx   |  12 +-
 .../src/paths/instance/products/list/Table.tsx     |  40 ++-
 .../frontend/src/paths/instance/update/index.tsx   |   2 +-
 packages/frontend/src/schemas/index.ts             |  11 +-
 .../frontend/tests/__mocks__/fileTransformer.js    |   9 +
 packages/frontend/tests/stories.test.tsx           |  37 +++
 pnpm-lock.yaml                                     | 323 ++++++++++++++++++---
 27 files changed, 642 insertions(+), 193 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d7b60d..b2df239 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ and this project adheres to [Semantic 
Versioning](https://semver.org/spec/v2.0.0
  - product detail: we could have some button that brings us to the detailed 
screen for the product
  - navigation to another instance should not do full refresh
  - cleanup instance and token management, because code is a mess and can be 
refactored 
+
 ## [Unreleased]
  - fixed bug when updating token and not admin
  - showing a yellow bar on non-default instance navigation (admin)
diff --git a/packages/frontend/.gitignore b/packages/frontend/.gitignore
index 71f7c3d..df14910 100644
--- a/packages/frontend/.gitignore
+++ b/packages/frontend/.gitignore
@@ -3,3 +3,4 @@
 /storybook-static
 /docs
 /single
+/coverage
diff --git a/packages/frontend/.storybook/main.js 
b/packages/frontend/.storybook/main.js
index 7dc5cc2..6e3ec15 100644
--- a/packages/frontend/.storybook/main.js
+++ b/packages/frontend/.storybook/main.js
@@ -14,10 +14,10 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
 
 
 module.exports = {
@@ -34,7 +34,6 @@ module.exports = {
     // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
     // You can change the configuration based on that.
     // 'PRODUCTION' is used when building the static version of storybook.
-
     // Make whatever fine-grained changes you need
     config.module.rules.push({
       test: [/\.pot?$/, /\.mo$/],
diff --git a/packages/frontend/.storybook/preview.js 
b/packages/frontend/.storybook/preview.js
index 012e9f8..7330bbe 100644
--- a/packages/frontend/.storybook/preview.js
+++ b/packages/frontend/.storybook/preview.js
@@ -47,6 +47,7 @@ export const globalTypes = {
 
 export const decorators = [
   (Story, { globals }) => {
+    
     return <MessageProvider locale={globals.locale} onError="warn" 
messages={messages[globals.locale]} pathSep={null} onError={() => null}>
       <Story />
     </MessageProvider>
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 34eff24..02e598b 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -94,6 +94,7 @@
     "messageformat-po-loader": "^0.3.0",
     "node-sass": "^5.0.0",
     "preact-cli": "^3.0.5",
+    "preact-render-to-json": "^3.6.6",
     "preact-render-to-string": "^5.1.16",
     "rimraf": "^3.0.2",
     "sass-loader": "10.1.1",
@@ -107,6 +108,13 @@
     "setupFiles": [
       "<rootDir>/tests/__mocks__/browserMocks.ts",
       "<rootDir>/tests/__mocks__/setupTests.ts"
-    ]
+    ],
+    "collectCoverage": true,
+    "moduleNameMapper": {
+      "\\.(css|less)$": "identity-obj-proxy"
+    },
+    "transform": {
+      
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|po)$":
 "<rootDir>/tests/__mocks__/fileTransformer.js"
+    }
   }
-}
+}
\ No newline at end of file
diff --git a/packages/frontend/src/InstanceRoutes.tsx 
b/packages/frontend/src/InstanceRoutes.tsx
index eabf463..aa97914 100644
--- a/packages/frontend/src/InstanceRoutes.tsx
+++ b/packages/frontend/src/InstanceRoutes.tsx
@@ -23,7 +23,7 @@ import { createHashHistory } from 'history';
 import { Fragment, FunctionComponent, h, VNode } from 'preact';
 import { useMessageTemplate } from 'preact-messages';
 import { Route, route, Router } from 'preact-router';
-import { useCallback, useEffect, useMemo } from "preact/hooks";
+import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
 import { Loading } from './components/exception/loading';
 import { NotificationCard } from './components/menu';
 import { InstanceContextProvider, useBackendContext } from './context/backend';
@@ -41,10 +41,11 @@ import TransferListPage from 
'./paths/instance/transfers/list';
 import InstanceUpdatePage, { Props as InstanceUpdatePageProps } from 
"./paths/instance/update";
 import LoginPage from './paths/login';
 import NotFoundPage from './paths/notfound';
-
+import { Notification } from './utils/types';
 
 export enum InstancePaths {
   // details = '/',
+  error = '/error',
   update = '/update',
 
   product_list = '/products',
@@ -64,7 +65,7 @@ export enum InstancePaths {
 }
 
 // eslint-disable-next-line @typescript-eslint/no-empty-function
-const noop = () => {}
+const noop = () => { }
 
 export enum AdminPaths {
   list_instances = '/instances',
@@ -83,6 +84,7 @@ export function InstanceRoutes({ id, admin }: Props): VNode {
   const { changeBackend, addTokenCleaner } = useBackendContext();
   const cleaner = useCallback(() => { updateToken(undefined); }, [id]);
   const i18n = useMessageTemplate('');
+  const [globalNotification, setGlobalNotification] = useState<Notification & 
{to:string} | undefined>(undefined)
 
   useEffect(() => {
     addTokenCleaner(cleaner);
@@ -100,10 +102,15 @@ export function InstanceRoutes({ id, admin }: Props): 
VNode {
 
   const value = useMemo(() => ({ id, token, admin }), [id, token, admin])
 
-  const LoginPageServerError = (error: HttpError) => <Fragment>
-    <NotificationCard notification={{ message: `Server reported a problem: 
HTTP status #${error.status}`, description: `Got message: ${error.message} 
from: ${error.info?.url}`, type: 'ERROR' }} />
-    <LoginPage onConfirm={updateLoginStatus} />
-  </Fragment>
+  const ServerErrorRedirectTo = (to: InstancePaths | AdminPaths) => (error: 
HttpError) => {
+    setGlobalNotification({
+      message: `HTTP status #${error.status}: Server reported a problem`,
+      description: `Got message: "${error.message}" from: ${error.info?.url}`,
+      type: 'ERROR',
+      to
+    })
+    return <Redirect to={to} />
+  }
 
   const LoginPageAccessDenied = () => <Fragment>
     <NotificationCard notification={{ message: i18n`Access denied`, 
description: i18n`Check your token is valid`, type: 'ERROR', }} />
@@ -126,14 +133,23 @@ export function InstanceRoutes({ id, admin }: Props): 
VNode {
       }
       if (props) {
         return <Next {...props} />
-      } 
-        return <Next />
-      
+      }
+      return <Next />
+
     }
   }
 
   return <InstanceContextProvider value={value}>
-    <Router history={createHashHistory()}>
+
+    <NotificationCard notification={globalNotification} />
+
+    <Router history={createHashHistory()} onChange={(e) => {
+      const movingOutFromNotification = globalNotification && e.url !== 
globalNotification.to
+      if (movingOutFromNotification) {
+        setGlobalNotification(undefined)
+      }
+    }} >
+
       <Route path="/" component={Redirect} to={InstancePaths.order_list} />
 
       {/**
@@ -144,7 +160,7 @@ export function InstanceRoutes({ id, admin }: Props): VNode 
{
           onCreate={() => { route(AdminPaths.new_instance) }}
           onUpdate={(id: string): void => { route(`/instance/${id}/update`); }}
           onUnauthorized={LoginPageAccessDenied}
-          onLoadError={LoginPageServerError}
+          onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
         />
       }
 
@@ -159,7 +175,8 @@ export function InstanceRoutes({ id, admin }: Props): VNode 
{
         <Route path={AdminPaths.update_instance} 
component={AdminInstanceUpdatePage}
           onBack={() => route(AdminPaths.list_instances)}
           onConfirm={() => { route(AdminPaths.list_instances); }}
-          onUpdateError={noop}
+          onUpdateError={ServerErrorRedirectTo(AdminPaths.list_instances)}
+          onLoadError={ServerErrorRedirectTo(AdminPaths.list_instances)}
           onNotFound={NotFoundPage}
         />
       }
@@ -173,7 +190,7 @@ export function InstanceRoutes({ id, admin }: Props): VNode 
{
         onUpdateError={noop}
         onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
         onUnauthorized={LoginPageAccessDenied}
-        onLoadError={LoginPageServerError}
+        onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
       />
 
       {/**
@@ -181,19 +198,22 @@ export function InstanceRoutes({ id, admin }: Props): 
VNode {
        */}
       <Route path={InstancePaths.product_list} component={ProductListPage}
         onUnauthorized={LoginPageAccessDenied}
-        onLoadError={LoginPageServerError}
+        onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
         onCreate={() => { route(InstancePaths.product_new) }}
         onSelect={(id: string) => { 
route(InstancePaths.product_update.replace(':pid', id)) }}
         onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
       />
       <Route path={InstancePaths.product_update} component={ProductUpdatePage}
         onUnauthorized={LoginPageAccessDenied}
-        onLoadError={LoginPageServerError}
+        onLoadError={ServerErrorRedirectTo(InstancePaths.product_list)}
         onConfirm={() => { route(InstancePaths.product_list); }}
         onBack={() => { route(InstancePaths.product_list); }}
         onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
       />
-      <Route path={InstancePaths.product_new} component={ProductCreatePage}
+      <Route path={InstancePaths.product_new} 
+        component={ProductCreatePage}
+        onConfirm={() => { route(InstancePaths.product_list); }}
+        onBack={() => { route(InstancePaths.product_list); }}
       />
 
       {/**
@@ -203,12 +223,12 @@ export function InstanceRoutes({ id, admin }: Props): 
VNode {
         onCreate={() => { route(InstancePaths.order_new) }}
         onSelect={(id: string) => { 
route(InstancePaths.order_details.replace(':oid', id)) }}
         onUnauthorized={LoginPageAccessDenied}
-        onLoadError={LoginPageServerError}
+        onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
         onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
       />
       <Route path={InstancePaths.order_details} component={OrderDetailsPage}
         onUnauthorized={LoginPageAccessDenied}
-        onLoadError={LoginPageServerError}
+        onLoadError={ServerErrorRedirectTo(InstancePaths.order_list)}
         onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
         onBack={() => { route(InstancePaths.order_list) }}
       />
@@ -222,7 +242,7 @@ export function InstanceRoutes({ id, admin }: Props): VNode 
{
        */}
       <Route path={InstancePaths.transfers_list} component={TransferListPage}
         onUnauthorized={LoginPageAccessDenied}
-        onLoadError={LoginPageServerError}
+        onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
         onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
       />
 
diff --git a/packages/frontend/src/components/form/InputSearchProduct.tsx 
b/packages/frontend/src/components/form/InputSearchProduct.tsx
index 1bdc94f..3dbbcf7 100644
--- a/packages/frontend/src/components/form/InputSearchProduct.tsx
+++ b/packages/frontend/src/components/form/InputSearchProduct.tsx
@@ -24,6 +24,7 @@ import { InputWithAddon } from "./InputWithAddon";
 import { FormErrors, FormProvider, useField } from "./Field";
 import { useInstanceProducts } from "../../hooks/product";
 import { useState } from "preact/hooks";
+import emptyImage from "../../assets/empty.png";
 
 type Entity = MerchantBackend.Products.ProductDetail & WithId
 
@@ -47,13 +48,13 @@ export function InputSearchProduct<T>({ selected, onChange 
}: Props) {
     return <article class="media">
       <figure class="media-left">
         <p class="image is-128x128">
-          <img src="https://avatars.dicebear.com/v2/gridy/Ms.-Lora-Kiehn.svg"; 
/>
+          <img src={selected.image ? selected.image : emptyImage} />
         </p>
       </figure>
       <div class="media-content">
         <div class="content">
-          <p class="media-meta">Product #{selected.id}</p>
-          <p>{selected.description}</p>
+          <p class="media-meta">Product id: <b>{selected.id}</b></p>
+          <p>Description: {selected.description}</p>
           <div class="buttons is-right mt-5">
             <button class="button" onClick={() => 
onChange(undefined)}>clear</button>
           </div>
@@ -64,7 +65,7 @@ export function InputSearchProduct<T>({ selected, onChange }: 
Props) {
 
   return <FormProvider<ProductSearch> errors={errors} object={prodForm} 
valueHandler={setProdName} >
 
-    <InputWithAddon<ProductSearch> 
+    <InputWithAddon<ProductSearch>
       name="name"
       addonBefore={<span class="icon" ><i class="mdi mdi-magnify" /></span>}
     >
@@ -102,17 +103,30 @@ function ProductList({ name, onSelect }: 
ProductListProps) {
         </div>
       </div>
     } else {
+      const filtered = result.data.filter(p => re.test(p.id) || 
re.test(p.description))
       products = <div class="dropdown-content">
-        {result.data.filter(p => re.test(p.description)).map(p => (
-          <div class="dropdown-item" onClick={() => onSelect(p)}>
-            {p.description}
-          </div>
-        ))}
+        {!filtered.length ?
+          <div class="dropdown-item" >
+            no results
+          </div> :
+          result.data.filter(p => re.test(p.id) || 
re.test(p.description)).map(p => (
+            <div class="dropdown-item" onClick={() => onSelect(p)} style={{ 
cursor: 'pointer' }}>
+              <table>
+                <tr>
+                  <td style={{width:32}}>
+                    <div class="image" style={{minWidth:32}}><img 
src={p.image} style={{ width: 32, height: 32 }} /></div>
+                  </td>
+                  <td><b>{p.id}</b>: {p.description}</td>
+                </tr>
+              </table>
+            </div>
+          ))
+        }
       </div>
     }
   }
   return <div class="dropdown is-active">
-    <div class="dropdown-menu" id="dropdown-menu" role="menu">
+    <div class="dropdown-menu" id="dropdown-menu" role="menu" 
style={{minWidth: '20rem'}}>
       {products}
     </div>
   </div>
diff --git a/packages/frontend/src/components/form/InputSecured.stories.tsx 
b/packages/frontend/src/components/form/InputSecured.stories.tsx
index a83036a..9ec3bee 100644
--- a/packages/frontend/src/components/form/InputSecured.stories.tsx
+++ b/packages/frontend/src/components/form/InputSecured.stories.tsx
@@ -28,13 +28,11 @@ export default {
   title: 'Fields/InputSecured',
   component: InputSecured,
 };
-{/* <FormProvider<Entity> errors={errors} object={value} 
valueHandler={valueHandler} > */ }
-{/* <InputSecured<Entity> name="auth_token" /> */ }
 
-type T = {auth_token: string | null}
+type T = { auth_token: string | null }
 
 export const InitialValueEmpty = () => {
-  const [state, setState] = useState<Partial<T>>({auth_token: ''})
+  const [state, setState] = useState<Partial<T>>({ auth_token: '' })
   return <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
     Initial value: ''
     <InputSecured<T> name="auth_token" />
@@ -42,14 +40,14 @@ export const InitialValueEmpty = () => {
 }
 
 export const InitialValueToken = () => {
-  const [state, setState] = useState<Partial<T>>({auth_token: 'token'})
+  const [state, setState] = useState<Partial<T>>({ auth_token: 'token' })
   return <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
     <InputSecured<T> name="auth_token" />
   </FormProvider>
 }
 
 export const InitialValueNull = () => {
-  const [state, setState] = useState<Partial<T>>({auth_token: null})
+  const [state, setState] = useState<Partial<T>>({ auth_token: null })
   return <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
     Initial value: ''
     <InputSecured<T> name="auth_token" />
diff --git a/packages/frontend/src/components/form/InputStock.tsx 
b/packages/frontend/src/components/form/InputStock.tsx
index 50307b6..f9c9c10 100644
--- a/packages/frontend/src/components/form/InputStock.tsx
+++ b/packages/frontend/src/components/form/InputStock.tsx
@@ -62,13 +62,15 @@ export function InputStock<T>({ name, readonly, 
alreadyExist }: Props<T>) {
   const [addedStock, setAddedStock] = useState<StockDelta>({ incoming: 0, 
lost: 0 })
 
   useLayoutEffect(() => {
-    console.log(formValue)
-
-    onChange({
-      ...formValue,
-      current: (formValue?.current || 0) + addedStock.incoming,
-      lost: (formValue?.lost || 0) + addedStock.lost
-    } as any)
+    if (!formValue) {
+      onChange(undefined as any)
+    } else {
+      onChange({
+        ...formValue,
+        current: (formValue?.current || 0) + addedStock.incoming,
+        lost: (formValue?.lost || 0) + addedStock.lost
+      } as any)
+    }
   }, [formValue, addedStock])
 
   if (!formValue) {
diff --git a/packages/frontend/src/components/product/ProductForm.tsx 
b/packages/frontend/src/components/product/ProductForm.tsx
index af5eed0..8ed6436 100644
--- a/packages/frontend/src/components/product/ProductForm.tsx
+++ b/packages/frontend/src/components/product/ProductForm.tsx
@@ -56,8 +56,8 @@ export function ProductForm({ onSubscribe, initial, 
alreadyExist, }: Props) {
 
   const submit = useCallback((): Entity | undefined => {
     try {
-      (alreadyExist ?  updateSchema : createSchema).validateSync(value, { 
abortEarly: false })
-      const stock:Stock = (value as any).stock;
+      (alreadyExist ? updateSchema : createSchema).validateSync(value, { 
abortEarly: false })
+      const stock: Stock = (value as any).stock;
       delete (value as any).stock;
 
       if (!stock) {
@@ -68,7 +68,6 @@ export function ProductForm({ onSubscribe, initial, 
alreadyExist, }: Props) {
         value.next_restock = stock.nextRestock instanceof Date ? { t_ms: 
stock.nextRestock.getTime() } : stock.nextRestock;
         value.address = stock.address;
       }
-      console.log(value)
       return value as MerchantBackend.Products.ProductDetail & { product_id: 
string }
     } catch (err) {
       const errors = err.inner as yup.ValidationError[]
@@ -86,8 +85,8 @@ export function ProductForm({ onSubscribe, initial, 
alreadyExist, }: Props) {
   return <div>
     <FormProvider<Entity> name="product" errors={errors} object={value} 
valueHandler={valueHandler} >
 
-      {alreadyExist ? undefined : <InputWithAddon<Entity> name="product_id" 
addonBefore={`${backend.url}/product/`} /> }
-      
+      {alreadyExist ? undefined : <InputWithAddon<Entity> name="product_id" 
addonBefore={`${backend.url}/product/`} />}
+
       <InputImage<Entity> name="image" />
       <Input<Entity> name="description" inputType="multiline" />
       <Input<Entity> name="unit" />
diff --git a/packages/frontend/src/components/product/ProductList.tsx 
b/packages/frontend/src/components/product/ProductList.tsx
index b313cf5..a62fd01 100644
--- a/packages/frontend/src/components/product/ProductList.tsx
+++ b/packages/frontend/src/components/product/ProductList.tsx
@@ -16,6 +16,7 @@
 import { h, VNode } from "preact"
 import { MerchantBackend } from "../../declaration"
 import { multiplyPrice } from "../../utils/amount"
+import emptyImage from "../../assets/empty.png";
 
 interface Props {
   list: MerchantBackend.Product[],
@@ -40,16 +41,18 @@ export function ProductList({ list, actions = [] }: Props): 
VNode {
       <tbody>
         {list.map((entry, index) => {
           return <tr>
-            <td>image</td>
+            <td>
+              <img style={{ height: 32, width: 32 }} src={entry.image ? 
entry.image : emptyImage} />
+            </td>
             <td >{entry.description}</td>
             <td >
-              {entry.quantity} {entry.unit}
+              {entry.quantity === 0 ? '--' : `${entry.quantity} ${entry.unit}`}
             </td>
             <td >{entry.price}</td>
             <td >{multiplyPrice(entry.price, entry.quantity)}</td>
             <td class="is-actions-cell right-sticky">
               {actions.map(a => {
-                <div class="buttons is-right">
+                return <div class="buttons is-right">
                   <button class="button is-small is-danger jb-modal" 
type="button" onClick={() => a.handler(entry, index)}>
                     {a.name}
                   </button>
diff --git a/packages/frontend/src/hooks/backend.ts 
b/packages/frontend/src/hooks/backend.ts
index f9466a0..94c26e2 100644
--- a/packages/frontend/src/hooks/backend.ts
+++ b/packages/frontend/src/hooks/backend.ts
@@ -27,7 +27,6 @@ import { useEffect, useState } from 'preact/hooks';
 
 export function mutateAll(re: RegExp) {
   cache.keys().filter(key => {
-    // console.log(key, re.test(key))
     return re.test(key)
   }).forEach(key => mutate(key, null))
 }
@@ -140,7 +139,7 @@ function buildRequestOk<T>(res: any, url: string, hasToken: 
boolean): HttpRespon
 
 function buildRequestFailed(ex: AxiosError<MerchantBackend.ErrorDetail>, url: 
string, hasToken: boolean): HttpResponseClientError | HttpResponseServerError | 
HttpResponseUnexpectedError {
   const status = ex.response?.status
-  
+
   const info = {
     data: ex.request?.data,
     params: ex.request?.params,
@@ -165,7 +164,7 @@ function buildRequestFailed(ex: 
AxiosError<MerchantBackend.ErrorDetail>, url: st
       serverError: true,
       status,
       info,
-      message: ex.response?.data?.hint || ex.message,
+      message: `${ex.response?.data?.hint} (code ${ex.response?.data?.code})` 
|| ex.message,
       error: ex.response?.data
     }
     return error;
@@ -181,19 +180,34 @@ function buildRequestFailed(ex: 
AxiosError<MerchantBackend.ErrorDetail>, url: st
   return error
 }
 
+
 export async function request<T>(url: string, options: RequestOptions = {}): 
Promise<HttpResponseOk<T>> {
   const headers = options.token ? { Authorization: `Bearer ${options.token}` } 
: undefined
 
+  // use this when simulating an error message from the server
+  // if (url.match(/\/orders\/.*/)) {
+  //   const pepe: any = {
+  //     message: 'qweqwe',
+  //     request: {
+  //       data: {
+          
+  //       },
+  //       params: {
+
+  //       },
+  //     },
+  //     response: {
+  //       data: {
+  //         hint: "This part is the hint",
+  //         code: 2008,
+  //       },
+  //       status: 500,
+  //     }
+  //   };
+  //   throw buildRequestFailed(pepe, url, !!options.token);
+  // }
 
   try {
-    // // http://localhost:9966/instances/blog/private/instances
-    // // Hack, endpoint should respond 404
-    // if (/^\/instances\/[^/]*\/private\/instances$/.test(new 
URL(url).pathname)) {
-    //   console.warn(`HACK: Not going to query ${url}, instead return 404`)
-    //   throw ({ response: { status: 404 }, message: 'not found' })
-    // }
-
-
     const res = await axios({
       url,
       responseType: 'json',
diff --git a/packages/frontend/src/messages/en.po 
b/packages/frontend/src/messages/en.po
index 3dc7eca..32a78a1 100644
--- a/packages/frontend/src/messages/en.po
+++ b/packages/frontend/src/messages/en.po
@@ -376,6 +376,9 @@ msgstr "Profit"
 msgid "fields.product.stock.label"
 msgstr "Stock"
 
+msgid "fields.product.quantity.label"
+msgstr "Quantity"
+
 msgid "fields.product.sold.label"
 msgstr "Sold"
 
diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
index b26fed2..d3f6243 100644
--- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
@@ -267,13 +267,9 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
             {productList.length > 0 &&
               <ProductList list={productList}
                 actions={[{
-                  name: 'Update', handler: (e, index) => {
-                    removeFromNewProduct(index);
-                    setEditingProduct(e);
-                  }
-                }, {
                   name: 'Remove', handler: (e, index) => {
                     removeFromNewProduct(index);
+                    setEditingProduct(e);
                   }
                 }]}
               />
diff --git 
a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx 
b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
index 6aa08f0..54733d5 100644
--- 
a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
+++ 
b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
@@ -32,7 +32,8 @@ interface Props {
 }
 
 export function InventoryProductForm({ currentProducts, onAddProduct }: 
Props): VNode {
-  const [state, setState] = useState<Partial<Form>>({})
+  const initialState = { quantity: 1 }
+  const [state, setState] = useState<Partial<Form>>(initialState)
   const [errors, setErrors] = useState<FormErrors<Form>>({})
 
   const submit = (): void => {
@@ -40,27 +41,31 @@ export function InventoryProductForm({ currentProducts, 
onAddProduct }: Props):
       setErrors({ product: { message: 'select a product first' } });
       return;
     }
-    if (!state.quantity || state.quantity <= 0) {
-      setErrors({ quantity: { message: 'should be greater than 0' } });
-      return;
-    }
-    const currentStock = state.product.total_stock - state.product.total_lost 
- state.product.total_sold
-    const p = currentProducts[state.product.id]
-    if (p) { 
-      if (state.quantity + p.quantity > currentStock) {
-        setErrors({ quantity: { message: `cannot be greater than current stock 
and quantity previously added. max: ${currentStock - p.quantity}` } });
-        return;
-      }
-      onAddProduct(state.product, state.quantity + p.quantity)
+    if (state.product.total_stock === -1) {
+      onAddProduct(state.product, 1)
     } else {
-      if (state.quantity > currentStock) {
-        setErrors({ quantity: { message: `cannot be greater than current stock 
${currentStock}` } });
+      if (!state.quantity || state.quantity <= 0) {
+        setErrors({ quantity: { message: 'should be greater than 0' } });
         return;
       }
-      onAddProduct(state.product, state.quantity)
+      const currentStock = state.product.total_stock - 
state.product.total_lost - state.product.total_sold
+      const p = currentProducts[state.product.id]
+      if (p) {
+        if (state.quantity + p.quantity > currentStock) {
+          setErrors({ quantity: { message: `cannot be greater than current 
stock and quantity previously added. max: ${currentStock - p.quantity}` } });
+          return;
+        }
+        onAddProduct(state.product, state.quantity + p.quantity)
+      } else {
+        if (state.quantity > currentStock) {
+          setErrors({ quantity: { message: `cannot be greater than current 
stock ${currentStock}` } });
+          return;
+        }
+        onAddProduct(state.product, state.quantity)
+      }
     }
 
-    setState({})
+    setState(initialState)
   }
 
   return <FormProvider<Form> errors={errors} object={state} 
valueHandler={setState}>
diff --git 
a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
 
b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
index dfd6b22..bf6a91e 100644
--- 
a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
+++ 
b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
@@ -14,11 +14,21 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 import { Fragment, h, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { FormErrors, FormProvider } from "../../../../components/form/Field";
+import { Input } from "../../../../components/form/Input";
+import { InputCurrency } from "../../../../components/form/InputCurrency";
+import { InputImage } from "../../../../components/form/InputImage";
+import { InputNumber } from "../../../../components/form/InputNumber";
+import { Stock } from "../../../../components/form/InputStock";
+import { InputTaxes } from "../../../../components/form/InputTaxes";
 import { ConfirmModal } from "../../../../components/modal";
-import { ProductForm } from "../../../../components/product/ProductForm";
 import { MerchantBackend } from "../../../../declaration";
 import { useListener } from "../../../../hooks";
+import {
+  NonInventoryProductSchema as schema
+} from '../../../../schemas';
+import * as yup from 'yup';
 
 type Entity = MerchantBackend.Product
 
@@ -35,11 +45,11 @@ export function NonInventoryProductFrom({ value, 
onAddProduct }: Props): VNode {
     setShowCreateProduct(editing)
   }, [editing])
 
-  const [submitForm, addFormSubmitter] = 
useListener<Partial<MerchantBackend.Products.ProductAddDetail> | 
undefined>((result) => {
+  const [submitForm, addFormSubmitter] = 
useListener<Partial<MerchantBackend.Product> | undefined>((result) => {
     if (result) {
       setShowCreateProduct(false)
       onAddProduct({
-        quantity: result.total_stock || 0,
+        quantity: result.quantity || 0,
         taxes: result.taxes || [],
         description: result.description || '',
         image: result.image || '',
@@ -49,18 +59,72 @@ export function NonInventoryProductFrom({ value, 
onAddProduct }: Props): VNode {
     }
   })
 
-  const initial: Partial<MerchantBackend.Products.ProductAddDetail> = {
-    ...value,
-    total_stock: value?.quantity || 0,
-    taxes: []
-  }
-
   return <Fragment>
     <div class="buttons">
       <button class="button is-success" onClick={() => 
setShowCreateProduct(true)} >add new product</button>
     </div>
     {showCreateProduct && <ConfirmModal active onCancel={() => 
setShowCreateProduct(false)} onConfirm={submitForm}>
-      <ProductForm initial={initial} onSubscribe={addFormSubmitter} />
+      <ProductForm initial={value} onSubscribe={addFormSubmitter} />
     </ConfirmModal>}
   </Fragment>
-}
\ No newline at end of file
+}
+
+interface ProductProps {
+  onSubscribe: (c: () => Entity | undefined) => void;
+  initial?: Partial<Entity>;
+}
+
+interface NonInventoryProduct {
+  quantity: number;
+  description: string;
+  unit: string;
+  price: string;
+  image: string;
+  taxes: MerchantBackend.Tax[];
+}
+
+export function ProductForm({ onSubscribe, initial }: ProductProps) {
+  const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({
+    taxes: [],
+    ...initial,
+  })
+  const [errors, setErrors] = useState<FormErrors<NonInventoryProduct>>({})
+
+  const submit = useCallback((): Entity | undefined => {
+    try {
+      const validated = schema.validateSync(value, { abortEarly: false })
+      const result : MerchantBackend.Product = {
+        description: validated.description,
+        image: validated.image,
+        price: validated.price,
+        quantity: validated.quantity,
+        taxes: validated.taxes,
+        unit: validated.unit,
+      }
+      return result
+    } catch (err) {
+      const errors = err.inner as yup.ValidationError[]
+      const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ 
...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message 
} }), {})
+      setErrors(pathMessages)
+    }
+  }, [value])
+
+  useEffect(() => {
+    onSubscribe(submit)
+  }, [submit])
+
+  return <div>
+    <FormProvider<NonInventoryProduct> name="product" errors={errors} 
object={value} valueHandler={valueHandler} >
+
+      <InputImage<NonInventoryProduct> name="image" />
+      <Input<NonInventoryProduct> name="description" inputType="multiline" />
+      <Input<NonInventoryProduct> name="unit" />
+      <InputCurrency<NonInventoryProduct> name="price" />
+
+      <InputNumber<NonInventoryProduct> name="quantity" />
+
+      <InputTaxes<NonInventoryProduct> name="taxes" />
+
+    </FormProvider>
+  </div>
+}
diff --git a/packages/frontend/src/paths/instance/orders/create/index.tsx 
b/packages/frontend/src/paths/instance/orders/create/index.tsx
index c918b03..01d2e6c 100644
--- a/packages/frontend/src/paths/instance/orders/create/index.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/index.tsx
@@ -39,11 +39,7 @@ interface Props {
 export default function OrderCreate({ onConfirm, onBack }: Props): VNode {
   const { createOrder } = useOrderAPI()
   const [notif, setNotif] = useState<Notification | undefined>(undefined)
-  const [createdOk, setCreatedOk] = useState<Entity | undefined>(undefined);
 
-  if (createdOk) {
-    return <OrderCreatedSuccessfully entity={createdOk} onConfirm={onConfirm} 
onCreateAnother={() => setCreatedOk(undefined)} />
-  }
 
   return <Fragment>
     <NotificationCard notification={{
@@ -57,9 +53,7 @@ export default function OrderCreate({ onConfirm, onBack }: 
Props): VNode {
     <CreatePage
       onBack={onBack}
       onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => {
-        createOrder(request).then((response) => {
-          setCreatedOk({ request, response: response.data })
-        }).catch((error) => {
+        createOrder(request).then(onConfirm).catch((error) => {
           setNotif({
             message: 'could not create order',
             type: "ERROR",
diff --git a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx 
b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
index 7f1353f..e2e8fe7 100644
--- a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
+++ b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
@@ -48,11 +48,6 @@ type Claimed = 
MerchantBackend.Orders.CheckPaymentClaimedResponse
 
 function ClaimedPage({ id, order }: { id: string; order: 
MerchantBackend.Orders.CheckPaymentClaimedResponse }) {
   const events: Event[] = []
-  events.push({
-    when: new Date(),
-    description: 'now',
-    type: 'now'
-  })
   events.push({
     when: new Date(order.contract_terms.timestamp.t_ms),
     description: 'order created',
@@ -79,7 +74,6 @@ function ClaimedPage({ id, order }: { id: string; order: 
MerchantBackend.Orders.
     type: 'delivery'
   })
 
-  events.sort((a, b) => a.when.getTime() - b.when.getTime())
   const [value, valueHandler] = useState<Partial<Claimed>>(order)
 
   return <div>
@@ -154,15 +148,17 @@ function ClaimedPage({ id, order }: { id: string; order: 
MerchantBackend.Orders.
             </div>
           </section>
 
-          <section class="section">
-            <div class="columns">
-              <div class="column is-12" >
-                <div class="title">Product list</div>
-                <ProductList list={order.contract_terms.products} />
+          {order.contract_terms.products.length > 0 &&
+            <section class="section">
+              <div class="columns">
+                <div class="column is-12" >
+                  <div class="title">Product list</div>
+                  <ProductList list={order.contract_terms.products} />
+                </div>
+                <div class="column" />
               </div>
-              <div class="column" />
-            </div>
-          </section>
+            </section>
+          }
 
         </div>
         <div class="column" />
@@ -172,11 +168,6 @@ function ClaimedPage({ id, order }: { id: string; order: 
MerchantBackend.Orders.
 }
 function PaidPage({ id, order, onRefund }: { id: string; order: 
MerchantBackend.Orders.CheckPaymentPaidResponse, onRefund: (id: string) => void 
}) {
   const events: Event[] = []
-  events.push({
-    when: new Date(),
-    description: 'now',
-    type: 'now'
-  })
   events.push({
     when: new Date(order.contract_terms.timestamp.t_ms),
     description: 'order created',
@@ -223,7 +214,6 @@ function PaidPage({ id, order, onRefund }: { id: string; 
order: MerchantBackend.
       type: 'wired',
     })
 
-  events.sort((a, b) => a.when.getTime() - b.when.getTime())
   const [value, valueHandler] = useState<Partial<Paid>>(order)
 
   const refundable = new Date().getTime() < 
order.contract_terms.refund_deadline.t_ms
diff --git a/packages/frontend/src/paths/instance/orders/details/Timeline.tsx 
b/packages/frontend/src/paths/instance/orders/details/Timeline.tsx
index 3881e26..a7a8121 100644
--- a/packages/frontend/src/paths/instance/orders/details/Timeline.tsx
+++ b/packages/frontend/src/paths/instance/orders/details/Timeline.tsx
@@ -15,12 +15,37 @@
  */
 import { format } from "date-fns";
 import { h } from "preact";
+import { useEffect, useState } from "preact/hooks";
 
 interface Props {
   events: Event[]
 }
 
-export function Timeline({ events }: Props) {
+export function Timeline({ events:e }: Props) {
+  const events = [...e]
+  events.push({
+    when: new Date(),
+    description: 'now',
+    type: 'now'
+  })
+
+  events.sort((a, b) => a.when.getTime() - b.when.getTime())
+
+  const [state, setState] = useState(events)
+  useEffect(() => {
+    const handle = setTimeout(() => {
+      const eventsWithoutNow = state.filter(e => e.type !== 'now')
+      eventsWithoutNow.push({
+        when: new Date(),
+        description: 'now',
+        type: 'now'
+      })
+      setState(eventsWithoutNow)
+    },1000)
+    return () => {
+      clearTimeout(handle)
+    }
+  })
   return <div class="timeline">
     {events.map(e => {
       return <div class="timeline-item">
diff --git 
a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
index 1255d24..427ebc0 100644
--- a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
@@ -25,7 +25,7 @@ import { ProductForm } from 
"../../../../components/product/ProductForm";
 import { MerchantBackend } from "../../../../declaration";
 import { useListener } from "../../../../hooks";
 
-type Entity = MerchantBackend.Products.ProductDetail & { product_id: string}
+type Entity = MerchantBackend.Products.ProductAddDetail & { product_id: string}
 
 interface Props {
   onCreate: (d: Entity) => void;
diff --git a/packages/frontend/src/paths/instance/products/create/index.tsx 
b/packages/frontend/src/paths/instance/products/create/index.tsx
index e31ccdc..ed997c4 100644
--- a/packages/frontend/src/paths/instance/products/create/index.tsx
+++ b/packages/frontend/src/paths/instance/products/create/index.tsx
@@ -25,7 +25,6 @@ import { NotificationCard } from 
'../../../../components/menu';
 import { MerchantBackend } from '../../../../declaration';
 import { useProductAPI } from '../../../../hooks/product';
 import { Notification } from '../../../../utils/types';
-import { CreatedSuccessfully } from './CreatedSuccessfully';
 import { CreatePage } from './CreatePage';
 
 export type Entity = MerchantBackend.Products.ProductAddDetail
@@ -36,20 +35,13 @@ interface Props {
 export default function CreateProduct({ onConfirm, onBack }: Props): VNode {
   const { createProduct } = useProductAPI()
   const [notif, setNotif] = useState<Notification | undefined>(undefined)
-  const [createdOk, setCreatedOk] = useState<Entity | undefined>(undefined);
-
-  if (createdOk) {
-    return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} 
onCreateAnother={() => setCreatedOk(undefined)} />
-  }
 
   return <Fragment>
     <NotificationCard notification={notif} />
     <CreatePage
       onBack={onBack}
-      onCreate={(request: MerchantBackend.Products.ProductDetail & { 
product_id: string}) => {
-        createProduct(request).then(() => {
-          setCreatedOk(request)
-        }).catch((error) => {
+      onCreate={(request: MerchantBackend.Products.ProductAddDetail) => {
+        createProduct(request).then(() => onConfirm()).catch((error) => {
           setNotif({
             message: 'could not create product',
             type: "ERROR",
diff --git a/packages/frontend/src/paths/instance/products/list/Table.tsx 
b/packages/frontend/src/paths/instance/products/list/Table.tsx
index 8fc6540..b548b56 100644
--- a/packages/frontend/src/paths/instance/products/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/products/list/Table.tsx
@@ -27,6 +27,7 @@ import { FormErrors, FormProvider } from 
"../../../../components/form/Field"
 import { InputCurrency } from "../../../../components/form/InputCurrency"
 import { InputNumber } from "../../../../components/form/InputNumber"
 import { MerchantBackend, WithId } from "../../../../declaration"
+import emptyImage from "../../../../assets/empty.png";
 
 type Entity = MerchantBackend.Products.ProductDetail & WithId
 
@@ -108,7 +109,9 @@ function Table({ rowSelection, rowSelectionHandler, 
instances, onSelect, onUpdat
             }
 
             return <Fragment><tr>
-              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} ><img src={i.image} 
style={{ border: 'solid black 1px', width: 100, height: 100 }} /></td>
+              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >
+                <img src={i.image ? i.image : emptyImage} style={{ border: 
'solid black 1px', width: 100, height: 100 }} />
+              </td>
               <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.description}</td>
               <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.price} / 
{i.unit}</td>
               <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{sum(i.taxes)}</td>
@@ -150,7 +153,30 @@ interface FastProductUpdate {
   price: string;
 }
 
-function FastProductUpdateForm({ product, onUpdate, onCancel }: 
FastProductUpdateFormProps) {
+function FastProductWithInfiniteStockUpdateForm({ product, onUpdate, onCancel 
}: FastProductUpdateFormProps) {
+  const [value, valueHandler] = useState<{price:string}>({price: 
product.price})
+
+  return <Fragment>
+    <FormProvider<FastProductUpdate> name="added" object={value} 
valueHandler={valueHandler as any} >
+      <InputCurrency<FastProductUpdate> name="price" />
+    </FormProvider>
+
+    <div class="buttons is-right mt-5">
+      <button class="button" onClick={onCancel} ><Message id="Cancel" 
/></button>
+      <button class="button is-info" onClick={() => {
+
+        return onUpdate({
+          ...product,
+          price: value.price,
+        })
+
+      }}><Message id="Confirm" /></button>
+    </div>
+
+  </Fragment>
+}
+
+function FastProductWithManagedStockUpdateForm({ product, onUpdate, onCancel 
}: FastProductUpdateFormProps) {
   const [value, valueHandler] = useState<FastProductUpdate>({
     incoming: 0, lost: 0, price: product.price
   })
@@ -170,7 +196,6 @@ function FastProductUpdateForm({ product, onUpdate, 
onCancel }: FastProductUpdat
   )
 
   const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== 
undefined)
-  const isDirty = Object.keys(value).some(k => !!(value as any)[k])
 
   return <Fragment>
     <FormProvider<FastProductUpdate> name="added" errors={errors} 
object={value} valueHandler={valueHandler as any} >
@@ -189,12 +214,12 @@ function FastProductUpdateForm({ product, onUpdate, 
onCancel }: FastProductUpdat
 
     <div class="buttons is-right mt-5">
       <button class="button" onClick={onCancel} ><Message id="Cancel" 
/></button>
-      <button class="button is-info" disabled={hasErrors || !isDirty} 
onClick={() => {
+      <button class="button is-info" disabled={hasErrors} onClick={() => {
 
         return onUpdate({
           ...product,
           total_stock: product.total_stock + value.incoming,
-          total_lost: product.total_lost+ value.lost,
+          total_lost: product.total_lost + value.lost,
           price: value.price,
         })
 
@@ -202,7 +227,12 @@ function FastProductUpdateForm({ product, onUpdate, 
onCancel }: FastProductUpdat
     </div>
 
   </Fragment>
+}
 
+function FastProductUpdateForm(props: FastProductUpdateFormProps) {
+  return props.product.total_stock === -1 ?
+    <FastProductWithInfiniteStockUpdateForm {...props} /> :
+    <FastProductWithManagedStockUpdateForm {...props} />
 }
 
 function EmptyTable(): VNode {
diff --git a/packages/frontend/src/paths/instance/update/index.tsx 
b/packages/frontend/src/paths/instance/update/index.tsx
index c1bd457..81c9a06 100644
--- a/packages/frontend/src/paths/instance/update/index.tsx
+++ b/packages/frontend/src/paths/instance/update/index.tsx
@@ -27,7 +27,7 @@ export interface Props {
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
   onLoadError: (e: HttpError) => VNode;
-  onUpdateError: (e: Error) => void;
+  onUpdateError: (e: HttpError) => void;
 
 }
 
diff --git a/packages/frontend/src/schemas/index.ts 
b/packages/frontend/src/schemas/index.ts
index 54a3a8e..4e0e1c8 100644
--- a/packages/frontend/src/schemas/index.ts
+++ b/packages/frontend/src/schemas/index.ts
@@ -188,4 +188,13 @@ export const TaxSchema = yup.object().shape({
   tax: yup.string()
     .required()
     .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
-})
\ No newline at end of file
+})
+
+export const NonInventoryProductSchema = yup.object().shape({
+  quantity: yup.number().required().positive(),
+  description: yup.string().required(),
+  unit: yup.string().ensure().required(),
+  price: yup.string()
+    .required()
+    .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
+})
diff --git a/packages/frontend/tests/__mocks__/fileTransformer.js 
b/packages/frontend/tests/__mocks__/fileTransformer.js
new file mode 100644
index 0000000..fb0ad54
--- /dev/null
+++ b/packages/frontend/tests/__mocks__/fileTransformer.js
@@ -0,0 +1,9 @@
+// fileTransformer.js
+const path = require('path');
+
+module.exports = {
+  process(src, filename, config, options) {
+    return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
+  },
+};
+
diff --git a/packages/frontend/tests/stories.test.tsx 
b/packages/frontend/tests/stories.test.tsx
new file mode 100644
index 0000000..afc9a11
--- /dev/null
+++ b/packages/frontend/tests/stories.test.tsx
@@ -0,0 +1,37 @@
+import { mount } from 'enzyme';
+import { h } from 'preact';
+import * as ctx from '../src/context/backend';
+
+const fs = require('fs');
+
+function getFiles (dir: string, files_: string[] = []){
+    var files = fs.readdirSync(dir);
+    for (var i in files){
+        var name = dir + '/' + files[i];
+        if (fs.statSync(name).isDirectory()){
+            getFiles(name, files_);
+        } else {
+            files_.push(name);
+        }
+    }
+    return files_;
+}
+
+const re = RegExp('.*\.stories.tsx')
+
+it('render every story', () => {
+  jest.spyOn(ctx, 'useConfigContext').mockImplementation(() => ({ version: 
'1.0.0', currency: 'EUR' }));
+
+  getFiles('./src').filter(f => re.test(f)).map(f => {
+    const s = require('../'+f)
+    delete s.default
+    Object.keys(s).forEach(k => {
+      const Component = s[k];
+      try {
+        mount(<Component {...Component.args}/>);
+      } catch (error) {
+        console.error(`problem rendering ${f} example ${k}`, error)
+      }
+    })
+  })
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 259207a..e3dc9fb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -54,6 +54,7 @@ importers:
       messageformat-po-loader: 0.3.0_messageformat@2.3.0
       node-sass: 5.0.0
       preact-cli: 3.0.5_5ba117d350dffefc029551565eae09b5
+      preact-render-to-json: 3.6.6_preact@10.5.13
       preact-render-to-string: 5.1.16_preact@10.5.13
       rimraf: 3.0.2
       sass-loader: 10.1.1_node-sass@5.0.0
@@ -109,6 +110,7 @@ importers:
       preact: ^10.5.13
       preact-cli: ^3.0.5
       preact-messages: workspace:*
+      preact-render-to-json: ^3.6.6
       preact-render-to-string: ^5.1.16
       preact-router: ^3.2.1
       rimraf: ^3.0.2
@@ -153,6 +155,10 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-BwKEkO+2a67DcFeS3RLl0Z3Gs2OvdXewuWjc1Hfokhb5eQWP9YRYH1/+VrVZvql2CfjOiNGqSAFOYt4lsqTHzg==
+  /@babel/compat-data/7.13.15:
+    dev: true
+    resolution:
+      integrity: 
sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA==
   /@babel/core/7.12.9:
     dependencies:
       '@babel/code-frame': 7.12.13
@@ -199,6 +205,36 @@ packages:
       node: '>=6.9.0'
     resolution:
       integrity: 
sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==
+  /@babel/core/7.13.16:
+    dependencies:
+      '@babel/code-frame': 7.12.13
+      '@babel/generator': 7.13.16
+      '@babel/helper-compilation-targets': 7.13.16_@babel+core@7.13.16
+      '@babel/helper-module-transforms': 7.13.14
+      '@babel/helpers': 7.13.17
+      '@babel/parser': 7.13.16
+      '@babel/template': 7.12.13
+      '@babel/traverse': 7.13.17
+      '@babel/types': 7.13.17
+      convert-source-map: 1.7.0
+      debug: 4.3.1
+      gensync: 1.0.0-beta.2
+      json5: 2.2.0
+      semver: 6.3.0
+      source-map: 0.5.7
+    dev: true
+    engines:
+      node: '>=6.9.0'
+    resolution:
+      integrity: 
sha512-sXHpixBiWWFti0AV2Zq7avpTasr6sIAu7Y396c608541qAU2ui4a193m0KSQmfPSKFZLnQ3cvlKDOm3XkuXm3Q==
+  /@babel/generator/7.13.16:
+    dependencies:
+      '@babel/types': 7.13.17
+      jsesc: 2.5.2
+      source-map: 0.5.7
+    dev: true
+    resolution:
+      integrity: 
sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg==
   /@babel/generator/7.13.9:
     dependencies:
       '@babel/types': 7.13.0
@@ -232,6 +268,18 @@ packages:
       '@babel/core': ^7.0.0
     resolution:
       integrity: 
sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA==
+  /@babel/helper-compilation-targets/7.13.16_@babel+core@7.13.16:
+    dependencies:
+      '@babel/compat-data': 7.13.15
+      '@babel/core': 7.13.16
+      '@babel/helper-validator-option': 7.12.17
+      browserslist: 4.16.5
+      semver: 6.3.0
+    dev: true
+    peerDependencies:
+      '@babel/core': ^7.0.0
+    resolution:
+      integrity: 
sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA==
   /@babel/helper-create-class-features-plugin/7.13.11_@babel+core@7.13.10:
     dependencies:
       '@babel/core': 7.13.10
@@ -304,12 +352,24 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-yvRf8Ivk62JwisqV1rFRMxiSMDGnN6KH1/mDMmIrij4jztpQNRoHqqMG3U6apYbGRPJpgPalhva9Yd06HlUxJQ==
+  /@babel/helper-member-expression-to-functions/7.13.12:
+    dependencies:
+      '@babel/types': 7.13.17
+    dev: true
+    resolution:
+      integrity: 
sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==
   /@babel/helper-module-imports/7.12.13:
     dependencies:
       '@babel/types': 7.13.0
     dev: true
     resolution:
       integrity: 
sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==
+  /@babel/helper-module-imports/7.13.12:
+    dependencies:
+      '@babel/types': 7.13.17
+    dev: true
+    resolution:
+      integrity: 
sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==
   /@babel/helper-module-transforms/7.13.0:
     dependencies:
       '@babel/helper-module-imports': 7.12.13
@@ -324,6 +384,19 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw==
+  /@babel/helper-module-transforms/7.13.14:
+    dependencies:
+      '@babel/helper-module-imports': 7.13.12
+      '@babel/helper-replace-supers': 7.13.12
+      '@babel/helper-simple-access': 7.13.12
+      '@babel/helper-split-export-declaration': 7.12.13
+      '@babel/helper-validator-identifier': 7.12.11
+      '@babel/template': 7.12.13
+      '@babel/traverse': 7.13.17
+      '@babel/types': 7.13.17
+    dev: true
+    resolution:
+      integrity: 
sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==
   /@babel/helper-optimise-call-expression/7.12.13:
     dependencies:
       '@babel/types': 7.13.0
@@ -355,12 +428,27 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-Segd5me1+Pz+rmN/NFBOplMbZG3SqRJOBlY+mA0SxAv6rjj7zJqr1AVr3SfzUVTLCv7ZLU5FycOM/SBGuLPbZw==
+  /@babel/helper-replace-supers/7.13.12:
+    dependencies:
+      '@babel/helper-member-expression-to-functions': 7.13.12
+      '@babel/helper-optimise-call-expression': 7.12.13
+      '@babel/traverse': 7.13.17
+      '@babel/types': 7.13.17
+    dev: true
+    resolution:
+      integrity: 
sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==
   /@babel/helper-simple-access/7.12.13:
     dependencies:
       '@babel/types': 7.13.0
     dev: true
     resolution:
       integrity: 
sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==
+  /@babel/helper-simple-access/7.13.12:
+    dependencies:
+      '@babel/types': 7.13.17
+    dev: true
+    resolution:
+      integrity: 
sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==
   /@babel/helper-skip-transparent-expression-wrappers/7.12.1:
     dependencies:
       '@babel/types': 7.13.0
@@ -398,6 +486,14 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==
+  /@babel/helpers/7.13.17:
+    dependencies:
+      '@babel/template': 7.12.13
+      '@babel/traverse': 7.13.17
+      '@babel/types': 7.13.17
+    dev: true
+    resolution:
+      integrity: 
sha512-Eal4Gce4kGijo1/TGJdqp3WuhllaMLSrW6XcL0ulyUAQOuxHcCafZE8KHg9857gcTehsm/v7RcOx2+jp0Ryjsg==
   /@babel/highlight/7.13.10:
     dependencies:
       '@babel/helper-validator-identifier': 7.12.11
@@ -413,6 +509,13 @@ packages:
     hasBin: true
     resolution:
       integrity: 
sha512-PhuoqeHoO9fc4ffMEVk4qb/w/s2iOSWohvbHxLtxui0eBg3Lg5gN1U8wp1V1u61hOWkPQJJyJzGH6Y+grwkq8Q==
+  /@babel/parser/7.13.16:
+    dev: true
+    engines:
+      node: '>=6.0.0'
+    hasBin: true
+    resolution:
+      integrity: 
sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw==
   /@babel/plugin-proposal-async-generator-functions/7.13.8_@babel+core@7.13.10:
     dependencies:
       '@babel/core': 7.13.10
@@ -1279,18 +1382,24 @@ packages:
       '@babel/core': ^7.0.0-0
     resolution:
       integrity: 
sha512-yCVtABcmvQjRsX2elcZFUV5Q5kDDpHdtXKKku22hNDma60lYuhKmtp1ykZ/okRCPLT2bR5S+cA1kvtBdAFlDTQ==
-  /@babel/runtime-corejs3/7.13.10:
+  /@babel/runtime-corejs3/7.13.17:
     dependencies:
-      core-js-pure: 3.9.1
+      core-js-pure: 3.11.0
       regenerator-runtime: 0.13.7
     dev: true
     resolution:
-      integrity: 
sha512-x/XYVQ1h684pp1mJwOV4CyvqZXqbc8CMsMGUnAbuc82ZCdv1U63w5RSUzgDSXQHG5Rps/kiksH6g2D5BuaKyXg==
+      integrity: 
sha512-RGXINY1YvduBlGrP+vHjJqd/nK7JVpfM4rmZLGMx77WoL3sMrhheA0qxii9VNn1VHnxJLEyxmvCB+Wqc+x/FMw==
   /@babel/runtime/7.13.10:
     dependencies:
       regenerator-runtime: 0.13.7
     resolution:
       integrity: 
sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
+  /@babel/runtime/7.13.17:
+    dependencies:
+      regenerator-runtime: 0.13.7
+    dev: true
+    resolution:
+      integrity: 
sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA==
   /@babel/template/7.12.13:
     dependencies:
       '@babel/code-frame': 7.12.13
@@ -1313,6 +1422,19 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ==
+  /@babel/traverse/7.13.17:
+    dependencies:
+      '@babel/code-frame': 7.12.13
+      '@babel/generator': 7.13.16
+      '@babel/helper-function-name': 7.12.13
+      '@babel/helper-split-export-declaration': 7.12.13
+      '@babel/parser': 7.13.16
+      '@babel/types': 7.13.17
+      debug: 4.3.1
+      globals: 11.12.0
+    dev: true
+    resolution:
+      integrity: 
sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg==
   /@babel/types/7.13.0:
     dependencies:
       '@babel/helper-validator-identifier': 7.12.11
@@ -1321,6 +1443,13 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==
+  /@babel/types/7.13.17:
+    dependencies:
+      '@babel/helper-validator-identifier': 7.12.11
+      to-fast-properties: 2.0.0
+    dev: true
+    resolution:
+      integrity: 
sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==
   /@base2/pretty-print-object/1.0.0:
     dev: true
     resolution:
@@ -1331,7 +1460,7 @@ packages:
       integrity: 
sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
   /@cnakazawa/watch/1.0.4:
     dependencies:
-      exec-sh: 0.3.4
+      exec-sh: 0.3.6
       minimist: 1.2.5
     dev: true
     engines:
@@ -1719,17 +1848,17 @@ packages:
       integrity: 
sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==
   /@jest/transform/26.6.2:
     dependencies:
-      '@babel/core': 7.13.10
+      '@babel/core': 7.13.16
       '@jest/types': 26.6.2
       babel-plugin-istanbul: 6.0.0
-      chalk: 4.1.0
+      chalk: 4.1.1
       convert-source-map: 1.7.0
       fast-json-stable-stringify: 2.1.0
       graceful-fs: 4.2.6
       jest-haste-map: 26.6.2
       jest-regex-util: 26.0.0
       jest-util: 26.6.2
-      micromatch: 4.0.2
+      micromatch: 4.0.4
       pirates: 4.0.1
       slash: 3.0.0
       source-map: 0.6.1
@@ -1743,9 +1872,9 @@ packages:
     dependencies:
       '@types/istanbul-lib-coverage': 2.0.3
       '@types/istanbul-reports': 3.0.0
-      '@types/node': 14.14.35
+      '@types/node': 15.0.0
       '@types/yargs': 15.0.13
-      chalk: 4.1.0
+      chalk: 4.1.1
     dev: true
     engines:
       node: '>= 10.14.2'
@@ -2962,13 +3091,13 @@ packages:
       node: '>=6'
     resolution:
       integrity: 
sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
-  /@testing-library/dom/7.30.0:
+  /@testing-library/dom/7.30.4:
     dependencies:
       '@babel/code-frame': 7.12.13
-      '@babel/runtime': 7.13.10
+      '@babel/runtime': 7.13.17
       '@types/aria-query': 4.2.1
       aria-query: 4.2.2
-      chalk: 4.1.0
+      chalk: 4.1.1
       dom-accessibility-api: 0.5.4
       lz-string: 1.4.4
       pretty-format: 26.6.2
@@ -2976,7 +3105,7 @@ packages:
     engines:
       node: '>=10'
     resolution:
-      integrity: 
sha512-v4GzWtltaiDE0yRikLlcLAfEiiK8+ptu6OuuIebm9GdC2XlZTNDPGEfM2UkEtnH7hr9TRq2sivT5EA9P1Oy7bw==
+      integrity: 
sha512-GObDVMaI4ARrZEXaRy4moolNAxWPKvEYNV/fa6Uc2eAzR/t4otS6A7EhrntPBIQLeehL9DbVhscvvv7gd6hWqA==
   /@testing-library/preact-hooks/1.1.0_8a3b8354086a0a31d950b2aa8b26d524:
     dependencies:
       '@testing-library/preact': 2.0.1_preact@10.5.13
@@ -2989,7 +3118,7 @@ packages:
       integrity: 
sha512-+JIor+NsOHkK3oIrwMDGKGHXTN0JJi462dBJlj4FNbGaDPTlctE6eu2ranWQirh7/FJMkWfzQCP+tk7jmY8ZrQ==
   /@testing-library/preact/2.0.1_preact@10.5.13:
     dependencies:
-      '@testing-library/dom': 7.30.0
+      '@testing-library/dom': 7.30.4
       preact: 10.5.13
     dev: true
     engines:
@@ -3031,7 +3160,7 @@ packages:
       integrity: 
sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==
   /@types/babel__traverse/7.11.1:
     dependencies:
-      '@babel/types': 7.13.0
+      '@babel/types': 7.13.17
     dev: true
     resolution:
       integrity: 
sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==
@@ -3066,14 +3195,14 @@ packages:
       integrity: sha1-pYHWiDR+EOUN18F9byiAoQNUMZ0=
   /@types/glob/7.1.3:
     dependencies:
-      '@types/minimatch': 3.0.3
-      '@types/node': 14.14.35
+      '@types/minimatch': 3.0.4
+      '@types/node': 15.0.0
     dev: true
     resolution:
       integrity: 
sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==
   /@types/graceful-fs/4.1.5:
     dependencies:
-      '@types/node': 14.14.35
+      '@types/node': 15.0.0
     dev: true
     resolution:
       integrity: 
sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==
@@ -3144,10 +3273,10 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-my6fLBvpY70KattTNzYOK6KU1oR1+UCz9ug/JbcF5UrEmeCt9P7DV2t7L8+t18mMPINqGQCE4O8PLOPbI84gxw==
-  /@types/minimatch/3.0.3:
+  /@types/minimatch/3.0.4:
     dev: true
     resolution:
-      integrity: 
sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+      integrity: 
sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==
   /@types/node-fetch/2.5.8:
     dependencies:
       '@types/node': 14.14.35
@@ -3159,6 +3288,14 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==
+  /@types/node/14.14.42:
+    dev: true
+    resolution:
+      integrity: 
sha512-88QoObqn9WYIUMRzOx92GmSHmU3JCyukC2ulEv8tFjUG9VeV2FQ/cA7VQ1gi+rB/+gBMVvzVFcTnz8RdMDVIWw==
+  /@types/node/15.0.0:
+    dev: true
+    resolution:
+      integrity: 
sha512-YN1d+ae2MCb4U0mMa+Zlb5lWTdpFShbAj5nmte6lel27waMMBfivrm0prC16p/Di3DyTrmerrYUT8/145HXxVw==
   /@types/normalize-package-data/2.4.0:
     dev: true
     resolution:
@@ -3266,7 +3403,7 @@ packages:
       integrity: 
sha512-Fx+NpfOO0CpeYX2g9bkvX8O5qh9wrU1sOF4g8sft4Mu7z+qfe387YlyY8w8daDyDsKY5vUxM0yxkAYnbkRbZEw==
   /@types/webpack-sources/2.1.0:
     dependencies:
-      '@types/node': 14.14.35
+      '@types/node': 14.14.42
       '@types/source-list-map': 0.1.2
       source-map: 0.7.3
     dev: true
@@ -3840,6 +3977,15 @@ packages:
       node: '>= 8'
     resolution:
       integrity: 
sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
+  /anymatch/3.1.2:
+    dependencies:
+      normalize-path: 3.0.0
+      picomatch: 2.2.3
+    dev: true
+    engines:
+      node: '>= 8'
+    resolution:
+      integrity: 
sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
   /app-root-dir/1.0.2:
     dev: true
     resolution:
@@ -3863,8 +4009,8 @@ packages:
       integrity: 
sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
   /aria-query/4.2.2:
     dependencies:
-      '@babel/runtime': 7.13.10
-      '@babel/runtime-corejs3': 7.13.10
+      '@babel/runtime': 7.13.17
+      '@babel/runtime-corejs3': 7.13.17
     dev: true
     engines:
       node: '>=6.0'
@@ -4897,6 +5043,19 @@ packages:
     hasBin: true
     resolution:
       integrity: 
sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==
+  /browserslist/4.16.5:
+    dependencies:
+      caniuse-lite: 1.0.30001216
+      colorette: 1.2.2
+      electron-to-chromium: 1.3.720
+      escalade: 3.1.1
+      node-releases: 1.1.71
+    dev: true
+    engines:
+      node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7
+    hasBin: true
+    resolution:
+      integrity: 
sha512-C2HAjrM1AI/djrpAUU/tr4pml1DqLIzJKSLDBXBrNErl9ZCCTXdhwxdJjYc16953+mBWf7Lw+uUJgpgb8cN71A==
   /bser/2.1.1:
     dependencies:
       node-int64: 0.4.0
@@ -5167,6 +5326,10 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-ic/jXfa6tgiPBAISWk16jRI2q8YfjxHnSG7ddSL1ptrIP8Uy11SayFrjXRAk3NumHpDb21fdTkbTxb/hOrFrnQ==
+  /caniuse-lite/1.0.30001216:
+    dev: true
+    resolution:
+      integrity: 
sha512-1uU+ww/n5WCJRwUcc9UH/W6925Se5aNnem/G5QaSDga2HzvjYMs8vRbekGUN/PnTZ7ezTHcxxTEb9fgiMYwH6Q==
   /capture-exit/2.0.0:
     dependencies:
       rsvp: 4.8.5
@@ -5239,6 +5402,15 @@ packages:
       node: '>=10'
     resolution:
       integrity: 
sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
+  /chalk/4.1.1:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+    dev: true
+    engines:
+      node: '>=10'
+    resolution:
+      integrity: 
sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
   /char-regex/1.0.2:
     dev: true
     engines:
@@ -5840,6 +6012,11 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-jXAirMQxrkbiiLsCx9bQPJFA6llDadKMpYrBJQJ3/c4/vsPP/fAf29h24tviRlvwUL6AmY5CHLu2GvjuYviQqA==
+  /core-js-pure/3.11.0:
+    dev: true
+    requiresBuild: true
+    resolution:
+      integrity: 
sha512-PxEiQGjzC+5qbvE7ZIs5Zn6BynNeZO9zHhrrWmkRff2SZLq0CE/H5LuZOJHhmOQ8L38+eMzEHAmPYWrUtDfuDQ==
   /core-js-pure/3.9.1:
     dev: true
     requiresBuild: true
@@ -6843,6 +7020,10 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-WCn+ZaU3V8WttlLNSOGOAlR2XpxibGre7slwGrYBB6oTjYPgP29LNDGG6wLvLTMseLdE+G1vno7PfY7JyDV48g==
+  /electron-to-chromium/1.3.720:
+    dev: true
+    resolution:
+      integrity: 
sha512-B6zLTxxaOFP4WZm6DrvgRk8kLFYWNhQ5TrHMC0l5WtkMXhU5UbnvWoTfeEwqOruUSlNMhVLfYak7REX6oC5Yfw==
   /element-resize-detector/1.2.2:
     dependencies:
       batch-processor: 1.0.0
@@ -7431,10 +7612,10 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
-  /exec-sh/0.3.4:
+  /exec-sh/0.3.6:
     dev: true
     resolution:
-      integrity: 
sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==
+      integrity: 
sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==
   /execa/1.0.0:
     dependencies:
       cross-spawn: 6.0.5
@@ -7907,7 +8088,7 @@ packages:
     dependencies:
       asynckit: 0.4.0
       combined-stream: 1.0.8
-      mime-types: 2.1.29
+      mime-types: 2.1.30
     dev: true
     engines:
       node: '>= 6'
@@ -9954,7 +10135,7 @@ packages:
       integrity: 
sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==
   /jest-diff/26.6.2:
     dependencies:
-      chalk: 4.1.0
+      chalk: 4.1.1
       diff-sequences: 26.6.2
       jest-get-type: 26.3.0
       pretty-format: 26.6.2
@@ -10020,15 +10201,15 @@ packages:
     dependencies:
       '@jest/types': 26.6.2
       '@types/graceful-fs': 4.1.5
-      '@types/node': 14.14.35
-      anymatch: 3.1.1
+      '@types/node': 15.0.0
+      anymatch: 3.1.2
       fb-watchman: 2.0.1
       graceful-fs: 4.2.6
       jest-regex-util: 26.0.0
       jest-serializer: 26.6.2
       jest-util: 26.6.2
       jest-worker: 26.6.2
-      micromatch: 4.0.2
+      micromatch: 4.0.4
       sane: 4.1.0
       walker: 1.0.7
     dev: true
@@ -10074,7 +10255,7 @@ packages:
       integrity: 
sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==
   /jest-matcher-utils/26.6.2:
     dependencies:
-      chalk: 4.1.0
+      chalk: 4.1.1
       jest-diff: 26.6.2
       jest-get-type: 26.3.0
       pretty-format: 26.6.2
@@ -10088,9 +10269,9 @@ packages:
       '@babel/code-frame': 7.12.13
       '@jest/types': 26.6.2
       '@types/stack-utils': 2.0.0
-      chalk: 4.1.0
+      chalk: 4.1.1
       graceful-fs: 4.2.6
-      micromatch: 4.0.2
+      micromatch: 4.0.4
       pretty-format: 26.6.2
       slash: 3.0.0
       stack-utils: 2.0.3
@@ -10161,7 +10342,7 @@ packages:
   /jest-resolve/26.6.2:
     dependencies:
       '@jest/types': 26.6.2
-      chalk: 4.1.0
+      chalk: 4.1.1
       graceful-fs: 4.2.6
       jest-pnp-resolver: 1.2.2_jest-resolve@26.6.2
       jest-util: 26.6.2
@@ -10237,7 +10418,7 @@ packages:
       integrity: 
sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==
   /jest-serializer/26.6.2:
     dependencies:
-      '@types/node': 14.14.35
+      '@types/node': 15.0.0
       graceful-fs: 4.2.6
     dev: true
     engines:
@@ -10246,11 +10427,11 @@ packages:
       integrity: 
sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==
   /jest-snapshot/26.6.2:
     dependencies:
-      '@babel/types': 7.13.0
+      '@babel/types': 7.13.17
       '@jest/types': 26.6.2
       '@types/babel__traverse': 7.11.1
       '@types/prettier': 2.2.3
-      chalk: 4.1.0
+      chalk: 4.1.1
       expect: 26.6.2
       graceful-fs: 4.2.6
       jest-diff: 26.6.2
@@ -10261,7 +10442,7 @@ packages:
       jest-resolve: 26.6.2
       natural-compare: 1.4.0
       pretty-format: 26.6.2
-      semver: 7.3.4
+      semver: 7.3.5
     dev: true
     engines:
       node: '>= 10.14.2'
@@ -10270,11 +10451,11 @@ packages:
   /jest-util/26.6.2:
     dependencies:
       '@jest/types': 26.6.2
-      '@types/node': 14.14.35
-      chalk: 4.1.0
+      '@types/node': 15.0.0
+      chalk: 4.1.1
       graceful-fs: 4.2.6
       is-ci: 2.0.0
-      micromatch: 4.0.2
+      micromatch: 4.0.4
     dev: true
     engines:
       node: '>= 10.14.2'
@@ -11224,6 +11405,15 @@ packages:
       node: '>=8'
     resolution:
       integrity: 
sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+  /micromatch/4.0.4:
+    dependencies:
+      braces: 3.0.2
+      picomatch: 2.2.3
+    dev: true
+    engines:
+      node: '>=8.6'
+    resolution:
+      integrity: 
sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
   /miller-rabin/4.0.1:
     dependencies:
       bn.js: 4.12.0
@@ -11238,6 +11428,12 @@ packages:
       node: '>= 0.6'
     resolution:
       integrity: 
sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==
+  /mime-db/1.47.0:
+    dev: true
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: 
sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==
   /mime-types/2.1.29:
     dependencies:
       mime-db: 1.46.0
@@ -11246,6 +11442,14 @@ packages:
       node: '>= 0.6'
     resolution:
       integrity: 
sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==
+  /mime-types/2.1.30:
+    dependencies:
+      mime-db: 1.47.0
+    dev: true
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: 
sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==
   /mime/1.6.0:
     dev: true
     engines:
@@ -11798,6 +12002,10 @@ packages:
       node: '>=0.10.0'
     resolution:
       integrity: sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+  /object-inspect/1.10.2:
+    dev: true
+    resolution:
+      integrity: 
sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==
   /object-inspect/1.9.0:
     dev: true
     resolution:
@@ -12397,6 +12605,12 @@ packages:
       node: '>=8.6'
     resolution:
       integrity: 
sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+  /picomatch/2.2.3:
+    dev: true
+    engines:
+      node: '>=8.6'
+    resolution:
+      integrity: 
sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==
   /pify/2.3.0:
     dev: true
     engines:
@@ -12977,6 +13191,14 @@ packages:
       preact-render-to-string: '*'
     resolution:
       integrity: 
sha512-Oc9HOjwX/3Zk1eXkmP7TMmtqbaROl7F0RWZ2Ni5Q/grmx3yBLJmarkUcOSKabkI/Usw2dU3RVju32Q3Pvy5qIw==
+  /preact-render-to-json/3.6.6_preact@10.5.13:
+    dependencies:
+      preact: 10.5.13
+    dev: true
+    peerDependencies:
+      preact: '*'
+    resolution:
+      integrity: sha1-9n9IWBkSrFP8n0hzvG1840L3HCA=
   /preact-render-to-string/5.1.16_preact@10.5.13:
     dependencies:
       preact: 10.5.13
@@ -13053,7 +13275,7 @@ packages:
       '@jest/types': 26.6.2
       ansi-regex: 5.0.0
       ansi-styles: 4.3.0
-      react-is: 17.0.1
+      react-is: 17.0.2
     dev: true
     engines:
       node: '>= 10'
@@ -13538,6 +13760,10 @@ packages:
     dev: true
     resolution:
       integrity: 
sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
+  /react-is/17.0.2:
+    dev: true
+    resolution:
+      integrity: 
sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
   /react-lifecycles-compat/3.0.4:
     dev: true
     resolution:
@@ -14296,7 +14522,7 @@ packages:
       '@cnakazawa/watch': 1.0.4
       anymatch: 2.0.0
       capture-exit: 2.0.0
-      exec-sh: 0.3.4
+      exec-sh: 0.3.6
       execa: 1.0.0
       fb-watchman: 2.0.1
       micromatch: 3.1.10
@@ -14487,6 +14713,15 @@ packages:
     hasBin: true
     resolution:
       integrity: 
sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
+  /semver/7.3.5:
+    dependencies:
+      lru-cache: 6.0.0
+    dev: true
+    engines:
+      node: '>=10'
+    hasBin: true
+    resolution:
+      integrity: 
sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
   /send/0.17.1:
     dependencies:
       debug: 2.6.9
@@ -14656,7 +14891,7 @@ packages:
     dependencies:
       call-bind: 1.0.2
       get-intrinsic: 1.1.1
-      object-inspect: 1.9.0
+      object-inspect: 1.10.2
     dev: true
     resolution:
       integrity: 
sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]