Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
72985bc
add missing `is-collapsed` class to callout's div when closed
GamerGirlandCo Feb 11, 2026
841d48e
add new modal component
GamerGirlandCo Feb 11, 2026
626d437
add ability to `dc.require` the `obsidian` package
GamerGirlandCo Jan 23, 2026
db33036
add `sort` function to `Groupings` namespace
GamerGirlandCo Jan 22, 2026
a4fad0e
add hooks and reducers to facilitate sorting
GamerGirlandCo Jan 23, 2026
b979e2b
re-add sorting functionality to table view
GamerGirlandCo Jan 23, 2026
5e5007f
export `TableContextProvider`
GamerGirlandCo Jan 23, 2026
d131f4e
add table context getter prop to sort button
GamerGirlandCo Feb 5, 2026
3dc0d9e
lift common props from table context into their own type
GamerGirlandCo Feb 5, 2026
199960a
create a `useAsync` hook and a companion `Suspend` component
GamerGirlandCo Apr 22, 2025
5ce8f79
add `loader` parameter to `useEffect` dependencies
GamerGirlandCo Apr 27, 2025
d4246cb
make `Suspend` fallback prettier
GamerGirlandCo Apr 27, 2025
7ece762
update `useAsync` to be more resilient to race conditions
GamerGirlandCo May 10, 2025
221c5fc
rewrite `useAsync` to use suspense boundaries properly
GamerGirlandCo Jun 29, 2025
f205e63
expose `SETTINGS_CONTEXT`, `COMPONENT_CONTEXT`, `DATACORE_CONTEXT` an…
GamerGirlandCo Feb 17, 2025
629f915
add ability to create views for queries that are treated like any oth…
GamerGirlandCo Apr 19, 2025
1d5a4e5
upgrade react-select to silence typescript errors
GamerGirlandCo May 8, 2025
ea02777
re-add vim codemirror plugin
GamerGirlandCo May 8, 2025
0848f4a
re-add missing autocomplete and javascript language codemirror plugins
GamerGirlandCo May 8, 2025
26d7c6d
add more dev deps to appease typescript
GamerGirlandCo May 8, 2025
10e5fe2
bring `@codemirror/view` up to date
GamerGirlandCo May 8, 2025
c8745c9
silence more bogus errors
GamerGirlandCo May 10, 2025
279ffd2
move select augmentation to proper folder, re-export some things
GamerGirlandCo May 10, 2025
305c096
re-add `src/ui/fields` folder (for now?)
GamerGirlandCo Aug 4, 2025
dd32cf8
re-add missing autocomplete and javascript language codemirror plugins
GamerGirlandCo May 8, 2025
bb50758
add more dev deps to appease typescript
GamerGirlandCo May 8, 2025
1e87843
bring `@codemirror/view` up to date
GamerGirlandCo May 8, 2025
650f4af
silence more bogus errors
GamerGirlandCo May 10, 2025
30de22d
move select augmentation to proper folder, re-export some things
GamerGirlandCo May 10, 2025
691f763
add "controlled" editable component
GamerGirlandCo Apr 19, 2025
c73f265
add controlled editable component to local api
GamerGirlandCo Apr 21, 2025
9e037af
add task + tasklist components, add utilities for manipulating tasks,…
GamerGirlandCo Apr 19, 2025
53a508e
fix for multiline list items
GamerGirlandCo Nov 1, 2024
16f1c3b
add list styles
GamerGirlandCo Apr 19, 2025
6c965ea
remove unused import from task utils
GamerGirlandCo Apr 19, 2025
a920046
remove unused imports from a couple files
GamerGirlandCo Apr 20, 2025
26c8379
re-add `setTaskText` and `setTaskCompletion` utilities to local api
GamerGirlandCo Apr 20, 2025
b5c8e8e
fix errors caused by rebase
GamerGirlandCo May 8, 2025
53a7940
update `setTaskText` signature to allow overwriting inline fields wit…
GamerGirlandCo Jun 28, 2025
7587e62
fix bug where fields not present in `newFields` get set to an empty v…
GamerGirlandCo Jun 28, 2025
e8cae82
refactor and improve react code for rendering list/task fields
GamerGirlandCo Jan 23, 2026
97347a7
make table columns optionally editable
GamerGirlandCo Apr 19, 2025
52cae3f
add components to local api
GamerGirlandCo Apr 19, 2025
4675985
make table columns optionally editable
GamerGirlandCo Apr 19, 2025
9815fa3
add ui to create new table rows
GamerGirlandCo Apr 23, 2025
167490d
improve logic around the create button's onclick callback
GamerGirlandCo Apr 23, 2025
4494211
fix bug caused by using `useStableCallback`
GamerGirlandCo Apr 23, 2025
e3c477b
fix bug where create button is shown regardless of props
GamerGirlandCo Apr 23, 2025
9dfd5a1
add helper functions for inserting markdown to local api
GamerGirlandCo Apr 23, 2025
d1ac1dd
remove unused import
GamerGirlandCo Apr 23, 2025
f95e1a0
update click callback factory to take an optional `GroupingConfig` ar…
GamerGirlandCo Apr 23, 2025
b5a9969
remove unnecessary parameter from `GroupingConfig<T>.create`
GamerGirlandCo Apr 23, 2025
9e02797
add "current group" parameter to `GroupingConfig<T>.create`
GamerGirlandCo Apr 23, 2025
1af8b8b
update `insertMarkdownAt` helper
GamerGirlandCo Apr 23, 2025
c0f097a
move `insertListOrTaskItemAt` logic into a reusable utility function
GamerGirlandCo Apr 23, 2025
7fb19af
create utility to return zeroed/default values that correspond to a `…
GamerGirlandCo Apr 23, 2025
270f3d9
remove drop shadow from dashed buttons
GamerGirlandCo Apr 23, 2025
1051d80
implement adding new task+list items in their respective views
GamerGirlandCo Apr 23, 2025
35d6d98
rework adding new items in list views
GamerGirlandCo May 8, 2025
e12caa4
remove accidental import
GamerGirlandCo May 8, 2025
66ae8bb
changes!
GamerGirlandCo May 8, 2025
b7a05cc
fix critical off-by-one error in list item line counts
GamerGirlandCo May 9, 2025
d4a671f
refactor list component to avoid prop drilling
GamerGirlandCo Jan 23, 2026
e5e4701
refactor: use context to get/store the creation callback factory inst…
GamerGirlandCo Feb 2, 2026
c7b28a0
add components to local api
GamerGirlandCo Apr 19, 2025
75034d0
add tree table view and related utilities
GamerGirlandCo Apr 19, 2025
36ad282
add tree table to local api
GamerGirlandCo Apr 19, 2025
0de5766
delete unused imports from `table.tsx`
GamerGirlandCo Apr 20, 2025
b786d1f
fix type errors related to `width` property in `tree-table.tsx`
GamerGirlandCo Apr 20, 2025
3b427f4
add ui for creating new rows to tree table
GamerGirlandCo Apr 23, 2025
eae8826
minor visual improvements to "add item" buttons
GamerGirlandCo Apr 23, 2025
98e69c8
update treetable click callback factory to take an optional `Grouping…
GamerGirlandCo Apr 23, 2025
c60922d
update callback factory to use new `create` signature
GamerGirlandCo Apr 23, 2025
4452b09
use new type names for table-related props
GamerGirlandCo May 8, 2025
fac98fc
remove extra padding from tree-table `create` button
GamerGirlandCo May 9, 2025
31ef896
move tree utils to separate file
GamerGirlandCo May 9, 2025
92806bc
refactor: use tree table context to get/store the creation callback f…
GamerGirlandCo Feb 2, 2026
ca6b91f
set sort button's context getter
GamerGirlandCo Feb 5, 2026
14b0dca
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
124 changes: 120 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,6 +110,9 @@ 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 });
return result.orElseThrow();
}
Expand Down Expand Up @@ -189,6 +209,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 +293,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 +355,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 +467,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 +493,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