Move input validation into lib

This commit is contained in:
Rūdolfs Ošiņš
2026-10-01 18:20:25 +02:00
parent 71c31e234c
commit a1c3622f05
5 changed files with 103 additions and 66 deletions
+7 -33
View File
@@ -1,12 +1,9 @@
<script lang="ts">
import type { Author } from "@bindings/cob/Author";
import { parseAssignee } from "@app/lib/inputValidation";
import { invoke } from "@app/lib/invoke";
import {
authorForNodeId,
parseNodeId,
publicKeyFromDid,
} from "@app/lib/utils";
import { authorForNodeId, publicKeyFromDid } from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
import Icon from "@app/components/Icon.svelte";
@@ -32,9 +29,9 @@
let updatedAssignees: Author[] = $state([]);
let showInput: boolean = $state(false);
let inputValue = $state("");
let validationMessage: string | undefined = $state(undefined);
let valid: boolean = $state(false);
let assignee: string | undefined = undefined;
const parsed = $derived(parseAssignee(inputValue, updatedAssignees));
const validationMessage = $derived(parsed.error);
const valid = $derived(parsed.error === undefined);
let removeToggles: Record<string, boolean> = $state({});
@@ -45,35 +42,12 @@
updatedAssignees = assignees;
showInput = false;
validationMessage = undefined;
valid = true;
removeToggles = {};
});
$effect(() => {
if (inputValue === "") {
validationMessage = "";
valid = true;
} else {
const parsedNodeId = parseNodeId(inputValue);
if (parsedNodeId) {
assignee = `${parsedNodeId.prefix}${parsedNodeId.pubkey}`;
if (updatedAssignees.find(({ did }) => did === assignee)) {
validationMessage = "This assignee is already added";
valid = false;
} else {
validationMessage = undefined;
valid = true;
}
} else {
validationMessage = "This is not a valid DID";
valid = false;
}
}
});
async function addAssignee() {
if (valid && assignee) {
const assignee = parsed.did;
if (assignee) {
const alias = await invoke<string | null>("alias", {
nid: publicKeyFromDid(assignee),
});
+4 -22
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { labelError } from "@app/lib/inputValidation";
import Button from "@app/components/Button.svelte";
import Icon from "@app/components/Icon.svelte";
import Label from "@app/components/Label.svelte";
@@ -23,10 +25,9 @@
let updatedLabels: string[] = $state([]);
let showInput: boolean = $state(false);
let inputValue = $state("");
let validationMessage: string | undefined = $state();
let valid: boolean = $state(false);
const sanitizedValue = $derived(inputValue.trim());
const validationMessage = $derived(labelError(inputValue, updatedLabels));
const valid = $derived(validationMessage === undefined);
let removeToggles: Record<string, boolean> = $state({});
@@ -37,28 +38,9 @@
updatedLabels = labels;
showInput = false;
validationMessage = undefined;
valid = true;
removeToggles = {};
});
$effect(() => {
if (inputValue !== "") {
if (sanitizedValue.length > 0) {
if (updatedLabels.includes(sanitizedValue)) {
valid = false;
validationMessage = "This label is already assigned";
} else {
valid = true;
validationMessage = undefined;
}
}
} else {
valid = true;
validationMessage = "";
}
});
function addLabel() {
if (valid && sanitizedValue) {
updatedLabels = [...updatedLabels, sanitizedValue].sort();
+37
View File
@@ -0,0 +1,37 @@
import { parseNodeId } from "@app/lib/utils";
export type AliasError =
| "AliasError.EmptyAlias"
| "AliasError.TooLongAlias"
| "AliasError.InvalidAlias";
export function aliasErrors(alias: string): AliasError[] {
const errors: AliasError[] = [];
if (alias.length === 0) errors.push("AliasError.EmptyAlias");
if (alias.length > 32) errors.push("AliasError.TooLongAlias");
if (alias.includes(" ")) errors.push("AliasError.InvalidAlias");
return errors;
}
export function labelError(
label: string,
labels: string[],
): string | undefined {
if (labels.includes(label.trim())) {
return "This label is already assigned";
}
}
export function parseAssignee(
input: string,
assignees: { did: string }[],
): { did?: string; error?: string } {
if (input === "") return {};
const nodeId = parseNodeId(input);
if (!nodeId) return { error: "This is not a valid DID" };
const did = `${nodeId.prefix}${nodeId.pubkey}`;
if (assignees.some(assignee => assignee.did === did)) {
return { error: "This assignee is already added" };
}
return { did };
}
+4 -11
View File
@@ -3,6 +3,7 @@
import debounce from "lodash/debounce";
import { aliasErrors } from "@app/lib/inputValidation";
import { invoke } from "@app/lib/invoke";
import * as router from "@app/lib/router";
import { createEventEmittersOnce } from "@app/lib/startup.svelte";
@@ -28,23 +29,15 @@
}, 400);
function validateInput(field: "alias" | "passphrase") {
if (field === "alias" && alias.length === 0) {
errors.alias.push({ code: "AliasError.EmptyAlias" });
}
if (field === "alias" && alias.length > 32) {
errors.alias.push({ code: "AliasError.TooLongAlias" });
}
if (field === "alias" && alias.includes(" ")) {
errors.alias.push({ code: "AliasError.InvalidAlias" });
if (field === "alias") {
errors.alias = aliasErrors(alias).map(code => ({ code }));
}
if (field === "passphrase" && passphrase.length === 0) {
errors.passphrase.push({ code: "PassphraseError.InvalidPassphrase" });
}
}
const validAlias = $derived(
alias.length > 0 && alias.length <= 32 && !alias.includes(" "),
);
const validAlias = $derived(aliasErrors(alias).length === 0);
const validPassphrase = $derived(
passphrase.length > 0 && passphrase === passphraseRepeat,
);
+51
View File
@@ -0,0 +1,51 @@
import { describe, expect, test } from "vitest";
import {
aliasErrors,
labelError,
parseAssignee,
} from "@app/lib/inputValidation";
const nid = "z6MkqGC3nWZhYieEVTVDKW5v588CiGfsDSmRVG9ZwwWTvLSK";
test.each([
["alice", []],
["", ["AliasError.EmptyAlias"]],
["a".repeat(32), []],
["a".repeat(33), ["AliasError.TooLongAlias"]],
["a b", ["AliasError.InvalidAlias"]],
[
`${"a ".repeat(17)}`,
["AliasError.TooLongAlias", "AliasError.InvalidAlias"],
],
])("aliasErrors(%j) is %j", (alias, expected) => {
expect(aliasErrors(alias)).toEqual(expected);
});
test("labelError flags a label that is already assigned", () => {
expect(labelError(" bug ", ["bug"])).toBe("This label is already assigned");
expect(labelError("feature", ["bug"])).toBeUndefined();
expect(labelError(" ", ["bug"])).toBeUndefined();
});
describe("parseAssignee", () => {
test("accepts a NID or a DID", () => {
expect(parseAssignee(nid, [])).toEqual({ did: `did:key:${nid}` });
expect(parseAssignee(`did:key:${nid}`, [])).toEqual({
did: `did:key:${nid}`,
});
});
test("rejects an assignee that is already added", () => {
expect(parseAssignee(nid, [{ did: `did:key:${nid}` }])).toEqual({
error: "This assignee is already added",
});
});
test("rejects anything else, but not an empty input", () => {
expect(parseAssignee("alice", [])).toEqual({
error: "This is not a valid DID",
});
expect(parseAssignee("", [])).toEqual({});
});
});