Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
83b5edb
add missing `is-collapsed` class to callout's div when closed
GamerGirlandCo Feb 11, 2026
07baefe
add new modal component
GamerGirlandCo Feb 11, 2026
e5bbbba
add ability to `dc.require` the `obsidian` package
GamerGirlandCo Jan 23, 2026
2b3a497
add `sort` function to `Groupings` namespace
GamerGirlandCo Jan 22, 2026
83fd96b
add hooks and reducers to facilitate sorting
GamerGirlandCo Jan 23, 2026
e3d4b24
re-add sorting functionality to table view
GamerGirlandCo Jan 23, 2026
5996813
export `TableContextProvider`
GamerGirlandCo Jan 23, 2026
bbc8dca
add table context getter prop to sort button
GamerGirlandCo Feb 5, 2026
32f1d58
lift common props from table context into their own type
GamerGirlandCo Feb 5, 2026
e9dde80
create a `useAsync` hook and a companion `Suspend` component
GamerGirlandCo Apr 22, 2025
8a06ed1
add `loader` parameter to `useEffect` dependencies
GamerGirlandCo Apr 27, 2025
4ae3c08
make `Suspend` fallback prettier
GamerGirlandCo Apr 27, 2025
1ac5022
update `useAsync` to be more resilient to race conditions
GamerGirlandCo May 10, 2025
29cdf89
rewrite `useAsync` to use suspense boundaries properly
GamerGirlandCo Jun 29, 2025
293ee74
expose `SETTINGS_CONTEXT`, `COMPONENT_CONTEXT`, `DATACORE_CONTEXT` an…
GamerGirlandCo Feb 17, 2025
6cbd5ff
add ability to create views for queries that are treated like any oth…
GamerGirlandCo Apr 19, 2025
69147b2
upgrade react-select to silence typescript errors
GamerGirlandCo May 8, 2025
d86f9e4
re-add vim codemirror plugin
GamerGirlandCo May 8, 2025
894c335
re-add missing autocomplete and javascript language codemirror plugins
GamerGirlandCo May 8, 2025
da56bc3
add more dev deps to appease typescript
GamerGirlandCo May 8, 2025
cd33589
bring `@codemirror/view` up to date
GamerGirlandCo May 8, 2025
3a4ddd0
silence more bogus errors
GamerGirlandCo May 10, 2025
045aecc
move select augmentation to proper folder, re-export some things
GamerGirlandCo May 10, 2025
9719e59
re-add `src/ui/fields` folder (for now?)
GamerGirlandCo Aug 4, 2025
91c7ef7
re-add missing autocomplete and javascript language codemirror plugins
GamerGirlandCo May 8, 2025
5fbcd15
add more dev deps to appease typescript
GamerGirlandCo May 8, 2025
5e99195
bring `@codemirror/view` up to date
GamerGirlandCo May 8, 2025
a9cfd3e
silence more bogus errors
GamerGirlandCo May 10, 2025
6196e1d
move select augmentation to proper folder, re-export some things
GamerGirlandCo May 10, 2025
2fa6125
add "controlled" editable component
GamerGirlandCo Apr 19, 2025
035945f
add controlled editable component to local api
GamerGirlandCo Apr 21, 2025
bf4a8c9
add task + tasklist components, add utilities for manipulating tasks,…
GamerGirlandCo Apr 19, 2025
872de4f
fix for multiline list items
GamerGirlandCo Nov 1, 2024
f917c0d
add list styles
GamerGirlandCo Apr 19, 2025
77109c1
remove unused import from task utils
GamerGirlandCo Apr 19, 2025
c2f5cff
remove unused imports from a couple files
GamerGirlandCo Apr 20, 2025
c2f36ca
re-add `setTaskText` and `setTaskCompletion` utilities to local api
GamerGirlandCo Apr 20, 2025
d9783b8
fix errors caused by rebase
GamerGirlandCo May 8, 2025
15d667c
update `setTaskText` signature to allow overwriting inline fields wit…
GamerGirlandCo Jun 28, 2025
a8041ca
fix bug where fields not present in `newFields` get set to an empty v…
GamerGirlandCo Jun 28, 2025
a3d9a7d
refactor and improve react code for rendering list/task fields
GamerGirlandCo Jan 23, 2026
ebb4439
add controlled editable component to local api
GamerGirlandCo Apr 21, 2025
993f722
make table columns optionally editable
GamerGirlandCo Apr 19, 2025
ce0b8d4
make table columns optionally editable
GamerGirlandCo Apr 19, 2025
f190c31
add components to local api
GamerGirlandCo Apr 19, 2025
12a66c2
make table columns optionally editable
GamerGirlandCo Apr 19, 2025
bff11d3
add ui to create new table rows
GamerGirlandCo Apr 23, 2025
bcff1f5
improve logic around the create button's onclick callback
GamerGirlandCo Apr 23, 2025
80ccdb7
fix bug caused by using `useStableCallback`
GamerGirlandCo Apr 23, 2025
2e720b9
fix bug where create button is shown regardless of props
GamerGirlandCo Apr 23, 2025
626d9bf
add helper functions for inserting markdown to local api
GamerGirlandCo Apr 23, 2025
dae0242
remove unused import
GamerGirlandCo Apr 23, 2025
bd0c7c2
update click callback factory to take an optional `GroupingConfig` ar…
GamerGirlandCo Apr 23, 2025
bc5a194
remove unnecessary parameter from `GroupingConfig<T>.create`
GamerGirlandCo Apr 23, 2025
cb5c624
add "current group" parameter to `GroupingConfig<T>.create`
GamerGirlandCo Apr 23, 2025
b315a1d
update `insertMarkdownAt` helper
GamerGirlandCo Apr 23, 2025
4958f45
move `insertListOrTaskItemAt` logic into a reusable utility function
GamerGirlandCo Apr 23, 2025
83f9a0f
create utility to return zeroed/default values that correspond to a `…
GamerGirlandCo Apr 23, 2025
a6601d2
remove drop shadow from dashed buttons
GamerGirlandCo Apr 23, 2025
91264f6
implement adding new task+list items in their respective views
GamerGirlandCo Apr 23, 2025
128fab9
rework adding new items in list views
GamerGirlandCo May 8, 2025
fcdcabb
remove accidental import
GamerGirlandCo May 8, 2025
b83ce4f
changes!
GamerGirlandCo May 8, 2025
31acf67
fix critical off-by-one error in list item line counts
GamerGirlandCo May 9, 2025
061411f
refactor list component to avoid prop drilling
GamerGirlandCo Jan 23, 2026
7899ed8
refactor: use context to get/store the creation callback factory inst…
GamerGirlandCo Feb 2, 2026
49943c5
add components to local api
GamerGirlandCo Apr 19, 2025
c47a95e
add tree table view and related utilities
GamerGirlandCo Apr 19, 2025
6722e7b
add tree table to local api
GamerGirlandCo Apr 19, 2025
c9196aa
delete unused imports from `table.tsx`
GamerGirlandCo Apr 20, 2025
47c073e
fix type errors related to `width` property in `tree-table.tsx`
GamerGirlandCo Apr 20, 2025
14f549b
add ui for creating new rows to tree table
GamerGirlandCo Apr 23, 2025
4031969
minor visual improvements to "add item" buttons
GamerGirlandCo Apr 23, 2025
96b1113
update treetable click callback factory to take an optional `Grouping…
GamerGirlandCo Apr 23, 2025
3ba88c0
update callback factory to use new `create` signature
GamerGirlandCo Apr 23, 2025
1aea922
use new type names for table-related props
GamerGirlandCo May 8, 2025
3b40f4e
remove extra padding from tree-table `create` button
GamerGirlandCo May 9, 2025
9e2a8fd
move tree utils to separate file
GamerGirlandCo May 9, 2025
0ac95ab
refactor: use tree table context to get/store the creation callback f…
GamerGirlandCo Feb 2, 2026
460565e
set sort button's context getter
GamerGirlandCo Feb 5, 2026
ae66796
refactor table types to be generic
GamerGirlandCo Feb 6, 2026
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
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
"author": "Michael Brenan",
"license": "MIT",
"devDependencies": {
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/commands": "^6.8.1",
"@codemirror/language": "https://github.com/lishid/cm-language",
"@codemirror/search": "^6.5.10",
"@codemirror/state": "^6.0.1",
"@codemirror/view": "^6.0.1",
"@codemirror/view": "^6.36.7",
"@microsoft/api-extractor": "^7.52.7",
"@types/jest": "^27.0.1",
"@types/luxon": "^2.3.2",
Expand All @@ -45,17 +48,19 @@
"typescript": "^5.4.2"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.2.3",
"@datastructures-js/queue": "^4.2.3",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@replit/codemirror-vim": "^6.3.0",
"emoji-regex": "^10.2.1",
"flatqueue": "^2.0.3",
"localforage": "1.10.0",
"luxon": "^2.4.0",
"parsimmon": "^1.18.0",
"preact": "^10.17.1",
"react-select": "^5.8.0",
"preact": "^10.26.6",
"react-select": "^5.10.1",
"sorted-btree": "^1.8.1",
"sucrase": "3.35.0",
"yaml": "^2.3.3"
Expand Down
127 changes: 123 additions & 4 deletions src/api/local-api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,42 @@ import { Datacore } from "index/datacore";
import { SearchResult } from "index/datastore";
import { IndexQuery } from "index/types/index-query";
import { Indexable } from "index/types/indexable";
import { MarkdownPage } from "index/types/markdown";
import { MarkdownListItem, MarkdownPage, MarkdownTaskItem } from "index/types/markdown";
import { App } from "obsidian";
import { useFileMetadata, useFullQuery, useIndexUpdates, useInterning, useQuery } from "ui/hooks";
import { useAsync, useFileMetadata, useFullQuery, useIndexUpdates, useInterning, useQuery } from "ui/hooks";
import * as luxon from "luxon";
import * as preact from "preact";
import * as hooks from "preact/hooks";
import { Result } from "./result";
import { Group, Stack } from "./ui/layout";
import { Embed, LineSpanEmbed } from "api/ui/embed";
import { CURRENT_FILE_CONTEXT, ErrorMessage, Lit, Markdown, ObsidianLink } from "ui/markdown";
import { CSSProperties } from "preact/compat";
import { APP_CONTEXT, COMPONENT_CONTEXT, CURRENT_FILE_CONTEXT, DATACORE_CONTEXT, ErrorMessage, Lit, Markdown, ObsidianLink, SETTINGS_CONTEXT } from "ui/markdown";
import { CSSProperties, Suspense } from "preact/compat";
import { Literal, Literals } from "expression/literal";
import { Button, Checkbox, Icon, Slider, Switch, Textbox, VanillaSelect } from "./ui/basics";
import { TableView } from "./ui/views/table";
import { Callout } from "./ui/views/callout";
import { TaskList } from "./ui/views/task";
import { DataArray } from "./data-array";
import { Coerce } from "./coerce";
import { ScriptCache } from "./script-cache";
import { Expression } from "expression/expression";
import { Card } from "./ui/views/cards";
import { ListView } from "./ui/views/list";
import { Modal, Modals, SubmittableModal, useModalContext } from "./ui/views/modal";
import * as obsidian from "obsidian";
import { ControlledEditable } from "ui/fields/editable";
import { setTaskText, useSetField } from "utils/fields";
import { completeTask, insertListOrTaskItemAt } from "utils/task";
import {
ControlledEditableTextField,
EditableTextField,
FieldCheckbox,
FieldSelect,
FieldSlider,
FieldSwitch,
} from "ui/fields/editable-fields";
import { TreeTableView } from "./ui/views/tree-table";

/**
* Local API provided to specific codeblocks when they are executing.
Expand All @@ -37,6 +52,8 @@ export class DatacoreLocalApi {
/** @internal The cache of all currently loaded scripts in this context. */
private scriptCache: ScriptCache;

private modalTypes: Modals = new Modals();

public constructor(public api: DatacoreApi, public path: string) {
this.scriptCache = new ScriptCache(this.core.datastore);
}
Expand Down Expand Up @@ -93,7 +110,13 @@ export class DatacoreLocalApi {
* ```
*/
public async require(path: string | Link): Promise<unknown> {
if (typeof path === "string" && path === "obsidian") {
return Result.success(obsidian);
}
const result = await this.scriptCache.load(path, { dc: this });
if (typeof path === "string" && path === "obsidian") {
return Result.success(obsidian);
}
return result.orElseThrow();
}

Expand Down Expand Up @@ -189,6 +212,60 @@ export class DatacoreLocalApi {
public tryFullQuery<T extends Indexable = Indexable>(query: string | IndexQuery): Result<SearchResult<T>, string>;
public tryFullQuery(query: string | IndexQuery): Result<SearchResult<Indexable>, string> {
return this.api.tryFullQuery(query);
}
/** Sets the text of a given task programmatically. */
public setTaskText(newText: string, task: MarkdownTaskItem, newFields: Record<string, Literal> = {}): void {
setTaskText(this.app, this.core, newText, task, newFields);
}

/** Sets the completion status of a given task programmatically. */
public setTaskCompletion(completed: boolean, task: MarkdownTaskItem): void {
completeTask(completed, task, this.app.vault, this.core);
}

/** inserts the provided markdown string at the given position in a file. */
public async insertMarkdownAt(line: number, path: string, markdown: string) {
const file = this.app.vault.getFileByPath(path);
if (file != null) {
const content = await this.app.vault.read(file);
const lines = content.split(/\r\n|\r|\n/u);
if (line < lines.length) {
if (line < 0) line = lines.length + line;
lines.splice(line, 0, markdown);
await this.app.vault.modify(file, lines.join(content.contains("\r") ? "\r\n" : "\n"));
}
}
}

public async insertListOrTaskItemAt(
parent: MarkdownTaskItem | MarkdownListItem | number,
atEnd: boolean,
status: string,
text: string,
path?: string,
fields: Record<string, any> = {}
) {
await insertListOrTaskItemAt(this.app, parent, atEnd, status, text, path, fields);
}

//////////////
// Contexts //
//////////////

// export the necessary contexts to enable rendering
// datacore components outside the datacore plugin
// itself
get SETTINGS_CONTEXT(): typeof SETTINGS_CONTEXT {
return SETTINGS_CONTEXT;
}
get COMPONENT_CONTEXT(): typeof COMPONENT_CONTEXT {
return COMPONENT_CONTEXT;
}
get DATACORE_CONTEXT(): typeof DATACORE_CONTEXT {
return DATACORE_CONTEXT;
}
get APP_CONTEXT(): typeof APP_CONTEXT {
return APP_CONTEXT;
}

/////////////
Expand Down Expand Up @@ -219,6 +296,8 @@ export class DatacoreLocalApi {
* React's reference-equality-based caching.
*/
public useInterning = useInterning;
public useAsync = useAsync;
public useSetField = useSetField;

/** Memoize the input automatically and process it using a DataArray; returns a vanilla array back. */
public useArray<T, U>(
Expand Down Expand Up @@ -279,6 +358,8 @@ export class DatacoreLocalApi {
/** Horizontal flexbox container; good for putting items together in a row. */
public Group = Group;

public Suspense = Suspense;

/** Renders a literal value in a pretty way that respects settings. */
public Literal = (({ value, sourcePath, inline }: { value: Literal; sourcePath?: string; inline?: boolean }) => {
const implicitSourcePath = hooks.useContext(CURRENT_FILE_CONTEXT);
Expand Down Expand Up @@ -389,6 +470,19 @@ export class DatacoreLocalApi {
return <ErrorMessage message={`No valid embedding for element '${element.$id}' from '${element.$file}'`} />;
}).bind(this);

/** Accessor for raw modal classes. */
public get modals() {
return this.modalTypes;
}

/** Wrapper around an obsidian modal. */
public Modal = Modal;

/** Wrapper around an obsidian modal that returns a result when submitted. */
public SubmittableModal = SubmittableModal;

public useModalContext = useModalContext;

///////////
// Views //
///////////
Expand All @@ -402,16 +496,41 @@ export class DatacoreLocalApi {
public List = ListView;
/** A single card which can be composed into a grid view. */
public Card = Card;
public TaskList = TaskList;
public TreeTable = TreeTableView;

/////////////////////////
// Interative elements //
/////////////////////////

public ControlledEditable = ControlledEditable;
public Button = Button;
public Textbox = Textbox;
public Callout = Callout;
public Checkbox = Checkbox;
public Slider = Slider;
public Switch = Switch;
public VanillaSelect = VanillaSelect;
public VanillaTextBox = ControlledEditableTextField;

////////////////////////////////////
// Stateful / internal components //
////////////////////////////////////

/**
* Updates the path for the local API; usually only called by the top-level script renderer on
* path changes (such as renaming a file).
* @internal
*/
updatePath(path: string): void {
this.path = path;
}
/////////////////////////
// field editors //
/////////////////////////
public FieldCheckbox = FieldCheckbox;
public FieldSlider = FieldSlider;
public FieldSelect = FieldSelect;
public FieldSwitch = FieldSwitch;
public TextField = EditableTextField;
}
130 changes: 130 additions & 0 deletions src/api/ui/table-types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { App } from "obsidian";
import { GroupElement, Grouping, Literal } from "expression/literal";
import { TreeTableRowData } from "utils/tree";
import { GroupingConfig, TableColumn } from "./views/table";
import { Context as ReactContext, createContext, Dispatch, PropsWithChildren, ReactNode, useContext } from "preact/compat";
import { SortDirection, SortOn } from "./views/table-dispatch";

export type TableKind = "table" | "tree-table";

export type TableData<T, K extends TableKind> = K extends "tree-table" ? TreeTableRowData<T> : T;

type CreateRowFn<E, K extends TableKind = "table"> = K extends "table"
? (
prevElement: TableData<E, "table"> | null,
parentGroup: GroupElement<TableData<E, "table">> | null,
app: App
) => Promise<unknown>
: (
prevElement: TableData<E, "tree-table"> | null,
parentElement: TableData<E, "tree-table"> | null,
parentGroup: GroupElement<TableData<E, "tree-table">> | null,
app: App
) => Promise<unknown>;

type ClickCallbackFn<T, K extends TableKind = "table"> = K extends "table"
? (
previousElement: GroupElement<T> | T | null,
element: GroupElement<T> | T | null,
groupConfig?: GroupingConfig<T>
) => () => Promise<void>
: (
previousElement: GroupElement<TreeTableRowData<T>> | TreeTableRowData<T> | null,
parent: TreeTableRowData<T> | null,
maybeGroup: GroupElement<TreeTableRowData<T>> | TreeTableRowData<T> | null,
groupConfig?: GroupingConfig<TreeTableRowData<T>>
) => () => Promise<void>;

type TreeTableProps<T> = {
id?: (obj: T) => string;
childSelector: (raw: T) => T[];
};

export type GenericTableViewProps<T, K extends TableKind = "table"> = {
/** The type of table to render. */
type: K;
/** The columns to render in the table. */
columns: TableColumn<T>[];
/** The rows to render; may potentially be grouped or just a plain array. */
rows: T[] | Grouping<T>;
groupings?:
| GroupingConfig<TableData<T, K>>
| GroupingConfig<TableData<T, K>>[]
| ((key: Literal, rows: Grouping<TableData<T, K>>) => Literal | ReactNode);
/**
* If set to a boolean - enables or disables paging.
* If set to a number, paging will be enabled with the given number of rows per page.
*/
paging?: boolean | number;

/**
* Whether the view will scroll to the top automatically on page changes. If true, will always scroll on page changes.
* If a number, will scroll only if the number is greater than the current page size.
**/
scrollOnPaging?: boolean | number;

/** The fields to sort the view on, if relevant. */
sortOn?: SortOn[];

/** whether this table allows creation new elements. */
creatable?: boolean;
createRow?: CreateRowFn<T, K>;
} & (K extends "tree-table" ? TreeTableProps<T> : {});

export type GenericTableState<T, K extends TableKind = "table"> = {
/** mapping of column ids to sort directions */
sorts: Record<string, SortDirection>;
} & (K extends "tree-table"
? {
/** mapping of row ids to whether they are open or not */
openMap: Map<string, boolean>;
/** function to get the id of a row */
id: (obj: T) => string;
}
: {});

export type GenericTableAction<T, K extends TableKind = "table"> =
| {
type: "sort-column";
column: string;
direction: SortDirection | undefined;
}
| (K extends "tree-table"
?
| {
type: "row-expand";
row: T;
newValue: boolean;
}
| { type: "open-map-changed"; newValue: Map<string, boolean> }
: never);

export type GenericTableContext<T, K extends TableKind = "table"> = {
state: GenericTableState<T, K>;
dispatch: Dispatch<GenericTableAction<T, K>>;
clickCallbackFactory: ClickCallbackFn<T, K>;
};

export const GENERIC_TABLE_CONTEXT = createContext<GenericTableContext<any, any> | null>(null);

export function useGenericTableContext<T, K extends TableKind = "table">() {
return useContext(GENERIC_TABLE_CONTEXT) as GenericTableContext<T, K>;
}

export function typedTableContext<T, K extends TableKind = "table">() {
return GENERIC_TABLE_CONTEXT as ReactContext<GenericTableContext<T, K>>;
}

/**
* Provides a context for a generic table.
* @hidden
* @group Components
*/

export function TableContextProvider<T, K extends TableKind = "table">({
children,
...props
}: PropsWithChildren<GenericTableContext<T, K>>) {
const Context = typedTableContext<T, K>();
return <Context.Provider value={props}>{children}</Context.Provider>;
}
2 changes: 1 addition & 1 deletion src/api/ui/views/callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function Callout({
data-callout-metadata={type?.split(METADATA_SPLIT_REGEX)?.[1]}
data-callout={type?.split(METADATA_SPLIT_REGEX)?.[0]}
data-callout-fold={open ? "+" : "-"}
className={combineClasses("datacore", "callout", collapsible ? "is-collapsible" : undefined)}
className={combineClasses("datacore", "callout", collapsible ? "is-collapsible" : undefined, !open ? "is-collapsed" : undefined)}
>
<div className="callout-title" onClick={() => collapsible && setOpen(!open)}>
{icon && <div className="callout-icon">{icon}</div>}
Expand Down
Loading
Loading