Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,852 changes: 2,136 additions & 1,716 deletions plugin/package-lock.json

Large diffs are not rendered by default.

50 changes: 25 additions & 25 deletions plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,41 +14,41 @@
"author": "Jamie Brynes",
"license": "ISC",
"dependencies": {
"@internationalized/date": "3.8.2",
"camelize-ts": "^3.0.0",
"classnames": "^2.5.1",
"framer-motion": "12.23.3",
"@internationalized/date": "3.10.0",
"camelize-ts": "3.0.0",
"classnames": "2.5.1",
"framer-motion": "12.23.25",
"obsidian": "1.7.2",
"react": "19.1.0",
"react-aria-components": "1.10.1",
"react-dom": "19.1.0",
"react": "19.2.1",
"react-aria-components": "1.13.0",
"react-dom": "19.2.1",
"react-textarea-autosize": "8.5.9",
"snakify-ts": "^2.3.0",
"tslib": "^2.4.1",
"yaml": "^2.1.3",
"zod": "^3.24.1",
"zustand": "5.0.6"
"snakify-ts": "2.3.0",
"tslib": "2.8.1",
"yaml": "2.8.2",
"zod": "4.1.13",
"zustand": "5.0.9"
},
"devDependencies": {
"@biomejs/biome": "2.1.1",
"@rollup/plugin-replace": "6.0.2",
"@testing-library/jest-dom": "^6.4.2",
"@biomejs/biome": "2.3.8",
"@rollup/plugin-replace": "6.0.3",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.0",
"@types/node": "24.0.13",
"@types/react-dom": "19.1.0",
"@types/react-dom": "19.2.1",
"@typescript/native-preview": "^7.0.0-dev.20251130.1",
"@vitejs/plugin-react": "4.6.0",
"jsdom": "^24.0.0",
"sass": "^1.71.1",
"typescript": "^5.3.3",
"vite": "7.0.4",
"vite-node": "^3.2.4",
"vite-plugin-static-copy": "3.1.1",
"@vitejs/plugin-react": "5.1.2",
"jsdom": "27.3.0",
"sass": "1.96.0",
"typescript": "5.9.3",
"vite": "7.2.7",
"vite-node": "5.2.0",
"vite-plugin-static-copy": "3.1.4",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4"
"vitest": "4.0.15"
},
"optionalDependencies": {
"@biomejs/cli-linux-x64-musl": "2.1.1"
"@biomejs/cli-linux-x64-musl": "2.3.8"
},
"overrides": {
"rollup": "4.44.2"
Expand Down
104 changes: 104 additions & 0 deletions plugin/src/query/__snapshots__/parser.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`parseQuery - error message snapshots > array with mixed valid and invalid enum values 1`] = `
[Error: Field 'sorting' elements have the following issues:
Item 'sorting[1]': Invalid option: expected one of "priority"|"priorityAscending"|"priorityDescending"|"date"|"dateAscending"|"dateDescending"|"order"|"dateAdded"|"dateAddedAscending"|"dateAddedDescending"]
`;

exports[`parseQuery - error message snapshots > autorefresh must be a number 1`] = `
[Error: Field 'autorefresh' has the following issues:
Invalid input: expected number, received string]
`;

exports[`parseQuery - error message snapshots > autorefresh must be non-negative 1`] = `
[Error: Field 'autorefresh' has the following issues:
Too small: expected number to be >=0]
`;

exports[`parseQuery - error message snapshots > filter must be a string 1`] = `
[Error: Field 'filter' has the following issues:
Invalid input: expected string, received number]
`;

exports[`parseQuery - error message snapshots > groupBy must be a string 1`] = `
[Error: Field 'groupBy' has the following issues:
Invalid option: expected one of "project"|"section"|"priority"|"due"|"date"|"labels"]
`;

exports[`parseQuery - error message snapshots > groupBy must have valid enum value 1`] = `
[Error: Field 'groupBy' has the following issues:
Invalid option: expected one of "project"|"section"|"priority"|"due"|"date"|"labels"]
`;

exports[`parseQuery - error message snapshots > invalid JSON - missing quotes 1`] = `
[Error: Field 'filter' has the following issues:
Invalid input: expected string, received undefined]
`;

exports[`parseQuery - error message snapshots > invalid JSON - unclosed brace 1`] = `[Error: Unable to parse as YAML or JSON]`;

exports[`parseQuery - error message snapshots > invalid YAML - incorrect indentation 1`] = `[Error: Unable to parse as YAML or JSON]`;

exports[`parseQuery - error message snapshots > missing required filter field 1`] = `
[Error: Field 'filter' has the following issues:
Invalid input: expected string, received undefined]
`;

exports[`parseQuery - error message snapshots > multiple validation errors 1`] = `
[Error: Field 'name' has the following issues:
Invalid input: expected string, received number
Field 'filter' has the following issues:
Invalid input: expected string, received undefined
Field 'autorefresh' has the following issues:
Too small: expected number to be >=0
Field 'sorting' has the following issues:
Invalid input: expected array, received string]
`;

exports[`parseQuery - error message snapshots > name must be a string 1`] = `
[Error: Field 'name' has the following issues:
Invalid input: expected string, received number]
`;

exports[`parseQuery - error message snapshots > neither valid JSON nor YAML 1`] = `[Error: Invalid input: expected object, received string]`;

exports[`parseQuery - error message snapshots > show array must contain strings 1`] = `
[Error: Field 'show' has the following issues:
Invalid input: expected "none"
Field 'show' elements have the following issues:
Item 'show[0]': Invalid option: expected one of "due"|"date"|"description"|"labels"|"project"|"deadline"
Item 'show[1]': Invalid option: expected one of "due"|"date"|"description"|"labels"|"project"|"deadline"
Item 'show[2]': Invalid option: expected one of "due"|"date"|"description"|"labels"|"project"|"deadline"]
`;

exports[`parseQuery - error message snapshots > show field - invalid literal (not 'none') 1`] = `
[Error: Field 'show' has the following issues:
Invalid input: expected array, received string
Invalid input: expected "none"]
`;

exports[`parseQuery - error message snapshots > show must have valid enum values 1`] = `
[Error: Field 'show' has the following issues:
Invalid input: expected "none"
Field 'show' elements have the following issues:
Item 'show[0]': Invalid option: expected one of "due"|"date"|"description"|"labels"|"project"|"deadline"
Item 'show[1]': Invalid option: expected one of "due"|"date"|"description"|"labels"|"project"|"deadline"]
`;

exports[`parseQuery - error message snapshots > sorting array must contain strings 1`] = `
[Error: Field 'sorting' elements have the following issues:
Item 'sorting[0]': Invalid option: expected one of "priority"|"priorityAscending"|"priorityDescending"|"date"|"dateAscending"|"dateDescending"|"order"|"dateAdded"|"dateAddedAscending"|"dateAddedDescending"
Item 'sorting[1]': Invalid option: expected one of "priority"|"priorityAscending"|"priorityDescending"|"date"|"dateAscending"|"dateDescending"|"order"|"dateAdded"|"dateAddedAscending"|"dateAddedDescending"
Item 'sorting[2]': Invalid option: expected one of "priority"|"priorityAscending"|"priorityDescending"|"date"|"dateAscending"|"dateDescending"|"order"|"dateAdded"|"dateAddedAscending"|"dateAddedDescending"]
`;

exports[`parseQuery - error message snapshots > sorting must be an array 1`] = `
[Error: Field 'sorting' has the following issues:
Invalid input: expected array, received string]
`;

exports[`parseQuery - error message snapshots > sorting must have valid enum values 1`] = `
[Error: Field 'sorting' elements have the following issues:
Item 'sorting[0]': Invalid option: expected one of "priority"|"priorityAscending"|"priorityDescending"|"date"|"dateAscending"|"dateDescending"|"order"|"dateAdded"|"dateAddedAscending"|"dateAddedDescending"
Item 'sorting[1]': Invalid option: expected one of "priority"|"priorityAscending"|"priorityDescending"|"date"|"dateAscending"|"dateDescending"|"order"|"dateAdded"|"dateAddedAscending"|"dateAddedDescending"]
`;
92 changes: 92 additions & 0 deletions plugin/src/query/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,95 @@ describe("parseQuery - warnings", () => {
});
}
});

describe("parseQuery - error message snapshots", () => {
type ErrorTestCase = {
description: string;
input: string;
};

const testcases: ErrorTestCase[] = [
{
description: "invalid JSON - missing quotes",
input: "{name: foo}",
},
{
description: "invalid JSON - unclosed brace",
input: '{"filter": "bar"',
},
{
description: "invalid YAML - incorrect indentation",
input: "filter: bar\n name: foo\n name: baz",
},
{
description: "neither valid JSON nor YAML",
input: "this is not valid at all {{",
},
{
description: "missing required filter field",
input: '{"name": "foo"}',
},
{
description: "name must be a string",
input: '{"name": 123, "filter": "bar"}',
},
{
description: "filter must be a string",
input: '{"filter": 123}',
},
{
description: "autorefresh must be a number",
input: '{"filter": "bar", "autorefresh": "not a number"}',
},
{
description: "autorefresh must be non-negative",
input: '{"filter": "bar", "autorefresh": -5}',
},
{
description: "sorting must be an array",
input: '{"filter": "bar", "sorting": "not an array"}',
},
{
description: "sorting array must contain strings",
input: '{"filter": "bar", "sorting": [1, 2, 3]}',
},
{
description: "sorting must have valid enum values",
input: '{"filter": "bar", "sorting": ["invalid", "values"]}',
},
{
description: "groupBy must be a string",
input: '{"filter": "bar", "groupBy": 123}',
},
{
description: "groupBy must have valid enum value",
input: '{"filter": "bar", "groupBy": "invalid"}',
},
{
description: "show array must contain strings",
input: '{"filter": "bar", "show": [1, 2, 3]}',
},
{
description: "show must have valid enum values",
input: '{"filter": "bar", "show": ["invalid", "values"]}',
},
{
description: "show field - invalid literal (not 'none')",
input: '{"filter": "bar", "show": "nonee"}',
},
{
description: "multiple validation errors",
input: '{"name": 123, "autorefresh": -1, "sorting": "invalid"}',
},
{
description: "array with mixed valid and invalid enum values",
input: '{"filter": "bar", "sorting": ["date", "invalid", "priority"]}',
},
];

for (const tc of testcases) {
it(tc.description, () => {
expect(() => parseQuery(tc.input)).toThrowErrorMatchingSnapshot();
});
}
});
63 changes: 40 additions & 23 deletions plugin/src/query/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,23 @@ import { GroupVariant, type Query, ShowMetadataVariant, SortingVariant } from "@

type ErrorTree = string | { msg: string; children: ErrorTree[] };

function formatErrorTree(tree: ErrorTree, indent = ""): string {
if (typeof tree === "string") {
return `${indent}${tree}`;
}
const lines = [`${indent}${tree.msg}`];
for (const child of tree.children) {
lines.push(formatErrorTree(child, `${indent} `));
}
return lines.join("\n");
}

export class ParsingError extends Error {
messages: ErrorTree[];
inner: unknown | undefined;

constructor(msgs: ErrorTree[], inner: unknown | undefined = undefined) {
super(msgs.join("\n"));
super(msgs.map((tree) => formatErrorTree(tree)).join("\n"));
this.inner = inner;
this.messages = msgs;
}
Expand Down Expand Up @@ -70,7 +81,6 @@ function tryParseAsYaml(raw: string): Record<string, unknown> {

const lookupToEnum = <T>(lookup: Record<string, T>) => {
const keys = Object.keys(lookup);
//@ts-ignore: There is at least one element for these.
return z.enum(keys).transform((key) => lookup[key]);
};

Expand Down Expand Up @@ -164,27 +174,34 @@ function parseObjectZod(query: Record<string, unknown>): [Query, QueryWarning[]]
];
}

function formatZodError(error: z.ZodError): ErrorTree[] {
return error.errors.map((err) => {
const field = formatPath(err.path);
switch (err.code) {
case "invalid_type":
return `Field '${field}' is ${err.received === "undefined" ? "required" : `must be a ${err.expected}`}`;
case "invalid_enum_value":
return `Field '${field}' has invalid value '${err.received}'. Valid options are: ${err.options?.join(", ")}`;
case "invalid_union":
return {
msg: "One of the following rules must be met:",
children: err.unionErrors.flatMap(formatZodError),
};
case "invalid_literal":
return `Field '${field}' has invalid value '${err.received}', must be exactly '${err.expected}'`;
default:
return `Field '${field}': ${err.message}`;
type QuerySchema = z.infer<typeof querySchema>;

function formatZodError(error: z.ZodError<QuerySchema>): ErrorTree[] {
const tree = z.treeifyError(error);

const errors: ErrorTree[] = [...tree.errors];
if (tree.properties === undefined) {
return errors;
}

for (const [key, child] of Object.entries(tree.properties)) {
if (child.errors.length > 0) {
errors.push({
msg: `Field '${key}' has the following issues:`,
children: child.errors,
});
}
});
}

function formatPath(path: (string | number)[]): string {
return path.map((p) => (typeof p === "number" ? `[${p}]` : p)).join(".");
if ("items" in child && child.items !== undefined) {
const root: ErrorTree = {
msg: `Field '${key}' elements have the following issues:`,
children: child.items.flatMap((item, idx) =>
item.errors.map((msg) => `Item '${key}[${idx}]': ${msg}`),
),
};
errors.push(root);
}
}

return errors;
}
4 changes: 2 additions & 2 deletions plugin/src/styles/main.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import "colors";
@use "colors";

body {
@each $name, $color in $todoist-colors {
@each $name, $color in colors.$todoist-colors {
--todoist-#{$name}: #{$color};
}
}
Expand Down
Loading