Skip to content

List things of a specific type, with a custom display #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 27 additions & 1 deletion core/src/Store.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { when } from "jest-when";
import { sym } from "rdflib";
import { quad, sym } from "rdflib";
import { Parser as SparqlParser, Update } from "sparqljs";
import { AuthenticatedFetch, PodOsSession } from "./authentication";
import { Store } from "./Store";
Expand Down Expand Up @@ -450,6 +450,32 @@ describe("Store", () => {
);
});
});

describe("findMembers", () => {
it("finds instances of classes and subclasses", () => {
const store = new Store({} as PodOsSession);
store.graph.addAll([
quad(
sym("http://recipe.test/1"),
sym("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
sym("http://schema.org/Recipe"),
),
quad(
sym("http://recipe.test/RecipeClass"),
sym("http://www.w3.org/2000/01/rdf-schema#subClassOf"),
sym("http://schema.org/Recipe"),
),
quad(
sym("http://recipe.test/2"),
sym("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
sym("http://recipe.test/RecipeClass"),
),
]);
const members = store.findMembers("http://schema.org/Recipe");
expect(members).toContain("http://recipe.test/1");
expect(members).toContain("http://recipe.test/2");
});
});
});

export function thenSparqlUpdateIsSentToUrl(
Expand Down
8 changes: 8 additions & 0 deletions core/src/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,12 @@ export class Store {
async executeUpdate(operation: UpdateOperation) {
await executeUpdate(this.fetcher, this.updater, operation);
}

/**
* Finds instances of the given class or its sub-classes
* @param classUri
*/
findMembers(classUri: string): string[] {
return Object.keys(this.graph.findMemberURIs(sym(classUri)));
Copy link
Contributor

Choose a reason for hiding this comment

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

This does more than is covered by tests. With the current test you could just return all things from the store. Make sure to add tests that asserts only things of given type are found

}
}
24 changes: 24 additions & 0 deletions elements/src/components/pos-list/pos-list.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { PosList } from './pos-list';
import { when } from 'jest-when';

describe('pos-list', () => {
it('contains only template initially', async () => {
Expand Down Expand Up @@ -70,6 +71,29 @@ describe('pos-list', () => {
expect(el.querySelectorAll('pos-resource')).toHaveLength(2);
});

it('renders if-typeof objects', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

The test name should be less technical. E.g. renders the template for all things of the given type

const page = await newSpecPage({
components: [PosList],
html: `
<pos-list if-typeof="http://schema.org/Recipe">
<template>
Test
</template>
</pos-list>`,
});
const os = {
store: {
findMembers: jest.fn(),
},
};
when(os.store.findMembers).calledWith('http://schema.org/Recipe').mockReturnValue(['https://recipe.test/1']);
await page.rootInstance.receivePodOs(os);
await page.waitForChanges();

const el: HTMLElement = page.root as unknown as HTMLElement;
expect(el.querySelector('pos-resource')?.getAttribute('about')).toEqual('https://recipe.test/1');
});

it('displays error on missing template', async () => {
const page = await newSpecPage({
components: [PosList],
Expand Down
21 changes: 17 additions & 4 deletions elements/src/components/pos-list/pos-list.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import { Thing } from '@pod-os/core';
import { PodOS, Thing } from '@pod-os/core';
import { Component, Element, Event, h, Prop, State } from '@stencil/core';
import { ResourceAware, ResourceEventEmitter, subscribeResource } from '../events/ResourceAware';
import { PodOsAware, PodOsEventEmitter, subscribePodOs } from '../events/PodOsAware';

@Component({
tag: 'pos-list',
shadow: false,
})
export class PosList implements ResourceAware {
export class PosList implements PodOsAware, ResourceAware {
/**
* URI of the predicate to follow
*/
@Prop() rel: string;
/**
* URI of a class for which instances will be listed
*/
@Prop() ifTypeof: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

How about all-of-type? I don't like the if in there, because it then sounds like a conditional structure, not like a filtered list. Just a thought

/**
* Whether listed resources should be fetched before being displayed
*/
@Prop() fetch: boolean = false;

@Element() host: HTMLElement;
@State() error: string = null;
@State() os: PodOS;
@State() resource: Thing;
@State() items: string[] = [];
@State() templateString: string;

@Event({ eventName: 'pod-os:resource' })
subscribeResource: ResourceEventEmitter;
@Event({ eventName: 'pod-os:init' })
subscribePodOs: PodOsEventEmitter;

componentWillLoad() {
subscribeResource(this);
if (this.rel) subscribeResource(this);
if (this.ifTypeof) subscribePodOs(this);
const templateElement = this.host.querySelector('template');
if (templateElement == null) {
this.error = 'No template element found';
Expand All @@ -36,10 +45,14 @@ export class PosList implements ResourceAware {
}

receiveResource = (resource: Thing) => {
this.items = [];
if (this.rel) this.items = resource.relations(this.rel).flatMap(relation => relation.uris);
};

receivePodOs = async (os: PodOS) => {
this.os = os;
this.items = os.store.findMembers(this.ifTypeof);
};

render() {
if (this.error) return this.error;
const elems = this.items.map(it => (
Expand Down