commit 929e8f389ea8eec3bb942c111f8e37efffba2d63
parent d37369bbdeb9ff471cee42e794076a3d7d7eda45
Author: Sebastian <sebasjm@gmail.com>
Date: Thu, 18 Feb 2021 09:46:00 -0300
payto as array
Diffstat:
8 files changed, 108 insertions(+), 47 deletions(-)
diff --git a/src/components/hooks/useLang.ts b/src/components/hooks/useLang.ts
@@ -2,5 +2,5 @@ import { StateUpdater } from "preact/hooks";
import { useNotNullLocalStorage } from "./useLocalStorage";
export default function useLang(): [string, StateUpdater<string>] {
- return useNotNullLocalStorage('lang-preference', navigator.language || (navigator as any).userLanguage )
+ return useNotNullLocalStorage('lang-preference', typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : 'en' )
}
diff --git a/src/components/hooks/useLocalStorage.ts b/src/components/hooks/useLocalStorage.ts
@@ -2,16 +2,18 @@ import { StateUpdater, useState } from "preact/hooks";
export function useLocalStorage(key: string, initialValue?: string): [string | undefined, StateUpdater<string | undefined>] {
const [storedValue, setStoredValue] = useState<string | undefined>((): string | undefined => {
- return window.localStorage.getItem(key) || initialValue;
+ return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
});
const setValue = (value?: string | ((val?: string) => string | undefined)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
- if (!valueToStore) {
- window.localStorage.removeItem(key)
- } else {
- window.localStorage.setItem(key, valueToStore);
+ if (typeof window !== "undefined") {
+ if (!valueToStore) {
+ window.localStorage.removeItem(key)
+ } else {
+ window.localStorage.setItem(key, valueToStore);
+ }
}
};
@@ -20,17 +22,19 @@ export function useLocalStorage(key: string, initialValue?: string): [string | u
export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
const [storedValue, setStoredValue] = useState<string>((): string => {
- return window.localStorage.getItem(key) || initialValue;
+ return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
});
const setValue = (value: string | ((val: string) => string)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
- if (!valueToStore) {
- window.localStorage.removeItem(key)
- } else {
- window.localStorage.setItem(key, valueToStore);
- }
+ if (typeof window !== "undefined") {
+ if (!valueToStore) {
+ window.localStorage.removeItem(key)
+ } else {
+ window.localStorage.setItem(key, valueToStore);
+ }
+ }
};
return [storedValue, setValue];
diff --git a/src/components/navbar/index.tsx b/src/components/navbar/index.tsx
@@ -2,7 +2,8 @@ import { h, VNode } from 'preact';
import { useState } from 'preact/hooks';
import LoginPage from '../auth/LoginPage';
import i18n from '../../i18n'
-import logo from '../../assets/logo.jpeg'
+// TODO: Fix compilation problem
+// import * as logo from '../../assets/logo.jpeg';
interface Props {
lang: string;
@@ -14,7 +15,7 @@ export default function NavigationBar({ lang, setLang }: Props): VNode {
return (<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="https://taler.net">
- <img src={logo} style={{height: 50, maxHeight: 50}} />
+ <img src="https://taler.net/static/images/logo-2020.jpg" style={{height: 50, maxHeight: 50}} />
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
diff --git a/src/components/yup/YupField.tsx b/src/components/yup/YupField.tsx
@@ -1,7 +1,6 @@
import { h, VNode } from "preact";
-import { Localizer, Text, useText } from "preact-i18n";
+import { Text, useText } from "preact-i18n";
import { useState } from "preact/hooks";
-import { KeyValue } from "../../declaration";
import { useBackendConfig } from "../hooks/backend";
import useBackend from "../hooks/useBackend";
@@ -31,15 +30,17 @@ export default function YupField(name: string, field: string, errors: any, objec
onChange: updateField(field)
}
const [backend] = useBackend()
- const {data} = useBackendConfig()
-
+ const { data } = useBackendConfig()
+ const currency = data?.currency || ''
+
switch (info.meta?.type) {
case 'group': return <YupObjectInput name={name}
info={info} errors={errors}
value={object && object[field]}
onChange={(updater: any): void => valueHandler((prev: any) => ({ ...prev, [field]: updater(prev[field]) }))}
/>
- case 'amount': return <YupInputWithAddon {...values} addon={data?.currency || ''} />;
+ case 'array': return <YupInputArray {...values} />;
+ case 'amount': return <YupInputWithAddon {...values} addon={currency} onChange={(v: string): void => values.onChange(`${currency}:${v}`)} value={values.value?.split(':')[1]} />;
case 'url': return <YupInputWithAddon {...values} addon={`${backend.url}/private/instances/`} />;
case 'secured': return <YupInputSecured {...values} />;
case 'duration': return <YupInput {...values} value={object && object[field]?.d_ms} />;
@@ -57,9 +58,9 @@ function YupObjectInput({ name, info, value, errors, onChange }: Props2): VNode
</p>
<button class="card-header-icon" aria-label="more options" onClick={(): void => setActive(!active)}>
<span class="icon">
- { active ?
+ {active ?
<i class="mdi mdi-arrow-up" /> :
- <i class="mdi mdi-arrow-down" /> }
+ <i class="mdi mdi-arrow-down" />}
</span>
</button>
</header>
@@ -90,7 +91,7 @@ function YupInput({ name, readonly, value, errors, onChange }: Props): VNode {
<div class="field">
<p class="control">
<input class={errors[name] ? "input is-danger" : "input"} type="text"
- placeholder={dict['placeholder']} readonly={readonly}
+ placeholder={dict.placeholder} readonly={readonly}
name={name} value={value}
onChange={(e): void => onChange(e.currentTarget.value)} />
<Text id={`fields.instance.${name}.help`} />
@@ -103,6 +104,57 @@ function YupInput({ name, readonly, value, errors, onChange }: Props): VNode {
</div>
}
+function YupInputArray({ name, readonly, value, errors, onChange }: Props): VNode {
+ const dict = useText({
+ placeholder: `fields.instance.${name}.placeholder`,
+ tooltip: `fields.instance.${name}.tooltip`,
+ })
+ const array = value as unknown as string[] || []
+ const [currentValue, setCurrentValue] = useState('')
+
+ return <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <Text id={`fields.instance.${name}.label`} />
+ {dict.tooltip && <span class="icon" data-tooltip={dict.tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <div class="field has-addons">
+ <p class="control">
+ <button class="button is-info" onClick={(): void => {
+ onChange([currentValue, ...array])
+ setCurrentValue('')
+ }} >add</button>
+ </p>
+ <p class="control">
+ <input class={errors[name] ? "input is-danger" : "input"} type="text"
+ placeholder={dict.placeholder} readonly={readonly}
+ name={name} value={currentValue}
+ onChange={(e): void => setCurrentValue(e.currentTarget.value)} />
+ <Text id={`fields.instance.${name}.help`} />
+ </p>
+ </div>
+ {errors[name] ? <p class="help is-danger">
+ <Text id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message}</Text>
+ </p> : null}
+ {array.map(v => <div class="tags has-addons">
+ <span class="tag is-medium is-info">{v}</span>
+ <a class="tag is-medium is-danger is-delete" onClick={() => {
+ onChange(array.filter(f => f !== v))
+ setCurrentValue(v)
+ }} />
+ </div>
+ )}
+ </div>
+
+ </div>
+ </div>
+}
+
function YupInputWithAddon({ name, readonly, value, errors, onChange, addon }: Props & { addon: string }): VNode {
const dict = useText({
placeholder: `fields.instance.${name}.placeholder`,
@@ -126,7 +178,7 @@ function YupInputWithAddon({ name, readonly, value, errors, onChange, addon }: P
</div>
<p class="control is-expanded">
<input class={errors[name] ? "input is-danger" : "input"} type="text"
- placeholder={dict['placeholder']} readonly={readonly}
+ placeholder={dict.placeholder} readonly={readonly}
name={name} value={value}
onChange={(e): void => onChange(e.currentTarget.value)} />
<Text id={`fields.instance.${name}.help`} />
@@ -145,7 +197,7 @@ function YupInputSecured({ name, readonly, value, errors, onChange }: Props): VN
})
const [active, setActive] = useState(false)
-
+
return <div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
diff --git a/src/declaration.d.ts b/src/declaration.d.ts
@@ -5,9 +5,10 @@ declare module "*.css" {
export default mapping;
}
declare module "*.jpeg" {
- const mapping: Record<string, string>;
- export default mapping;
+ const value: any;
+ export = value;
}
+
declare module "*.scss" {
const mapping: Record<string, string>;
export default mapping;
diff --git a/src/i18n/index.ts b/src/i18n/index.ts
@@ -151,7 +151,7 @@ export default {
},
payto_uris: {
label: 'Bank accounts',
- placeholder: 'comma separated values',
+ tooltip: 'Bank account URI',
help: 'payto://x-taler-bank/bank.taler:5882/blogger',
},
default_max_deposit_fee: {
diff --git a/src/routes/instances/CardTable.tsx b/src/routes/instances/CardTable.tsx
@@ -49,15 +49,20 @@ export default function CardTable({ instances, onCreate, onSelect, selected }: P
return <div class="card has-table">
<header class="card-header">
<p class="card-header-title"><span class="icon"><i class="mdi mdi-account-multiple" /></span><Text id="text.instances" /></p>
-
- <button class={rowSelection.length > 0 ? "card-header-icon" : "is-hidden"}
- type="button" onClick={(): void => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} >
- <span class="icon"><i class="mdi mdi-trash-can" /></span>
- </button>
- <button class="card-header-icon" type="button" onClick={onCreate}>
- <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span>
- </button>
+ <div class="card-header-icon" aria-label="more options">
+
+ <button class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"}
+ type="button" onClick={(): void => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} >
+ <span class="icon"><i class="mdi mdi-trash-can" /></span>
+ </button>
+ </div>
+ <div class="card-header-icon" aria-label="more options">
+ <button class="button is-info" type="button" onClick={onCreate}>
+ <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span>
+ </button>
+ </div>
+
</header>
<div class="card-content">
<div class="b-table has-pagination">
diff --git a/src/schemas/index.ts b/src/schemas/index.ts
@@ -12,13 +12,8 @@ yup.setLocale({
},
});
-function stringToArray(this: yup.AnySchema, current: any, original: string): string[] {
- if (this.isType(current)) return current;
- return original.split(',').filter(s => s.length > 0)
-}
-
function listOfPayToUrisAreValid(values?: (string | undefined)[]): boolean {
- return !!values && values.filter(v => v && PAYTO_REGEX.test(v)).length > 0;
+ return !!values && values.every(v => v && PAYTO_REGEX.test(v));
}
function numberToDuration(this: yup.AnySchema, current: any, original: string): Duration {
@@ -33,10 +28,13 @@ function currencyWithAmountIsValid(value?: string): boolean {
const InstanceSchema = yup.object().shape({
id: yup.string().required().meta({type: 'url'}),
name: yup.string().required(),
- auth_token: yup.string().min(8).max(20).optional().meta({type: 'secured'}),
+ auth_token: yup.string()
+ .min(8).max(20)
+ .optional()
+ .meta({type: 'secured'}),
payto_uris: yup.array().of(yup.string())
.min(1)
- .transform(stringToArray)
+ .meta({type: 'array'})
.test('payto', '{path} is not valid', listOfPayToUrisAreValid),
default_max_deposit_fee: yup.string()
.required()
@@ -75,13 +73,13 @@ const InstanceSchema = yup.object().shape({
default_pay_delay: yup.object()
.shape({ d_ms: yup.number() })
.required()
- .meta({ type: 'duration' })
- .transform(numberToDuration),
+ .meta({ type: 'duration' }),
+ // .transform(numberToDuration),
default_wire_transfer_delay: yup.object()
.shape({ d_ms: yup.number() })
.required()
- .meta({ type: 'duration' })
- .transform(numberToDuration),
+ .meta({ type: 'duration' }),
+ // .transform(numberToDuration),
})
export const InstanceUpdateSchema = InstanceSchema.clone().omit(['id']);