Complete useChange hooks

main
Jackson Morgan 3 weeks ago
parent 1784a2d322
commit 8c63295043
  1. 8
      packages/connected/src/ConnectedLdoTransactionDataset.ts
  2. 2
      packages/connected/src/trackingProxy/TrackingProxyContext.ts
  3. 2
      packages/connected/src/trackingProxy/TrackingSubjectProxy.ts
  4. 1
      packages/connected/src/trackingProxy/createTrackingProxy.ts
  5. 8
      packages/connected/test/mocks/MockConnectedPlugin.ts
  6. 24
      packages/connected/test/mocks/MockResource.ts
  7. 5
      packages/react/src/methods/change/useChangeDataset.ts
  8. 31
      packages/react/src/methods/change/useChangeMatchObject.ts
  9. 33
      packages/react/src/methods/change/useChangeMatchSubject.ts
  10. 3
      packages/react/src/methods/change/useChangeSubject.ts
  11. 2
      packages/react/src/methods/useSubject.ts
  12. 4
      packages/react/temptest/src/App.tsx
  13. 55
      packages/react/test/useLdoForm.test.tsx
  14. 2
      packages/subscribable-dataset/src/util.ts

@ -96,13 +96,6 @@ export class ConnectedLdoTransactionDataset<Plugins extends ConnectedPlugin[]>
this.instanceId = ConnectedLdoTransactionDataset.nextId++; this.instanceId = ConnectedLdoTransactionDataset.nextId++;
} }
add(quad: Quad): this {
console.log("Adding");
super.add(quad);
console.log(this);
return this;
}
getResource< getResource<
Name extends Plugins[number]["name"], Name extends Plugins[number]["name"],
Plugin extends Extract<Plugins[number], { name: Name }>, Plugin extends Extract<Plugins[number], { name: Name }>,
@ -231,6 +224,7 @@ export class ConnectedLdoTransactionDataset<Plugins extends ConnectedPlugin[]>
); );
// If one has errored, return error // If one has errored, return error
console.log(results);
const errors = ( const errors = (
results.map((result) => result[2]) as (SuccessResult | ErrorResult)[] results.map((result) => result[2]) as (SuccessResult | ErrorResult)[]
).filter((result): result is ErrorResult => result.isError); ).filter((result): result is ErrorResult => result.isError);

@ -41,7 +41,6 @@ export class TrackingProxyContext extends ProxyContext {
listener: nodeEventListener<Quad>, listener: nodeEventListener<Quad>,
) { ) {
super(options); super(options);
console.log("trackingProxyContextDataset", options.dataset);
this.subscribableDataset = options.dataset; this.subscribableDataset = options.dataset;
this.listener = listener; this.listener = listener;
} }
@ -52,7 +51,6 @@ export class TrackingProxyContext extends ProxyContext {
if (!listeners.includes(this.listener)) { if (!listeners.includes(this.listener)) {
this.subscribableDataset.on(eventName, this.listener); this.subscribableDataset.on(eventName, this.listener);
} }
console.log("Added Listener", this.subscribableDataset);
} }
protected createNewSubjectProxy(node: NamedNode | BlankNode): SubjectProxy { protected createNewSubjectProxy(node: NamedNode | BlankNode): SubjectProxy {

@ -24,7 +24,6 @@ export function createTrackingSubjectProxy(
key: string | symbol, key: string | symbol,
receiver, receiver,
) => { ) => {
console.log("Should be calling this get function");
const subject = target["@id"]; const subject = target["@id"];
const rdfTypes = proxyContext.getRdfType(subject); const rdfTypes = proxyContext.getRdfType(subject);
if (typeof key === "symbol") { if (typeof key === "symbol") {
@ -32,7 +31,6 @@ export function createTrackingSubjectProxy(
} else if (key === "@id") { } else if (key === "@id") {
proxyContext.addListener([subject, null, null, null]); proxyContext.addListener([subject, null, null, null]);
} else if (!proxyContext.contextUtil.isSet(key, rdfTypes)) { } else if (!proxyContext.contextUtil.isSet(key, rdfTypes)) {
console.log("Should be registering here!!!!", key);
const predicate = namedNode( const predicate = namedNode(
proxyContext.contextUtil.keyToIri(key, rdfTypes), proxyContext.contextUtil.keyToIri(key, rdfTypes),
); );

@ -25,7 +25,6 @@ export function createTrackingProxyBuilder<Type extends LdoBase>(
// Rebuild the LdoBuilder from scratch to inject TrackingProxyContext // Rebuild the LdoBuilder from scratch to inject TrackingProxyContext
const contextUtil = new ContextUtil(shapeType.context); const contextUtil = new ContextUtil(shapeType.context);
console.log("Creating proxy", dataset);
const proxyContext = new TrackingProxyContext( const proxyContext = new TrackingProxyContext(
{ {
dataset, dataset,

@ -25,13 +25,13 @@ export const mockConnectedPlugin: MockConnectedPlugin = {
getResource: function ( getResource: function (
uri: string, uri: string,
_context: ConnectedContext<MockConnectedPlugin[]>, context: ConnectedContext<MockConnectedPlugin[]>,
): MockResource { ): MockResource {
return new MockResource(uri); return new MockResource(uri, context);
}, },
createResource: async function (): Promise<MockResource> { createResource: async function (context): Promise<MockResource> {
return new MockResource(v4()); return new MockResource(v4(), context);
}, },
isUriValid: function (uri: string): uri is string { isUriValid: function (uri: string): uri is string {

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import EventEmitter from "events"; import EventEmitter from "events";
import type { ConnectedContext } from "../../src/index.js";
import { import {
Unfetched, Unfetched,
type ConnectedResult, type ConnectedResult,
@ -9,8 +10,10 @@ import {
} from "../../src/index.js"; } from "../../src/index.js";
import type { DatasetChanges } from "@ldo/rdf-utils"; import type { DatasetChanges } from "@ldo/rdf-utils";
import type { ReadSuccess } from "../../src/results/success/ReadSuccess.js"; import type { ReadSuccess } from "../../src/results/success/ReadSuccess.js";
import type { UpdateSuccess } from "../../src/results/success/UpdateSuccess.js"; import { UpdateSuccess } from "../../src/results/success/UpdateSuccess.js";
import { vi } from "vitest"; import { vi } from "vitest";
import type { MockConnectedPlugin } from "./MockConnectedPlugin.js";
import type { Quad } from "@rdfjs/types";
export class MockResource export class MockResource
extends (EventEmitter as new () => ResourceEventEmitter) extends (EventEmitter as new () => ResourceEventEmitter)
@ -21,10 +24,13 @@ export class MockResource
type = "mock" as const; type = "mock" as const;
status: ConnectedResult; status: ConnectedResult;
constructor(uri: string) { protected context: ConnectedContext<MockConnectedPlugin[]>;
constructor(uri: string, context: ConnectedContext<MockConnectedPlugin[]>) {
super(); super();
this.uri = uri; this.uri = uri;
this.status = new Unfetched(this); this.status = new Unfetched(this);
this.context = context;
} }
isLoading = vi.fn<() => boolean>(); isLoading = vi.fn<() => boolean>();
@ -38,12 +44,14 @@ export class MockResource
read = vi.fn<() => Promise<ReadSuccess<any> | ResourceError<any>>>(); read = vi.fn<() => Promise<ReadSuccess<any> | ResourceError<any>>>();
readIfUnfetched = readIfUnfetched =
vi.fn<() => Promise<ReadSuccess<any> | ResourceError<any>>>(); vi.fn<() => Promise<ReadSuccess<any> | ResourceError<any>>>();
update = update = vi.fn<
vi.fn< (
( changes: DatasetChanges<Quad>,
changes: DatasetChanges, ) => Promise<UpdateSuccess<any> | ResourceError<any>>
) => Promise<UpdateSuccess<any> | ResourceError<any>> >((changes) => {
>(); this.context.dataset.bulk(changes);
return new UpdateSuccess(this);
});
subscribeToNotifications = subscribeToNotifications =
vi.fn< vi.fn<

@ -32,7 +32,6 @@ export function createUseChangeDataset<Plugins extends ConnectedPlugin[]>(
specificDataset?: IConnectedLdoDataset<Plugins>, specificDataset?: IConnectedLdoDataset<Plugins>,
): useChangeDatasetReturn<Plugins> { ): useChangeDatasetReturn<Plugins> {
const transactionDataset = useMemo(() => { const transactionDataset = useMemo(() => {
console.log("Uh oh! Getting another dataset!!!");
return ( return (
specificDataset ?? dataset specificDataset ?? dataset
).startTransaction() as ConnectedLdoTransactionDataset<Plugins>; ).startTransaction() as ConnectedLdoTransactionDataset<Plugins>;
@ -48,10 +47,6 @@ export function createUseChangeDataset<Plugins extends ConnectedPlugin[]>(
); );
const commitData = useCallback<useChangeCommitData<Plugins>>(() => { const commitData = useCallback<useChangeCommitData<Plugins>>(() => {
const changes = transactionDataset.getChanges();
console.log("Changes!!!!!");
console.log(changes.added?.toString());
console.log(changes.removed?.toString());
return transactionDataset.commitToRemote(); return transactionDataset.commitToRemote();
}, [transactionDataset]); }, [transactionDataset]);

@ -1,11 +1,12 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected"; import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected";
import { createUseChangeDataset } from "./useChangeDataset.js"; import { createUseChangeDataset } from "./useChangeDataset.js";
import type { LdoBase, LdSet, ShapeType } from "@ldo/ldo"; import { write, type LdoBase, type LdSet, type ShapeType } from "@ldo/ldo";
import type { QuadMatch } from "@ldo/rdf-utils"; import type { QuadMatch } from "@ldo/rdf-utils";
import type { UseMatchObjectOptions } from "../useMatchObject.js"; import type { UseMatchObjectOptions } from "../useMatchObject.js";
import { createUseMatchObject } from "../useMatchObject.js"; import { createUseMatchObject } from "../useMatchObject.js";
import type { useChangeReturn, useChangeSetData } from "./types.js"; import type { useChangeReturn, useChangeSetData } from "./types.js";
import { createProxyInteractOptions } from "@ldo/jsonld-dataset-proxy";
/** /**
* @internal * @internal
@ -21,7 +22,7 @@ export function createUseChangeMatchObject<Plugins extends ConnectedPlugin[]>(
/** /**
* Returns a list of matched objects that can be modified and committed * Returns a list of matched objects that can be modified and committed
*/ */
return function useChangeSubject<Type extends LdoBase>( return function useChangeMatchObject<Type extends LdoBase>(
shapeType: ShapeType<Type>, shapeType: ShapeType<Type>,
subject?: QuadMatch[0] | string, subject?: QuadMatch[0] | string,
predicate?: QuadMatch[1] | string, predicate?: QuadMatch[1] | string,
@ -32,28 +33,34 @@ export function createUseChangeMatchObject<Plugins extends ConnectedPlugin[]>(
options?.dataset, options?.dataset,
); );
const ldObjects = useMatchObject(shapeType, subject, predicate, graph, { const ldObject = useMatchObject(shapeType, subject, predicate, graph, {
dataset: transactionDataset, dataset: transactionDataset,
}); });
const setData = useCallback<useChangeSetData<LdSet<Type>, Plugins>>( const setData = useCallback<useChangeSetData<Type, Plugins>>(
(writeResource, changer, _other) => { (writeResource, changer, otherType?) => {
setDataset((dataset) => { setDataset((dataset) => {
const ldSet = dataset const ldObject = otherType
.usingType(shapeType) ? write(writeResource.uri).usingCopy(
.write(writeResource.uri) createProxyInteractOptions("dataset", dataset).usingCopy(
.matchObject(subject, predicate, graph); otherType,
)[0],
)[0]
: dataset
.usingType(shapeType)
.write(writeResource.uri)
.matchObject(subject, predicate, graph);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
changer(ldSet); changer(ldObject);
}); });
}, },
[setDataset, subject, predicate, graph, shapeType], [setDataset, subject, predicate, graph, shapeType],
); );
return useMemo( return useMemo(
() => [ldObjects, setData, commitData], () => [ldObject, setData, commitData],
[ldObjects, setData, commitData], [ldObject, setData, commitData],
); );
}; };
} }

@ -1,13 +1,14 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected"; import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected";
import { createUseChangeDataset } from "./useChangeDataset.js"; import { createUseChangeDataset } from "./useChangeDataset.js";
import type { LdoBase, LdSet, ShapeType } from "@ldo/ldo"; import { write, type LdoBase, type LdSet, type ShapeType } from "@ldo/ldo";
import type { QuadMatch } from "@ldo/rdf-utils"; import type { QuadMatch } from "@ldo/rdf-utils";
import type { useChangeReturn, useChangeSetData } from "./types.js"; import type { useChangeReturn, useChangeSetData } from "./types.js";
import { import {
createUseMatchSubject, createUseMatchSubject,
type UseMatchSubjectOptions, type UseMatchSubjectOptions,
} from "../useMatchSubject.js"; } from "../useMatchSubject.js";
import { createProxyInteractOptions } from "@ldo/jsonld-dataset-proxy";
/** /**
* @internal * @internal
@ -23,7 +24,7 @@ export function createUseChangeMatchSubject<Plugins extends ConnectedPlugin[]>(
/** /**
* Returns a list of matched subjects that can be modified and committed * Returns a list of matched subjects that can be modified and committed
*/ */
return function useChangeSubject<Type extends LdoBase>( return function useChangeMatchSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>, shapeType: ShapeType<Type>,
predicate?: QuadMatch[1] | string, predicate?: QuadMatch[1] | string,
object?: QuadMatch[2] | string, object?: QuadMatch[2] | string,
@ -34,28 +35,34 @@ export function createUseChangeMatchSubject<Plugins extends ConnectedPlugin[]>(
options?.dataset, options?.dataset,
); );
const ldSubjects = useMatchSubject(shapeType, predicate, object, graph, { const ldObject = useMatchSubject(shapeType, predicate, object, graph, {
dataset: transactionDataset, dataset: transactionDataset,
}); });
const setData = useCallback<useChangeSetData<LdSet<Type>, Plugins>>( const setData = useCallback<useChangeSetData<Type, Plugins>>(
(writeResource, changer, _other) => { (writeResource, changer, otherType?) => {
setDataset((dataset) => { setDataset((dataset) => {
const ldSet = dataset const ldObject = otherType
.usingType(shapeType) ? write(writeResource.uri).usingCopy(
.write(writeResource.uri) createProxyInteractOptions("dataset", dataset).usingCopy(
.matchSubject(predicate, object, graph); otherType,
)[0],
)[0]
: dataset
.usingType(shapeType)
.write(writeResource.uri)
.matchSubject(predicate, object, graph);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
changer(ldSet); changer(ldObject);
}); });
}, },
[setDataset, object, predicate, graph, shapeType], [setDataset, predicate, object, graph, shapeType],
); );
return useMemo( return useMemo(
() => [ldSubjects, setData, commitData], () => [ldObject, setData, commitData],
[ldSubjects, setData, commitData], [ldObject, setData, commitData],
); );
}; };
} }

@ -55,7 +55,6 @@ export function createUseChangeSubject<Plugins extends ConnectedPlugin[]>(
const setData = useCallback<useChangeSetData<Type, Plugins>>( const setData = useCallback<useChangeSetData<Type, Plugins>>(
(writeResource, changer, otherType?) => { (writeResource, changer, otherType?) => {
console.log("Setting data");
if (!subject) return; if (!subject) return;
setDataset((dataset) => { setDataset((dataset) => {
const ldObject = otherType const ldObject = otherType
@ -76,8 +75,6 @@ export function createUseChangeSubject<Plugins extends ConnectedPlugin[]>(
[setDataset, subject, shapeType], [setDataset, subject, shapeType],
); );
console.log("This is ldObject", ldObject);
return useMemo( return useMemo(
() => [ldObject, setData, commitData], () => [ldObject, setData, commitData],
[ldObject, setData, commitData], [ldObject, setData, commitData],

@ -50,8 +50,6 @@ export function createUseSubject<Plugins extends ConnectedPlugin[]>(
subject?: string | SubjectNode, subject?: string | SubjectNode,
options?: UseSubjectOptions<Plugins>, options?: UseSubjectOptions<Plugins>,
): Type | undefined { ): Type | undefined {
console.log("options", options);
const fromSubject = useCallback( const fromSubject = useCallback(
(builder: LdoBuilder<Type>) => { (builder: LdoBuilder<Type>) => {
if (!subject) return; if (!subject) return;

@ -14,8 +14,6 @@ const FormTest: FunctionComponent = () => {
"Example0", "Example0",
); );
console.log("This is data", data);
return ( return (
<div> <div>
<h1>Form</h1> <h1>Form</h1>
@ -24,7 +22,6 @@ const FormTest: FunctionComponent = () => {
onSubmit={async (e) => { onSubmit={async (e) => {
e.preventDefault(); e.preventDefault();
const result = await commitData(); const result = await commitData();
console.log(result);
}} }}
> >
{/* Primary name field */} {/* Primary name field */}
@ -33,7 +30,6 @@ const FormTest: FunctionComponent = () => {
value={data?.fn ?? ""} value={data?.fn ?? ""}
onChange={(e) => { onChange={(e) => {
setData(randomResource, (profile) => { setData(randomResource, (profile) => {
console.log("Inside changer");
profile.fn = e.target.value; profile.fn = e.target.value;
}) })
}} }}

@ -2,7 +2,7 @@
/// <reference types="@testing-library/jest-dom" /> /// <reference types="@testing-library/jest-dom" />
import { useResource, useChangeSubject, useSubject } from "./mockLdoMethods.js"; import { useResource, useChangeSubject, useSubject } from "./mockLdoMethods.js";
import type { FunctionComponent } from "react"; import type { FunctionComponent } from "react";
import React from "react"; import React, { useState } from "react";
import { describe, it, expect, beforeEach } from "vitest"; import { describe, it, expect, beforeEach } from "vitest";
import { render, screen, within } from "@testing-library/react"; import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
@ -15,34 +15,35 @@ import "@testing-library/jest-dom/vitest";
* accessible labels, and committed data shown via useSubject. * accessible labels, and committed data shown via useSubject.
*/ */
const FormTest: FunctionComponent = () => { const FormTest: FunctionComponent = () => {
const randomResource = useResource("random"); const randomResource = useResource("http://example.com/resource.ttl");
const submittedData = useSubject(SolidProfileShapeShapeType, "Example0"); const submittedData = useSubject(SolidProfileShapeShapeType, "Example0");
const [count, setCount] = useState(1);
const [data, setData, commitData] = useChangeSubject( const [data, setData, commitData] = useChangeSubject(
SolidProfileShapeShapeType, SolidProfileShapeShapeType,
randomResource,
"Example0", "Example0",
); );
return ( return (
<div> <div>
<h1>Form</h1> <h1>Form</h1>
<button onClick={() => setCount(count + 1)}>Rerender {count}</button>
<form <form
onSubmit={async (e) => { onSubmit={async (e) => {
e.preventDefault(); e.preventDefault();
const result = await commitData(); await commitData();
console.log(result);
}} }}
> >
{/* Primary name field */} {/* Primary name field */}
<input <input
aria-label="Name" aria-label="Name"
value={data?.fn ?? ""} value={data?.fn ?? ""}
onChange={(e) => onChange={(e) => {
setData((profile) => { setData(randomResource, (profile) => {
profile.fn = e.target.value; profile.fn = e.target.value;
}) });
} }}
/> />
{/* Friends */} {/* Friends */}
@ -53,15 +54,19 @@ const FormTest: FunctionComponent = () => {
aria-label={`Friend name for ${person["@id"]}`} aria-label={`Friend name for ${person["@id"]}`}
value={person?.fn ?? ""} value={person?.fn ?? ""}
onChange={(e) => onChange={(e) =>
setData((p) => { setData(
p.fn = e.target.value; randomResource,
}, person) (p) => {
p.fn = e.target.value;
},
person,
)
} }
/> />
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
setData((cData) => { setData(randomResource, (cData) => {
cData.knows?.delete(person); cData.knows?.delete(person);
}); });
}} }}
@ -75,9 +80,9 @@ const FormTest: FunctionComponent = () => {
type="button" type="button"
onClick={() => { onClick={() => {
// Auto-generate deterministic IDs: Example1, Example2, ... // Auto-generate deterministic IDs: Example1, Example2, ...
const count = data?.knows?.size ?? 0; const friendId = `Example${count}`;
const friendId = `Example${count + 1}`; setCount(count + 1);
setData((cData) => { setData(randomResource, (cData) => {
cData.knows?.add({ cData.knows?.add({
"@id": friendId, "@id": friendId,
type: set({ "@id": "Person" }), type: set({ "@id": "Person" }),
@ -89,7 +94,7 @@ const FormTest: FunctionComponent = () => {
Add Friend Add Friend
</button> </button>
<button type="submit">Submit</button> <input type="submit" value="Submit" />
</form> </form>
<hr /> <hr />
@ -190,15 +195,25 @@ describe("useChangeSubject", () => {
expect(screen.queryByTestId("friend-Example2")).not.toBeInTheDocument(); expect(screen.queryByTestId("friend-Example2")).not.toBeInTheDocument();
// 5) Submit -> committed data reflects Example0 + Example1 only // 5) Submit -> committed data reflects Example0 + Example1 only
await user.click(screen.getByRole("button", { name: /submit/i })); await user.click(screen.getByRole("button", { name: /Submit/i }));
// Form retained its values // Form retained its values
expect(nameInput).toHaveValue("Example0"); expect(nameInput).toHaveValue("Example0");
expect(friend1Input).toHaveValue("Example1"); expect(friend1Input).toHaveValue("Example1");
expect(screen.queryByTestId("friend-Example2")).not.toBeInTheDocument(); expect(screen.queryByTestId("friend-Example2")).not.toBeInTheDocument();
// Committed view updated // Wait for the submitted data to appear in the document before asserting
expect(submittedName).toHaveTextContent("Name: Example0"); const submittedSection2 = screen.getByRole("region", {
name: /submitted data/i,
});
const submittedName2 =
within(submittedSection2).getByTestId("submitted-name");
// Use `findByText` from the `within` helper to wait for the change
await within(submittedSection2).findByText("Name: Example0");
// Now that we've successfully waited, we can safely make our assertions
expect(submittedName2).toHaveTextContent("Name: Example0");
const itemsAfterSubmit = within(submittedList).getAllByRole("listitem"); const itemsAfterSubmit = within(submittedList).getAllByRole("listitem");
expect(itemsAfterSubmit).toHaveLength(1); expect(itemsAfterSubmit).toHaveLength(1);
expect(itemsAfterSubmit[0]).toHaveTextContent( expect(itemsAfterSubmit[0]).toHaveTextContent(

@ -11,10 +11,8 @@ export function updateDatasetInBulk<InAndOutQuad extends BaseQuad = BaseQuad>(
dataset: Dataset<InAndOutQuad>, dataset: Dataset<InAndOutQuad>,
datasetChanges: DatasetChanges<InAndOutQuad>, datasetChanges: DatasetChanges<InAndOutQuad>,
) { ) {
console.log("Update Dataset In Bulk");
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((dataset as any).bulk) { if ((dataset as any).bulk) {
console.log("Updating in bulk");
(dataset as IBulkEditableDataset<InAndOutQuad>).bulk(datasetChanges); (dataset as IBulkEditableDataset<InAndOutQuad>).bulk(datasetChanges);
} else { } else {
if (datasetChanges.added) { if (datasetChanges.added) {

Loading…
Cancel
Save