Browse Source

fix buggy quota field

pull/2677/head
Bernd Storath 2 weeks ago
parent
commit
68571d99b9
  1. 33
      src/app/components/Form/QuotaField.vue
  2. 37
      src/app/utils/math.ts
  3. 21
      src/test/unit/math.app.spec.ts

33
src/app/components/Form/QuotaField.vue

@ -12,13 +12,16 @@
>
<BaseInput
:id="id"
v-model.number="quotaGiB"
:max="maximumGiB"
:min="minimumGiB"
:model-value="quotaInput"
:name="id"
step="any"
type="number"
class="w-full !border-0"
@blur="onBlur"
@focus="isFocused = true"
@update:model-value="updateQuotaInput"
/>
<div
class="flex h-full items-center bg-neutral-200 px-4 dark:bg-neutral-800"
@ -35,11 +38,27 @@ const quotaBytes = defineModel<number | null>({ required: true });
const minimumGiB = quotaBytesToGiB(1)!;
const maximumGiB = quotaBytesToGiB(Number.MAX_SAFE_INTEGER)!;
const quotaGiB = computed<number | null>({
get: () => quotaBytesToGiB(quotaBytes.value),
set: (value) => {
const normalized = (value as number | string | null) === '' ? null : value;
quotaBytes.value = quotaGiBToBytes(normalized);
},
const isFocused = ref(false);
const quotaInput = ref(quotaBytesToGiBInput(quotaBytes.value));
watch(quotaBytes, (value) => {
if (!isFocused.value) {
quotaInput.value = quotaBytesToGiBInput(value);
}
});
function updateQuotaInput(value: unknown) {
const nextValue = typeof value === 'string' ? value : String(value ?? '');
quotaInput.value = nextValue;
const nextBytes = quotaGiBInputToBytes(nextValue);
if (nextBytes !== undefined) {
quotaBytes.value = nextBytes;
}
}
function onBlur() {
isFocused.value = false;
quotaInput.value = quotaBytesToGiBInput(quotaBytes.value);
}
</script>

37
src/app/utils/math.ts

@ -4,12 +4,45 @@ export function quotaBytesToGiB(bytes: number | null): number | null {
return bytes === null ? null : bytes / GIBIBYTE_IN_BYTES;
}
export function quotaBytesToGiBInput(bytes: number | null): string {
if (bytes === null) {
return '';
}
return quotaBytesToGiB(bytes)!
.toFixed(9)
.replace(/\.?0+$/, '');
}
export function quotaGiBToBytes(gibibytes: number | null): number | null {
if (gibibytes === null || gibibytes === 0) {
if (gibibytes === null || gibibytes <= 0) {
return null;
}
const bytes = Math.round(gibibytes * GIBIBYTE_IN_BYTES);
return bytes > 0 ? bytes : null;
}
export function quotaGiBInputToBytes(
gibibytes: string
): number | null | undefined {
const value = gibibytes.trim();
if (value === '') {
return null;
}
return Math.round(gibibytes * GIBIBYTE_IN_BYTES);
const quotaGiB = Number(value);
if (!Number.isFinite(quotaGiB) || quotaGiB < 0) {
return undefined;
}
const quotaBytes = quotaGiBToBytes(quotaGiB);
if (quotaBytes !== null && !Number.isSafeInteger(quotaBytes)) {
return undefined;
}
return quotaBytes;
}
export function bytes(

21
src/test/unit/math.app.spec.ts

@ -3,20 +3,28 @@ import { describe, expect, test } from 'vitest';
import {
GIBIBYTE_IN_BYTES,
quotaBytesToGiB,
quotaBytesToGiBInput,
quotaGiBInputToBytes,
quotaGiBToBytes,
} from '#app/utils/math';
describe('quota conversions', () => {
test('keeps disabled quotas null', () => {
expect(quotaBytesToGiB(null)).toBeNull();
expect(quotaBytesToGiBInput(null)).toBe('');
expect(quotaGiBInputToBytes('')).toBeNull();
expect(quotaGiBInputToBytes('0')).toBeNull();
expect(quotaGiBToBytes(null)).toBeNull();
expect(quotaGiBToBytes(0)).toBeNull();
expect(quotaGiBToBytes(0.1 / GIBIBYTE_IN_BYTES)).toBeNull();
});
test('converts whole and fractional GiB to bytes', () => {
expect(quotaGiBToBytes(1)).toBe(GIBIBYTE_IN_BYTES);
expect(quotaGiBToBytes(0.5)).toBe(GIBIBYTE_IN_BYTES / 2);
expect(quotaGiBToBytes(1 / GIBIBYTE_IN_BYTES)).toBe(1);
expect(quotaGiBInputToBytes('1')).toBe(GIBIBYTE_IN_BYTES);
expect(quotaGiBInputToBytes('0.5')).toBe(GIBIBYTE_IN_BYTES / 2);
});
test('rounds edited GiB values to the nearest byte', () => {
@ -27,6 +35,19 @@ describe('quota conversions', () => {
'round-trips the exact byte value %s',
(bytes) => {
expect(quotaGiBToBytes(quotaBytesToGiB(bytes))).toBe(bytes);
expect(quotaGiBInputToBytes(quotaBytesToGiBInput(bytes))).toBe(bytes);
}
);
test('formats quota input values without noisy floating point decimals', () => {
expect(quotaBytesToGiBInput(GIBIBYTE_IN_BYTES)).toBe('1');
expect(quotaBytesToGiBInput(GIBIBYTE_IN_BYTES / 2)).toBe('0.5');
expect(quotaBytesToGiBInput(107374182)).toBe('0.1');
});
test('ignores invalid quota input values', () => {
expect(quotaGiBInputToBytes('-1')).toBeUndefined();
expect(quotaGiBInputToBytes('invalid')).toBeUndefined();
expect(quotaGiBInputToBytes('9007199254740992')).toBeUndefined();
});
});

Loading…
Cancel
Save