diff --git a/apps/roam/src/components/settings/utils/zodSchema.example.ts b/apps/roam/src/components/settings/utils/zodSchema.example.ts new file mode 100644 index 000000000..89df7ee22 --- /dev/null +++ b/apps/roam/src/components/settings/utils/zodSchema.example.ts @@ -0,0 +1,454 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { + CanvasSettings, + SuggestiveRules, + DiscourseNodeSettings, + FeatureFlags, + ExportSettings, + PageGroup, + SuggestiveModeGlobalSettings, + LeftSidebarGlobalSettings, + GlobalSettings, + PersonalSection, + LeftSidebarPersonalSettings, + StoredFilters, + QuerySettings, + PersonalSettings, + GithubSettings, + QueryCondition, + QuerySelection, + RoamNodeType, +} from "./zodSchema"; + +const canvasSettings: CanvasSettings = { + color: "#4A90D9", + alias: "CLM", + "key-image": true, + "key-image-option": "query-builder", + "query-builder-alias": "Key Image Query", +}; + +const suggestiveRules: SuggestiveRules = { + template: [ + { text: "Summary::", heading: 2 }, + { text: "Key Points::", heading: 2, children: [{ text: "" }] }, + ], + embeddingRef: "((block-uid-123))", + isFirstChild: { + uid: "first-child-uid", + value: true, + }, +}; + +const discourseNodeSettings: DiscourseNodeSettings = { + text: "Claim", + uid: "_CLM-node", + format: "[[CLM]] - {content}", + shortcut: "C", + tag: "#claim", + description: "A statement or assertion that can be supported or refuted", + specification: [ + { + uid: "spec-uid-1", + type: "clause", + source: "Claim", + relation: "has title", + target: "/^\\[\\[CLM\\]\\]/", + }, + ], + template: [ + { text: "Summary::", heading: 2 }, + { text: "Evidence::", heading: 2, children: [{ text: "" }] }, + { text: "Counterarguments::", heading: 2, children: [{ text: "" }] }, + ], + canvasSettings: { + color: "#4A90D9", + alias: "CLM", + }, + graphOverview: true, + attributes: { + Status: "status-attr-uid", + Confidence: "confidence-attr-uid", + }, + overlay: "Status", + index: [ + { + type: "filter", + condition: "has attribute", + attribute: "Status", + }, + ], + suggestiveRules: { + template: [], + embeddingRef: "((embed-ref))", + isFirstChild: { + uid: "is-first-child-uid", + value: false, + }, + }, + embeddingRef: "((main-embed-ref))", + isFirstChild: { + uid: "main-first-child-uid", + value: true, + }, + backedBy: "user", +}; + +const featureFlags: FeatureFlags = { + "Enable Left Sidebar": true, + "Suggestive Mode Enabled": true, + "Reified Relation Triples": false, +}; + +const defaultFeatureFlags: FeatureFlags = { + "Enable Left Sidebar": false, + "Suggestive Mode Enabled": false, + "Reified Relation Triples": false, +}; + +const exportSettings: ExportSettings = { + "Remove Special Characters": true, + "Resolve Block References": true, + "Resolve Block Embeds": false, + "Append Referenced Node": true, + "Link Type": "wikilinks", + "Max Filename Length": 128, + Frontmatter: [ + "title: {{page-title}}", + "date: {{date}}", + "tags: {{tags}}", + "type: discourse-node", + ], +}; + +const pageGroup: PageGroup = { + name: "Research Papers", + pages: ["page-uid-1", "page-uid-2", "page-uid-3"], +}; + +const suggestiveModeGlobalSettings: SuggestiveModeGlobalSettings = { + "Include Current Page Relations": true, + "Include Parent And Child Blocks": true, + "Page Groups": [ + { + name: "Research Papers", + pages: ["paper-1-uid", "paper-2-uid"], + }, + { + name: "Meeting Notes", + pages: ["meeting-1-uid", "meeting-2-uid", "meeting-3-uid"], + }, + ], +}; + +const leftSidebarGlobalSettings: LeftSidebarGlobalSettings = { + Children: ["daily-notes-uid", "quick-capture-uid", "inbox-uid"], + Settings: { + Collapsable: true, + Folded: false, + }, +}; + +const globalSettings: GlobalSettings = { + Trigger: ";;", + "Canvas Page Format": "Canvas - {date} - {title}", + "Left Sidebar": { + Children: ["daily-notes-uid", "quick-capture-uid", "inbox-uid"], + Settings: { + Collapsable: true, + Folded: false, + }, + }, + Export: { + "Remove Special Characters": true, + "Resolve Block References": true, + "Resolve Block Embeds": false, + "Append Referenced Node": true, + "Link Type": "wikilinks", + "Max Filename Length": 128, + Frontmatter: ["title: {{page-title}}", "date: {{date}}"], + }, + "Suggestive Mode": { + "Include Current Page Relations": true, + "Include Parent And Child Blocks": true, + "Page Groups": [ + { + name: "Research", + pages: ["research-uid-1", "research-uid-2"], + }, + ], + }, +}; + +const defaultGlobalSettings: GlobalSettings = { + Trigger: "", + "Canvas Page Format": "", + "Left Sidebar": { + Children: [], + Settings: { + Collapsable: false, + Folded: false, + }, + }, + Export: { + "Remove Special Characters": false, + "Resolve Block References": false, + "Resolve Block Embeds": false, + "Append Referenced Node": false, + "Link Type": "alias", + "Max Filename Length": 64, + Frontmatter: [], + }, + "Suggestive Mode": { + "Include Current Page Relations": false, + "Include Parent And Child Blocks": false, + "Page Groups": [], + }, +}; + +const personalSection: PersonalSection = { + Children: [ + { Page: "daily-notes-uid", Alias: "Daily Notes" }, + { Page: "inbox-uid", Alias: "Inbox" }, + { Page: "projects-uid", Alias: "" }, + ], + Settings: { + "Truncate-result?": 100, + Folded: false, + }, +}; + +const leftSidebarPersonalSettings: LeftSidebarPersonalSettings = { + "My Workspace": { + Children: [ + { Page: "daily-notes-uid", Alias: "Daily Notes" }, + { Page: "inbox-uid", Alias: "Inbox" }, + ], + Settings: { + "Truncate-result?": 75, + Folded: false, + }, + }, + Research: { + Children: [ + { Page: "papers-uid", Alias: "Papers" }, + { Page: "notes-uid", Alias: "Notes" }, + { Page: "ideas-uid", Alias: "Ideas" }, + ], + Settings: { + "Truncate-result?": 50, + Folded: true, + }, + }, +}; + +const storedFilters: StoredFilters = { + includes: { values: ["Claim", "Evidence"] }, + excludes: { values: ["archived"] }, +}; + +const querySettings: QuerySettings = { + "Hide Query Metadata": true, + "Default Page Size": 25, + "Query Pages": ["query-page-uid-1", "query-page-uid-2"], + "Default Filters": { + "node-type": { + includes: { values: ["Claim"] }, + excludes: { values: [] }, + }, + status: { + includes: { values: [] }, + excludes: { values: ["archived"] }, + }, + }, +}; + +const personalSettings: PersonalSettings = { + "Left Sidebar": { + "My Workspace": { + Children: [ + { Page: "daily-notes-uid", Alias: "Daily Notes" }, + { Page: "inbox-uid", Alias: "Inbox" }, + ], + Settings: { + "Truncate-result?": 75, + Folded: false, + }, + }, + Research: { + Children: [ + { Page: "papers-uid", Alias: "Papers" }, + { Page: "notes-uid", Alias: "Notes" }, + ], + Settings: { + "Truncate-result?": 50, + Folded: true, + }, + }, + }, + "Personal Node Menu Trigger": ";;", + "Node Search Menu Trigger": "//", + "Discourse Tool Shortcut": "d", + "Discourse Context Overlay": true, + "Suggestive Mode Overlay": true, + "Overlay in Canvas": false, + "Text Selection Popup": true, + "Disable Sidebar Open": false, + "Page Preview": true, + "Hide Feedback Button": false, + "Streamline Styling": true, + "Auto Canvas Relations": true, + "Disable Product Diagnostics": false, + Query: { + "Hide Query Metadata": true, + "Default Page Size": 25, + "Query Pages": ["query-page-uid-1"], + "Default Filters": { + "node-type": { + includes: { values: ["Claim"] }, + excludes: { values: [] }, + }, + }, + }, +}; + +const defaultPersonalSettings: PersonalSettings = { + "Left Sidebar": {}, + "Personal Node Menu Trigger": "", + "Node Search Menu Trigger": "", + "Discourse Tool Shortcut": "", + "Discourse Context Overlay": false, + "Suggestive Mode Overlay": false, + "Overlay in Canvas": false, + "Text Selection Popup": true, + "Disable Sidebar Open": false, + "Page Preview": false, + "Hide Feedback Button": false, + "Streamline Styling": false, + "Auto Canvas Relations": false, + "Disable Product Diagnostics": false, + Query: { + "Hide Query Metadata": false, + "Default Page Size": 10, + "Query Pages": [], + "Default Filters": {}, + }, +}; + +const githubSettings: GithubSettings = { + "oauth-github": "ghp_xxxxxxxxxxxxxxxxxxxx", + "selected-repo": "username/repository-name", +}; + +const clauseCondition: QueryCondition = { + uid: "clause-uid-1", + type: "clause", + source: "node", + relation: "is a", + target: "Claim", +}; + +const notCondition: QueryCondition = { + uid: "not-uid-1", + type: "not", + source: "node", + relation: "has attribute", + target: "Archived", +}; + +const orCondition: QueryCondition = { + uid: "or-uid-1", + type: "or", + conditions: [ + [ + { + uid: "clause-uid-2", + type: "clause", + source: "node", + relation: "has attribute", + target: "High Priority", + }, + ], + [ + { + uid: "clause-uid-3", + type: "clause", + source: "node", + relation: "has attribute", + target: "Urgent", + }, + ], + ], +}; + +const norCondition: QueryCondition = { + uid: "nor-uid-1", + type: "not or", + conditions: [ + [ + { + uid: "clause-uid-4", + type: "clause", + source: "node", + relation: "is a", + target: "Draft", + }, + ], + [ + { + uid: "clause-uid-5", + type: "clause", + source: "node", + relation: "is a", + target: "Archived", + }, + ], + ], +}; + +const exampleConditions: QueryCondition[] = [ + clauseCondition, + notCondition, + orCondition, + norCondition, +]; + +const titleSelection: QuerySelection = { + uid: "sel-uid-1", + text: "node", + label: "Title", +}; + +const dateSelection: QuerySelection = { + uid: "sel-uid-2", + text: "Created Date", + label: "Created", +}; + +const exampleSelections: QuerySelection[] = [titleSelection, dateSelection]; + +const simpleNode: RoamNodeType = { + text: "A simple block", +}; + +const nodeWithHeading: RoamNodeType = { + text: "Section Title", + heading: 1, +}; + +const nodeWithChildren: RoamNodeType = { + text: "Steps:", + children: [ + { text: "First step" }, + { text: "Second step" }, + { text: "Third step" }, + ], +}; + +const fullTemplateExample: RoamNodeType[] = [ + { text: "Meeting Notes", heading: 1 }, + { text: "Attendees::" }, + { text: "Agenda::", heading: 2, children: [{ text: "" }] }, + { text: "Discussion::", heading: 2 }, + { text: "Action Items::", heading: 2, children: [{ text: "" }] }, +]; diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts new file mode 100644 index 000000000..8c4bb1705 --- /dev/null +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -0,0 +1,269 @@ +import { z } from "zod"; + +/* eslint-disable @typescript-eslint/naming-convention */ + +export const CanvasSettingsSchema = z.object({ + color: z.string().default(""), + alias: z.string().default(""), + "key-image": z.boolean().default(false), + "key-image-option": z + .enum(["first-image", "query-builder"]) + .default("first-image"), + "query-builder-alias": z.string().default(""), +}); + +export const SuggestiveRulesSchema = z.lazy(() => + z.object({ + template: z.array(RoamNodeSchema).default([]), + embeddingRef: z.string().optional(), + isFirstChild: z + .object({ + uid: z.string(), + value: z.boolean(), + }) + .optional(), + }), +); + +export const AttributesSchema = z.record(z.string(), z.string()).default({}); + +const QBClauseDataSchema = z.object({ + uid: z.string(), + source: z.string(), + relation: z.string(), + target: z.string(), + not: z.boolean().optional(), +}); + +type Condition = + | (z.infer & { type: "clause" }) + | (z.infer & { type: "not" }) + | { uid: string; type: "or"; conditions: Condition[][] } + | { uid: string; type: "not or"; conditions: Condition[][] }; + +export const ConditionSchema: z.ZodType = z.discriminatedUnion( + "type", + [ + QBClauseDataSchema.extend({ type: z.literal("clause") }), + QBClauseDataSchema.extend({ type: z.literal("not") }), + z.object({ + uid: z.string(), + type: z.literal("or"), + conditions: z.lazy(() => ConditionSchema.array().array()), + }), + z.object({ + uid: z.string(), + type: z.literal("not or"), + conditions: z.lazy(() => ConditionSchema.array().array()), + }), + ], +); + +export const SelectionSchema = z.object({ + uid: z.string(), + text: z.string(), + label: z.string(), +}); + +type RoamNode = { + text: string; + children?: RoamNode[]; + uid?: string; + heading?: 0 | 1 | 2 | 3; + open?: boolean; +}; + +export const RoamNodeSchema: z.ZodType = z.lazy(() => + z.object({ + text: z.string(), + children: RoamNodeSchema.array().optional(), + uid: z.string().optional(), + heading: z + .union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]) + .optional(), + open: z.boolean().optional(), + }), +); + +const stringWithDefault = (defaultVal: string) => + z + .string() + .nullable() + .optional() + .transform((val) => val ?? defaultVal); + +const booleanWithDefault = (defaultVal: boolean) => + z + .boolean() + .nullable() + .optional() + .transform((val) => val ?? defaultVal); + +export const DiscourseNodeSchema = z.object({ + text: z.string(), + uid: z.string(), + format: stringWithDefault(""), + shortcut: stringWithDefault(""), + tag: stringWithDefault(""), + description: stringWithDefault(""), + specification: z + .array(ConditionSchema) + .nullable() + .optional() + .transform((val) => val ?? []), + template: z + .array(RoamNodeSchema) + .nullable() + .optional() + .transform((val) => val ?? []), + canvasSettings: CanvasSettingsSchema.partial().nullable().optional(), + graphOverview: booleanWithDefault(false), + attributes: z + .record(z.string(), z.string()) + .nullable() + .optional() + .transform((val) => val ?? {}), + overlay: stringWithDefault(""), + index: z.unknown().nullable().optional(), + suggestiveRules: SuggestiveRulesSchema.nullable().optional(), + embeddingRef: stringWithDefault(""), + isFirstChild: z + .object({ + uid: z.string(), + value: z.boolean(), + }) + .nullable() + .optional(), + backedBy: z + .enum(["user", "default", "relation"]) + .nullable() + .transform((val) => val ?? "user"), +}); + +export const FeatureFlagsSchema = z.object({ + "Enable Left Sidebar": z.boolean().default(false), + "Suggestive Mode Enabled": z.boolean().default(false), + "Reified Relation Triples": z.boolean().default(false), +}); + +export const ExportSettingsSchema = z.object({ + "Remove Special Characters": z.boolean().default(false), + "Resolve Block References": z.boolean().default(false), + "Resolve Block Embeds": z.boolean().default(false), + "Append Referenced Node": z.boolean().default(false), + "Link Type": z.enum(["alias", "wikilinks", "roam url"]).default("alias"), + "Max Filename Length": z.number().default(64), + Frontmatter: z.array(z.string()).default([]), +}); + +export const PageGroupSchema = z.object({ + name: z.string(), + pages: z.array(z.string()).default([]), +}); + +export const SuggestiveModeGlobalSettingsSchema = z.object({ + "Include Current Page Relations": z.boolean().default(false), + "Include Parent And Child Blocks": z.boolean().default(false), + "Page Groups": z.array(PageGroupSchema).default([]), +}); + +export const LeftSidebarGlobalSettingsSchema = z.object({ + Children: z.array(z.string()).default([]), + Settings: z + .object({ + Collapsable: z.boolean().default(false), + Folded: z.boolean().default(false), + }) + .default({}), +}); + +export const GlobalSettingsSchema = z.object({ + Trigger: z.string().default(""), + "Canvas Page Format": z.string().default(""), + "Left Sidebar": LeftSidebarGlobalSettingsSchema.default({}), + Export: ExportSettingsSchema.default({}), + "Suggestive Mode": SuggestiveModeGlobalSettingsSchema.default({}), +}); + +export const PersonalSectionSchema = z.object({ + Children: z + .array( + z.object({ + Page: z.string(), + Alias: z.string().default(""), + }), + ) + .default([]), + Settings: z + .object({ + "Truncate-result?": z.number().default(75), + Folded: z.boolean().default(false), + }) + .default({}), +}); + +export const LeftSidebarPersonalSettingsSchema = z + .record(z.string(), PersonalSectionSchema) + .default({}); + +export const StoredFiltersSchema = z.object({ + includes: z.object({ values: z.array(z.string()).default([]) }).default({}), + excludes: z.object({ values: z.array(z.string()).default([]) }).default({}), +}); + +export const QuerySettingsSchema = z.object({ + "Hide Query Metadata": z.boolean().default(false), + "Default Page Size": z.number().default(10), + "Query Pages": z.array(z.string()).default([]), + "Default Filters": z.record(z.string(), StoredFiltersSchema).default({}), +}); + +export const PersonalSettingsSchema = z.object({ + "Left Sidebar": LeftSidebarPersonalSettingsSchema, + "Personal Node Menu Trigger": z.string().default(""), + "Node Search Menu Trigger": z.string().default(""), + "Discourse Tool Shortcut": z.string().default(""), + "Discourse Context Overlay": z.boolean().default(false), + "Suggestive Mode Overlay": z.boolean().default(false), + "Overlay in Canvas": z.boolean().default(false), + "Text Selection Popup": z.boolean().default(true), + "Disable Sidebar Open": z.boolean().default(false), + "Page Preview": z.boolean().default(false), + "Hide Feedback Button": z.boolean().default(false), + "Streamline Styling": z.boolean().default(false), + "Auto Canvas Relations": z.boolean().default(false), + "Disable Product Diagnostics": z.boolean().default(false), + Query: QuerySettingsSchema.default({}), +}); + +export const GithubSettingsSchema = z.object({ + "oauth-github": z.string().optional(), + "selected-repo": z.string().optional(), +}); + +/* eslint-enable @typescript-eslint/naming-convention */ + +export type CanvasSettings = z.infer; +export type SuggestiveRules = z.infer; +export type DiscourseNodeSettings = z.infer; +export type FeatureFlags = z.infer; +export type ExportSettings = z.infer; +export type PageGroup = z.infer; +export type SuggestiveModeGlobalSettings = z.infer< + typeof SuggestiveModeGlobalSettingsSchema +>; +export type LeftSidebarGlobalSettings = z.infer< + typeof LeftSidebarGlobalSettingsSchema +>; +export type GlobalSettings = z.infer; +export type PersonalSection = z.infer; +export type LeftSidebarPersonalSettings = z.infer< + typeof LeftSidebarPersonalSettingsSchema +>; +export type StoredFilters = z.infer; +export type QuerySettings = z.infer; +export type PersonalSettings = z.infer; +export type GithubSettings = z.infer; +export type QueryCondition = z.infer; +export type QuerySelection = z.infer; +export type RoamNodeType = z.infer;