Compare commits

...

6 Commits

@ -2,6 +2,16 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.2.0](https://git.lufrai.com/rappli/rappli/compare/v1.1.0...v1.2.0) (2022-07-13)
### Features
* implement custom NumberInput component ([cf2a1c4](https://git.lufrai.com/rappli/rappli/commit/cf2a1c4d9040cd4d3a3a317c8bca14c032f5167e))
* replace gray with slate bg color for form labels ([6b89e28](https://git.lufrai.com/rappli/rappli/commit/6b89e28c5f98139a5deee5b4c1786c4d5c1935d8)), closes [#3](https://git.lufrai.com/rappli/rappli/issues/3)
* support decimals in position quantity input ([a935e6f](https://git.lufrai.com/rappli/rappli/commit/a935e6fe563ca9ada14c9dd0ff28551a5e1f69aa)), closes [#12](https://git.lufrai.com/rappli/rappli/issues/12)
* support non-numeric characters in line2 street field ([0bb8423](https://git.lufrai.com/rappli/rappli/commit/0bb8423b01a26d3f96ae5869483611d086fb2606)), closes [#8](https://git.lufrai.com/rappli/rappli/issues/8)
## [1.1.0](https://git.lufrai.com/rappli/rappli/compare/v1.0.1...v1.1.0) (2022-07-07)

4
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "rappli",
"version": "1.1.0",
"version": "1.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "rappli",
"version": "1.1.0",
"version": "1.2.0",
"license": "MIT",
"dependencies": {
"solid-start": "v0.1.0-alpha.89",

@ -1,6 +1,6 @@
{
"name": "rappli",
"version": "1.1.0",
"version": "1.2.0",
"bin": "./bin/rappli.js",
"scripts": {
"dev": "solid-start dev",

@ -1,7 +1,6 @@
import { formatISO9075, fromUnixTime, getUnixTime } from "date-fns";
import {
Component,
JSX,
Show,
mergeProps,
splitProps,
@ -14,6 +13,11 @@ import AsteriskIcon from "~icons/ph/asterisk-bold";
import MaximizeIcon from "~icons/carbon/maximize";
import MinimizeIcon from "~icons/carbon/minimize";
import autosize from "autosize";
import type { JSX } from "solid-js";
import {
createNativeInputValue,
createOptionalNumberInputHandler,
} from "~/util";
export const TextInput: Component<
{
@ -71,6 +75,7 @@ export const TextInput: Component<
"gap-1": true,
"h-8": props.vertical,
"font-bold": rest.required,
"bg-slate-200/70": vState().valid,
"bg-red-100": !vState().valid,
}}
>
@ -89,13 +94,13 @@ export const TextInput: Component<
[props.class || ""]: true,
}}
type="text"
lang={rest.type === "number" ? "en" : undefined}
placeholder={props.placeholder}
{...rest}
/>
<Show when={props.suffix}>
<span
classList={{
"bg-slate-200/70": vState().valid,
"bg-red-100": !vState().valid,
}}
>
@ -154,7 +159,7 @@ export const TextArea: Component<
</Show>
</button>
<label class="input-group input-group-vertical">
<span class="h-8 flex gap-2 justify-between pr-14">
<span class="h-8 bg-slate-200/70 flex gap-2 justify-between pr-14">
{props.label}
{props.labelSuffixJsx}
</span>
@ -216,3 +221,27 @@ export const UnixDateInput: Component<
/>
);
};
export const NumberInput: Component<
{ value?: number; onInput: (v: number | undefined) => void } & Omit<
Parameters<typeof TextInput>[0],
"onInput"
>
> = (p) => {
let el: HTMLInputElement = undefined!;
const [props, rest] = splitProps(p, ["value", "onInput"]);
const value = createNativeInputValue(
() => el,
() => props.value
);
return (
<TextInput
ref={el}
value={value()}
maxLength={9}
onInput={createOptionalNumberInputHandler(props.onInput)}
{...rest}
/>
);
};

@ -19,7 +19,13 @@ import { generate } from "node-iso11649";
import { customAlphabet } from "nanoid";
import createAccordion from "../Accordion";
import { Checkbox, TextArea, TextInput, UnixDateInput } from "../Form";
import {
Checkbox,
NumberInput,
TextArea,
TextInput,
UnixDateInput,
} from "../Form";
import { autoAnimate } from "~/directives/autoAnimate";
import {
LocalStoreContext,
@ -127,11 +133,9 @@ const SettingsOverlay: Component = () => {
/>
<TextInput
name={withPrefix("line2")}
type={isStructured() ? "number" : "text"}
type="text"
label={isStructured() ? "Nummer" : "Linie 2"}
maxLength={isStructured() ? undefined : 70}
max={isStructured() ? 9999999999999999 : undefined}
min={isStructured() ? 0 : undefined}
maxLength={isStructured() ? 16 : 70}
required
value={props.address().line2}
onInput={(evt) => props.setter("line2", evt.currentTarget.value)}
@ -289,7 +293,10 @@ const SettingsOverlay: Component = () => {
</div>
<div class="col-span-2">
<div class="input-group input-group-sm">
<span style={{ "min-width": fullWidthLabelWidth }}>
<span
class="bg-slate-200/70"
style={{ "min-width": fullWidthLabelWidth }}
>
Typ neuer Positionen
</span>
<select
@ -321,21 +328,18 @@ const SettingsOverlay: Component = () => {
</div>
</div>
<FullWidthAccordionInput
required
type="number"
label="Standard Einzelpreis"
min="0"
step="0.01"
value={state.defaultItemPrice}
onInput={(evt) =>
setState(
"defaultItemPrice",
parseFloat(evt.currentTarget.value) || 0
)
}
onBlur={resetInput(0)}
/>
<div class="col-span-2">
<NumberInput
required
label="Standard Einzelpreis"
labelMinWidth={fullWidthLabelWidth}
value={state.defaultItemPrice}
onInput={(v) =>
v != null && setState("defaultItemPrice", v)
}
onBlur={resetInput(0)}
/>
</div>
<div class="col-span-2">
<TextArea
@ -586,18 +590,16 @@ const SettingsOverlay: Component = () => {
/>
</div>
<div class="col-span-2">
<TextInput
<NumberInput
required
label="MwST-Satz"
type="number"
step="0.1"
suffix="%"
value={new Big(localState.vatRate).mul(100).toNumber()}
onInput={(evt) => {
evt.currentTarget.value !== "" &&
onInput={(v) => {
v != null &&
setLocalState(
"vatRate",
new Big(evt.currentTarget.value).div(100).toNumber()
new Big(v).div(100).toNumber()
);
}}
onBlur={resetInput(0)}

@ -21,9 +21,13 @@ import AddIcon from "~icons/carbon/add-filled";
import DeleteIcon from "~icons/carbon/trash-can";
import DragVerticalIcon from "~icons/carbon/drag-vertical";
import PositionSettingsIcon from "~icons/carbon/settings-adjust";
import { Checkbox, TextArea, TextInput } from "../Form";
import { Checkbox, NumberInput, TextArea, TextInput } from "../Form";
import { MarkdownHelpLabel } from "../Markdown";
import { createOptionalNumberInputHandler } from "~/util";
import {
createOptionalNumberInputHandler,
createNativeInputValue,
resetInput,
} from "~/util";
export const PositionsSettings: Component = () => {
const [state, setState] = useContext(StoreContext)!;
@ -125,6 +129,12 @@ export const PositionsSettings: Component = () => {
);
};
let quantityInputEl: HTMLInputElement = undefined!;
const quantityValue = createNativeInputValue(
() => quantityInputEl,
() => position.quantity
);
return (
<div class="indicator w-full">
<div class="indicator-item indicator-middle indicator-end flex items-center">
@ -243,24 +253,24 @@ export const PositionsSettings: Component = () => {
<Show when={position.type === POSITION_TYPE_QUANTITY}>
<div class="flex-1">
<input
ref={quantityInputEl}
class="w-full input input-bordered input-xs"
value={
position.quantity === 0
? ""
: position.quantity
}
value={quantityValue()}
placeholder="Menge"
min="0"
required
type="number"
onInput={(e) => {
setState(
"positions",
idx(),
"quantity",
parseFloat(e.currentTarget.value) || 0
);
}}
name="Menge"
onInput={createOptionalNumberInputHandler(
(v) => {
v != null &&
setState(
"positions",
idx(),
"quantity",
v
);
}
)}
onBlur={resetInput(0)}
/>
</div>
</Show>
@ -293,7 +303,7 @@ export const PositionsSettings: Component = () => {
</Show>
</div>
</div>
<TextInput
<NumberInput
size="xs"
value={position.itemPrice}
label="Einzelpreis"
@ -302,12 +312,9 @@ export const PositionsSettings: Component = () => {
? state.defaultItemPrice + ""
: undefined
}
step="0.01"
min="0"
type="number"
onInput={createOptionalNumberInputHandler((v) =>
onInput={(v) =>
setState("positions", idx(), "itemPrice", v)
)}
}
/>
<div
use:autoAnimate
@ -348,21 +355,18 @@ export const PositionsSettings: Component = () => {
/>
</div>
<div class="col-span-2">
<TextInput
<NumberInput
label="Aktionspreis"
suffix="CHF"
type="number"
step="0.01"
min="0"
value={position.fixedDiscountPrice}
onInput={createOptionalNumberInputHandler((v) =>
onInput={(v) =>
setState(
"positions",
idx(),
"fixedDiscountPrice",
v
)
)}
}
/>
</div>
<div class="col-span-2">

@ -1,6 +1,6 @@
import Big from "big.js";
import { fromUnixTime, intlFormat } from "date-fns";
import { JSX } from "solid-js";
import { createMemo, JSX } from "solid-js";
export const sleep = (timeout: number) =>
new Promise((res) => setTimeout(res, timeout));
@ -90,9 +90,51 @@ export const createOptionalNumberInputHandler = (
return;
}
const value =
e.currentTarget.value == "" ? undefined : e.currentTarget.valueAsNumber;
let value =
e.currentTarget.value == ""
? undefined
: parseNumberInput(e.currentTarget.value);
if (Number.isNaN(value)) {
return;
}
onInput(value);
};
};
const parseNumberInput = (v: string): number => parseFloat(v.replace(",", "."));
export const createNativeInputValue = (
getEl: () => HTMLInputElement,
signal: () => any
) =>
createMemo(function (prev) {
const value = signal();
const el = getEl();
if (!el) {
return value != null ? value : "";
}
const elValue = parseNumberInput(el.value);
// If the element value and signal value are equal, we can skip triggering the memo change by reusing the prev value
let result = elValue == value ? prev : value;
// NaN is always != NaN in js, we have to replace it with a value which has a proper identity
if (Number.isNaN(result)) {
result = undefined;
}
if (result == null) {
result = "";
}
// If both the value and prev value are the same, but the element value is different, we have to update it manually
if (value === prev && elValue != value) {
el.value = result;
}
return result;
});

Loading…
Cancel
Save