Skip to content

Commit

Permalink
feat(settings): Bulk add transitive rules
Browse files Browse the repository at this point in the history
  • Loading branch information
SkepticMystic committed May 1, 2024
1 parent 7ee38ba commit 7ff3cd5
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 98 deletions.
114 changes: 25 additions & 89 deletions src/codeblocks/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { EdgeField, EdgeFieldGroup } from "src/interfaces/settings";
import { remove_duplicates } from "src/utils/arrays";
import { resolve_field_group_labels } from "src/utils/edge_fields";
import { Mermaid } from "src/utils/mermaid";
import { quote_join } from "src/utils/strings";
import { zod } from "src/utils/zod";
import { z } from "zod";

const FIELDS = [
Expand Down Expand Up @@ -33,62 +33,8 @@ type InputData = {
field_groups: EdgeFieldGroup[];
};

const zod_not_string_msg = (field: CodeblockField, received: unknown) =>
`Expected a string (text), but got: \`${received}\` (${typeof received}). _Try wrapping the value in quotes._
**Example**: \`${field}: "${received}"\``;

const zod_invalid_enum_msg = (
field: CodeblockField,
options: any[] | readonly any[],
received: unknown,
) =>
`Expected one of the following options: ${quote_join(options, "`", ", or ")}, but got: \`${received}\`.
**Example**: \`${field}: ${options[0]}\``;

const zod_not_array_msg = (
field: CodeblockField,
options: any[] | readonly any[],
received: unknown,
) =>
`This field is now expected to be a YAML list (array), but got: \`${received}\` (${typeof received}). _Try wrapping it in square brackets._
**Example**: \`${field}: [${options.slice(0, 2).join(", ")}]\`, or possibly: \`${field}: [${received}]\``;

const dynamic_enum_schema = (
options: string[],
/** Optionally override ctx.path (useful in the sort.order/sort.field case) */
path?: CodeblockField,
) =>
z.string().superRefine((received, ctx) => {
if (options.includes(received)) {
return true;
} else {
ctx.addIssue({
options,
received: received,
code: "invalid_enum_value",
// NOTE: Leave the default path on _this_ object, but pass the override into the error message
message: zod_invalid_enum_msg(
path ?? (ctx.path.join(".") as CodeblockField),
options,
received,
),
});

return false;
}
});

const BOOLEANS = [true, false] as const;

const dynamic_enum_array_schema = (
field: CodeblockField,
options: string[],
received: unknown,
) =>
z.array(dynamic_enum_schema(options), {
invalid_type_error: zod_not_array_msg(field, options, received),
});

const build = (input: Record<string, unknown>, data: InputData) => {
const field_labels = data.edge_fields.map((f) => f.label);
const group_labels = data.field_groups.map((f) => f.label);
Expand All @@ -97,17 +43,13 @@ const build = (input: Record<string, unknown>, data: InputData) => {
.object({
title: z
.string({
message: zod_not_string_msg(
//
"title",
input["title"],
),
message: zod.error.not_string("title", input["title"]),
})
.optional(),

"start-note": z
.string({
message: zod_not_string_msg(
message: zod.error.not_string(
"start-note",
input["start-note"],
),
Expand All @@ -116,7 +58,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {

"dataview-from": z
.string({
message: zod_not_string_msg(
message: zod.error.not_string(
"dataview-from",
input["dataview-from"],
),
Expand All @@ -125,7 +67,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {

flat: z
.boolean({
message: zod_invalid_enum_msg(
message: zod.error.invalid_enum(
"flat",
BOOLEANS,
input["flat"],
Expand All @@ -135,7 +77,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {

collapse: z
.boolean({
message: zod_invalid_enum_msg(
message: zod.error.invalid_enum(
"collapse",
BOOLEANS,
input["collapse"],
Expand All @@ -145,7 +87,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {

"merge-fields": z
.boolean({
message: zod_invalid_enum_msg(
message: zod.error.invalid_enum(
"merge-fields",
BOOLEANS,
input["merge-fields"],
Expand All @@ -155,7 +97,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {

content: z
.enum(["open", "closed"], {
message: zod_invalid_enum_msg(
message: zod.error.invalid_enum(
"content",
["open", "closed"],
input["content"],
Expand All @@ -165,7 +107,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {

type: z
.enum(["tree", "mermaid"], {
message: zod_invalid_enum_msg(
message: zod.error.invalid_enum(
"type",
["tree", "mermaid"],
input["type"],
Expand All @@ -175,7 +117,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {

"mermaid-renderer": z
.enum(Mermaid.RENDERERS, {
message: zod_invalid_enum_msg(
message: zod.error.invalid_enum(
"mermaid-renderer",
Mermaid.RENDERERS,
input["mermaid-renderer"],
Expand All @@ -184,7 +126,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {
.optional(),
"mermaid-direction": z
.enum(Mermaid.DIRECTIONS, {
message: zod_invalid_enum_msg(
message: zod.error.invalid_enum(
"mermaid-direction",
Mermaid.DIRECTIONS,
input["mermaid-direction"],
Expand All @@ -193,7 +135,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {
.optional(),
"mermaid-curve": z
.enum(Mermaid.CURVE_STYLES, {
message: zod_invalid_enum_msg(
message: zod.error.invalid_enum(
"mermaid-curve",
Mermaid.CURVE_STYLES,
input["mermaid-curve"],
Expand All @@ -203,25 +145,25 @@ const build = (input: Record<string, unknown>, data: InputData) => {

"show-attributes": z
.array(z.enum(EDGE_ATTRIBUTES), {
message: zod_not_array_msg(
message: zod.error.not_array(
"show-attributes",
EDGE_ATTRIBUTES,
input["show-attributes"],
),
})
.optional(),

fields: dynamic_enum_array_schema(
"fields",
field_labels,
input["fields"],
).optional(),
fields: zod.schema
.dynamic_enum_array("fields", field_labels, input["fields"])
.optional(),

"field-groups": dynamic_enum_array_schema(
"field-groups",
group_labels,
input["field-groups"],
).optional(),
"field-groups": zod.schema
.dynamic_enum_array(
"field-groups",
group_labels,
input["field-groups"],
)
.optional(),

depth: z
.array(
Expand Down Expand Up @@ -282,7 +224,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {
z.object({
// TODO: Use a custom zod schema to retain string template literals here
// https://github.com/colinhacks/zod?tab=readme-ov-file#custom-schemas
field: dynamic_enum_schema(
field: zod.schema.dynamic_enum(
[
...SIMPLE_EDGE_SORT_FIELDS,
...data.edge_fields.map(
Expand All @@ -306,7 +248,7 @@ const build = (input: Record<string, unknown>, data: InputData) => {
{
// SOURCE: https://github.com/colinhacks/zod/issues/117#issuecomment-1595801389
errorMap: (_err, ctx) => ({
message: zod_invalid_enum_msg(
message: zod.error.invalid_enum(
"sort.order" as CodeblockField,
["asc", "desc"],
ctx.data,
Expand Down Expand Up @@ -367,12 +309,6 @@ export const CodeblockSchema = {
FIELDS,

build,

error: {
zod_not_array_msg,
zod_not_string_msg,
zod_invalid_enum_msg,
},
};

export type ICodeblock = {
Expand Down
86 changes: 85 additions & 1 deletion src/components/settings/TransitiveImpliedRelations.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
import { Menu, Notice } from "obsidian";
import { ICON_SIZE } from "src/const";
import type { EdgeField } from "src/interfaces/settings";
import { log } from "src/logger";
import type BreadcrumbsPlugin from "src/main";
import { Mermaid } from "src/utils/mermaid";
import { split_and_trim } from "src/utils/strings";
import {
get_transitive_rule_name,
input_transitive_rule_schema,
parse_transitive_relation,
stringify_transitive_relation,
transitive_rule_to_edges,
} from "src/utils/transitive_rules";
Expand Down Expand Up @@ -74,6 +78,63 @@
settings.is_dirty = true;
},
add_bulk: () => {
const textarea = document.getElementById(
"BC-transitive-bulk-str",
) as HTMLTextAreaElement | null;
if (!textarea) return new Notice("Could not find textarea.");
const value = textarea.value.trim();
if (!value) return new Notice("No rules to parse.");
const lines = split_and_trim(value, "\n").filter(Boolean);
const parsed = lines
.map(parse_transitive_relation)
.filter((r) => r.ok) as Extract<
ReturnType<typeof parse_transitive_relation>,
{ ok: true }
>[];
if (parsed.length !== lines.length) {
return new Notice(
"Some rules could not be parsed. Ensure you're using the correct syntax of `[field-one, field-two] -> close-field`, with each rule of a new line.",
);
}
const validated = parsed.map((r) =>
input_transitive_rule_schema({
fields: plugin.settings.edge_fields,
}).safeParse(r.data),
);
const validation_errors = validated.filter((r) => !r.success);
if (validation_errors.length) {
log.error(
"Bulk-add transitive rule errors >",
validation_errors.map((r) =>
r.success ? null : r.error?.issues,
),
);
return new Notice(
"Some rules could not be parsed. Check the logs for more information.",
);
}
validated.forEach((r) => {
if (r.success) {
transitives.push({ ...r.data, name: "", rounds: 10 });
}
});
new Notice(`Bulk added ${validated.length} rules ✅`);
transitives = transitives;
settings.is_dirty = true;
},
copy_transitive: (i: number) => {
const new_length = transitives.push({
...transitives[i],
Expand Down Expand Up @@ -211,7 +272,7 @@

{#if transitives.length > 3}
<button
class="w-8"
class="w-10"
aria-label="Jump to bottom"
on:click={() => actions.scroll_to(transitives.length - 1)}
>
Expand Down Expand Up @@ -397,6 +458,29 @@
<PlusIcon size={ICON_SIZE} />
Add New Transitive Implied Relation
</button>

<details open>
<summary>Bulk Add Rules (Advanced)</summary>

<div class="flex flex-col gap-1">
<p>
Quickly add multiple rules using the shorthand syntax: <code
>
[field-one, field-two] -> close-field
</code>. Each rule should be on a new line.
</p>

<textarea
id="BC-transitive-bulk-str"
class="h-32 w-60"
placeholder="[up] <- down"
></textarea>

<button class="w-60" on:click={actions.add_bulk}>
Bulk Add
</button>
</div>
</details>
</div>
</div>

Expand Down
Loading

0 comments on commit 7ff3cd5

Please sign in to comment.