Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
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
48 changes: 45 additions & 3 deletions src/api/local-api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import { IndexQuery } from "index/types/index-query";
import { Indexable } from "index/types/indexable";
import { MarkdownPage } 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";
Expand All @@ -28,6 +28,8 @@ 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";

/**
* Local API provided to specific codeblocks when they are executing.
Expand All @@ -37,6 +39,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 +97,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 +196,25 @@ 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);
}
//////////////
// Contexts //
//////////////

// export the necessary contexts to enable rendering
// datacore components outside the datacore plugin
// itself
get SETTINGS_CONTEXT(): typeof SETTINGS_CONTEXT {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I'd do it like this - it's better to expose hook functions which load this data instead (like dc.useSettings()).

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 +245,7 @@ export class DatacoreLocalApi {
* React's reference-equality-based caching.
*/
public useInterning = useInterning;
public useAsync = useAsync;

/** 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 +306,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 +418,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 Down
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
126 changes: 126 additions & 0 deletions src/api/ui/views/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {
ReactNode,
RefObject,
forwardRef,
useContext,
createContext,
memo,
Context as ReactContext,
useEffect,
useRef,
createPortal,
ComponentProps,
ForwardedRef,
useImperativeHandle,
useMemo,
} from "preact/compat";
import { App, Modal as ObsidianModal } from "obsidian";
import { APP_CONTEXT } from "ui/markdown";
import { Literal } from "expression/literal";
import { VNode } from "preact";

class DatacoreModal extends ObsidianModal {
constructor(app: App, public openCallback?: ObsidianModal["onOpen"], public onCancel?: ObsidianModal["onClose"]) {
super(app);
}
public onOpen() {
super.onOpen();
this.openCallback?.();
}
public onClose() {
super.onClose();
this.onCancel?.();
}
}

class SubmittableDatacoreModal<T> extends DatacoreModal {
constructor(
app: App,
public submitCallback?: (result: T) => void | Promise<void>,
public openCallback?: ObsidianModal["onOpen"],
public onCancel?: ObsidianModal["onClose"]
) {
super(app, openCallback, onCancel);
}
public onSubmit(result: T) {
this.close();
this.submitCallback?.(result);
}
}

export class Modals {
get submittableModal() {
return SubmittableDatacoreModal;
}
get modal() {
return DatacoreModal;
}
}

interface ModalContextType<M extends ObsidianModal> {
modal: M;
}

interface BaseModalProps {
title?: Literal | VNode | ReactNode;
children: ReactNode;
onCancel?: ObsidianModal["onClose"];
onOpen?: ObsidianModal["onOpen"];
}

const MODAL_CONTEXT = createContext<ModalContextType<any> | null>(null);

function ModalContext<M extends ObsidianModal>({ modal, children }: { modal: M; children: ReactNode }) {
const Ctx = MODAL_CONTEXT as ReactContext<ModalContextType<M>>;
return <Ctx.Provider value={{ modal }}>{children}</Ctx.Provider>;
}

function useReusableImperativeHandle<M extends ObsidianModal>(modal: M, ref: ForwardedRef<M>) {
useImperativeHandle(ref, () => modal, [modal]);
}

function InnerSubmittableModal<T>(
{
children,
onSubmit,
onCancel,
onOpen,
title,
}: BaseModalProps & {
onSubmit?: (result: T) => void | Promise<void>;
},
ref: ForwardedRef<SubmittableDatacoreModal<T>>
) {
const app = useContext(APP_CONTEXT)!;
const modal = useMemo(
() => new SubmittableDatacoreModal<T>(app, onSubmit, onOpen, onCancel),
[app, onSubmit, onOpen, onCancel]
);
useReusableImperativeHandle(modal, ref);
return (
<ModalContext modal={modal}>
{createPortal(<>{title}</>, modal.titleEl)}
{createPortal(<>{children}</>, modal.contentEl)}
</ModalContext>
);
}

function InnerModal({ children, onCancel, onOpen, title }: BaseModalProps, ref: ForwardedRef<DatacoreModal>) {
const app = useContext(APP_CONTEXT)!;
const modal = useMemo(() => new DatacoreModal(app, onOpen, onCancel), [app, onOpen, onCancel]);
useReusableImperativeHandle(modal, ref);
return (
<ModalContext modal={modal}>
{createPortal(<>{title}</>, modal.titleEl)}
{createPortal(<>{children}</>, modal.contentEl)}
</ModalContext>
);
}

export function useModalContext<M extends ObsidianModal>() {
return useContext(MODAL_CONTEXT) as ModalContextType<M>;
}

export const SubmittableModal = forwardRef(InnerSubmittableModal) as typeof InnerSubmittableModal;

export const Modal = forwardRef(InnerModal) as typeof InnerModal;
56 changes: 56 additions & 0 deletions src/api/ui/views/table-dispatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createContext } from "preact";
import { Dispatch, useMemo, useReducer, Reducer, useContext } from "preact/hooks";

/** The ways that the table can be sorted. */
export type SortDirection = "ascending" | "descending";
export type SortOn = { type: "column"; id: string; direction: SortDirection };

export type TableAction = { type: "sort-column"; column: string; direction: SortDirection | undefined };

export interface TableState {
/** mapping of column ids to sort directions */
sorts: Record<string, SortDirection>;
}

export function tableReducer(state: TableState, action: TableAction): TableState {
switch (action.type) {
case "sort-column": {
const newSorts = {...state.sorts};
if (action.direction === undefined) {
delete newSorts[action.column];
} else {
newSorts[action.column] = action.direction;
}
return {
...state,
sorts: newSorts,
};
}
}
console.warn("datacore: Encountered unrecognized operation: " + (action as TableAction).type);
return state;
}

export function useTableDispatch(initial: TableState | (() => TableState)): [TableState, Dispatch<TableAction>] {
const init = useMemo(() => (typeof initial == "function" ? initial() : initial), []);
return useReducer(tableReducer as Reducer<TableState, TableAction>, init);
}

export type CommonTableContext = TableState & {
dispatch: Dispatch<TableAction>
}

export type TableContext = CommonTableContext & {
}

export const TABLE_CONTEXT = createContext<TableContext | null>(null);

export const COMMON_TABLE_CONTEXT = createContext<CommonTableContext | null>(null);

export function useTableContext() {
return useContext(TABLE_CONTEXT);
}

export function useCommonTableContext() {
return useContext(COMMON_TABLE_CONTEXT);
}
Loading
Loading