Skip to content

Commit

Permalink
wip: instances select editor
Browse files Browse the repository at this point in the history
  • Loading branch information
tpluscode committed Dec 13, 2024
1 parent c36924e commit e722b2f
Show file tree
Hide file tree
Showing 54 changed files with 610 additions and 494 deletions.
6 changes: 5 additions & 1 deletion demos/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@coreui/react": "^5.4.1",
"@coreui/coreui": "^5.2.0",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@hydrofoil/shaperone-core": "^0.12.1",
"@hydrofoil/shaperone-wc": "^0.8.0",
"@hydrofoil/shaperone-wc-material": "^0.6.1",
"@hydrofoil/shaperone-wc-shoelace": "^0.4.1",
Expand All @@ -27,18 +28,21 @@
"@storybook/components": "^8.4.5",
"@storybook/web-components": "^8.4.5",
"@storybook/web-components-vite": "^8.4.5",
"@tpluscode/rdf-ns-builders": "^4.3.0",
"@vitejs/plugin-react": "^4.3.3",
"@zazuko/env": "^2",
"lit": "^3",
"onetime": "^7.0.0",
"react-syntax-highlighter": "^15.6.1",
"remark-gfm": "^4.0.0",
"rollup-plugin-polyfill-node": "^0.13.0",
"sparql-http-client": "^3.0.1",
"storybook": "^8.4.5",
"string-to-stream": "^3.0.1",
"ts-deepmerge": "^7"
},
"devDependencies": {
"@types/react-syntax-highlighter": "^15.5.13"
"@types/react-syntax-highlighter": "^15.5.13",
"@types/sparql-http-client": "^3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PREFIX ex: <http://example.org/>
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix wd: <http://www.wikidata.org/entity/>

ex:instance <http://schema.org/alumniOf> wd:Q184478 .

24 changes: 24 additions & 0 deletions demos/storybook/shapes/editors/dash/InstancesSelect/no-labels.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
PREFIX dash: <http://datashapes.org/dash#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX schema: <http://schema.org/>
PREFIX sh: <http://www.w3.org/ns/shacl#>

<> a sh:NodeShape ;
sh:property
[
sh:path schema:alumniOf ;
sh:class wd:Q3918 ;
sh:name "Alma mater" ;
sh:minCount 1 ;
sh:maxCount 1 ;
dash:editor dash:InstancesSelectEditor ;
] ;
.

wd:Q184478 a wd:Q3918 .
wd:Q1860208 a wd:Q3918 .
wd:Q49108 a wd:Q3918 .
wd:Q34433 a wd:Q3918 .
wd:Q35794 a wd:Q3918 .
wd:Q13371 a wd:Q3918 .
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PREFIX dash: <http://datashapes.org/dash#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX schema: <http://schema.org/>
Expand All @@ -11,6 +12,7 @@ PREFIX sh: <http://www.w3.org/ns/shacl#>
sh:name "Alma mater" ;
sh:minCount 1 ;
sh:maxCount 1 ;
dash:editor dash:InstancesSelectEditor ;
] ;
.

Expand Down
36 changes: 0 additions & 36 deletions demos/storybook/stories/DASH/EnumSelectEditor.stories.ts

This file was deleted.

6 changes: 3 additions & 3 deletions demos/storybook/stories/Editors/DASH.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ DASH is a set of auxiliary specifications by TopQuadrant's SHACL author Holger K

DASH defines two types of editors: `dash:SingleEditor` and `dash:MultiEditor` which represent a user input element of a form.

As the names imply, the former are editors for individual triple object while the latter replaces all values for a given property.
As the names imply, the former are editors for individual triple's object while the latter replaces all values for a given property.

It also provides a set of instances of the `dash:SingleEditor` type. The table below lists them and indicates which are implemented as [matchers](editors/matchers.md) and [components](components/implement.md) by shaperone. Below you will also find some additional information about some of them.

Expand All @@ -28,7 +28,7 @@ It also provides a set of instances of the `dash:SingleEditor` type. The table b
| [`dash:DateTimePickerEditor`](http://datashapes.org/forms.html#DateTimePickerEditor) | A calendar for selecting date and time |||
| [`dash:DetailsEditor`](http://datashapes.org/forms.html#DetailsEditor) | A drill-down-style editor for editing child focus nodes |||
| [`dash:EnumSelectEditor`](http://datashapes.org/forms.html#EnumSelectEditor) | A dropdown with choices from a closed set of elements || 🐲 |
| [`dash:InstancesSelectEditor`](http://datashapes.org/forms.html#InstancesSelectEditor) | A dropdown to choose among instances a specific type as indicated by `sh:class` shape property | | 🐉 |
| [`dash:InstancesSelectEditor`](http://datashapes.org/forms.html#InstancesSelectEditor) | A dropdown to choose among instances a specific type as indicated by `sh:class` shape property | ⚠️ | 🐲 |
| [`dash:RichTextEditor`](http://datashapes.org/forms.html#RichTextEditor) | A text editor with formatting, which returns HTML markup |||
| [`dash:TextAreaEditor`](http://datashapes.org/forms.html#TextAreaEditor) | Multiline text box |||
| [`dash:TextFieldEditor`](http://datashapes.org/forms.html#TextFieldEditor) | Single line text box |||
Expand All @@ -37,5 +37,5 @@ It also provides a set of instances of the `dash:SingleEditor` type. The table b
| [`dash:URIEditor`](http://datashapes.org/forms.html#URIEditor) | A text box for typing in URIs |||

<CCallout color="info">
Most of the default behaviour of components can be easily customized in your application. The [components page](components/implement.md) provides information about necessary implementation steps.
Most of the default behaviour of components can be customized in your application. The [components page](components/implement.md) provides information about necessary implementation steps.
</CCallout>
35 changes: 33 additions & 2 deletions demos/storybook/stories/Editors/DASH/InstancesSelectEditor.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Stories } from '@storybook/blocks';
import { Markdown, Stories, Story } from '@storybook/blocks'
import { CCallout } from '@coreui/react';
import * as InstanceSelect from './InstancesSelectEditor.stories';
import { Meta } from '@storybook/blocks'
import fetchDecoratorRaw from './InstancesSelectEditor/fetch.ts?raw'

# InstancesSelectEditor

Expand All @@ -12,6 +13,36 @@ an "instances selector" presents a dropdown, by default populated with instances
[Hydra integration library](../?path=/docs/extensions-hydra--docs) can be used to extend the functionality of Instances Select editor so that instance data is dereferenced from external resources.
</CCallout>

<CCallout color="warning">
Because both the matcher of `dash:InstancesSelectEditor` and `dash:DetailsEditor` return `null` for similar
objects, it is advised to always explicitly set the `dash:editor`, or decorate the matchers.
</CCallout>

## Stories

<Meta of={InstanceSelect}/>

<Stories />
### Resources by sh:class

Just as with enum select, `rdfs:label` is used for option text

<Story of={InstanceSelect.Wikidata} />

### Resources without labels

If the Shapes Graph does not contain `rdfs:label` for the options, the editor can request
that the instance data can be fetched from the server and merged with the Shapes Graph.

To do that, the editor must be wrapped in a decorator that provides the instance data.

In the example below, such a decorator is presented which fetches instance data from Wikidata using its public SPARQL endpoint.

<Markdown>
{`
\`\`\`ts
${fetchDecoratorRaw}
\`\`\`
`}
</Markdown>

<Story of={InstanceSelect.WikidataNoLabels} />
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import type { StoryObj as Story } from '@storybook/web-components'
import { defaultMeta } from '../../common.js'
import { render } from '../../render.js'
import { createStory, defaultMeta } from '../../common.js'
import instances from '../../../shapes/editors/dash/InstancesSelect/wikidata.ttl?raw'
import instancesNoLabels from '../../../shapes/editors/dash/InstancesSelect/no-labels.ttl?raw'
import noLabelsData from '../../../shapes/editors/dash/InstancesSelect/no-labels.data.ttl?raw'
import { configure as configureFetch } from './InstancesSelectEditor/fetch.js'

const meta = {
...defaultMeta,
}

export default meta

/**
* Just as with enum select, `rdfs:label` is used for option text
*/
export const Wikidata: Story = {
export const Wikidata: Story = createStory({
name: 'Resources by sh:class',
args: {
shapes: instances,
customPrefixes: {
wd: 'http://www.wikidata.org/entity/',
},
shapes: instances,
prefixes: ['rdfs'],
customPrefixes: {
wd: 'http://www.wikidata.org/entity/',
},
render,
}
})()

export const WikidataNoLabels: Story = createStory({
name: 'Resources without labels',
shapes: instancesNoLabels,
data: noLabelsData,
focusNode: 'http://example.org/instance',
prefixes: ['rdfs', 'schema'],
customPrefixes: {
wd: 'http://www.wikidata.org/entity/',
ex: 'http://example.org/',
},
})(configureFetch)
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { InstanceConfigCallback } from '@hydrofoil/shaperone-wc/configure.js'
import SparqlClient from 'sparql-http-client/ParsingClient.js'
import type { ComponentDecorator, InstancesSelectEditor } from '@hydrofoil/shaperone-core/components.js'
import type { ComponentConstructor } from '@hydrofoil/shaperone-core/models/components/index.js'
import type { GraphPointer } from 'clownface'
import type { PropertyValues } from 'lit'
import env from '@hydrofoil/shaperone-core/env.js'

const wikidata = new SparqlClient({
endpointUrl: 'https://query.wikidata.org/sparql',
})

const labelQuery = `
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
construct {
?id rdfs:label ?l
} where {
?id rdfs:label ?l
}`

const WikidataFetchMixin: ComponentDecorator<InstancesSelectEditor> = {
applicableTo(component: ComponentConstructor) {
return component.editor.equals(env().ns.dash.InstancesSelectEditor)
},

decorate(base) {
return class InstanceFetchingComponent extends base implements InstancesSelectEditor {
private loading = false
private loaded = env().clownface()

updated(_changedProperties: PropertyValues) {
super.updated(_changedProperties)

if (_changedProperties.has('choices') && this.choices.length) {
this.loadLabels(...this.choices)
}
}

async loadLabels(...iris: GraphPointer[]) {
if (this.loading) {
// avoid multiple requests
return
}

this.loading = true
try {
const toFetch = iris
// skip non-wikidata resources
.filter(resource => resource.value.startsWith('http://www.wikidata.org/entity/'))
// skip resources for which we already have data
.filter(resource => !this.loaded.node(resource)
.out(env().ns.rdfs.label).terms.length)

if (toFetch.length === 0) {
return
}

const query = `${labelQuery}
VALUES ?id {
${toFetch.map(resource => `<${resource.value}>`).join(' ')}
}`

const loaded = await wikidata.query.construct(query)
for (const quad of loaded) {
this.loaded.dataset.add(quad)
}

this.setChoices()
} finally {
this.loading = false
}
}

setChoices() {
const property = this.property.shape

// used combined triples from the Shapes Graph and the fetched data
const loadedGraph = env().clownface({
dataset: env().dataset([...this.loaded.dataset, ...property.pointer.dataset]),
})

if (property.class) {
this.choices = loadedGraph
.has(env().ns.rdf.type, property.class.id)
.toArray()
} else {
this.choices = []
}
}
}
},
}

export const configure: InstanceConfigCallback = ({ components }) => {
// register the decorator in the component registry
components.decorate(WikidataFetchMixin)
}
12 changes: 9 additions & 3 deletions demos/storybook/stories/common.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { Meta, StoryObj as Story } from '@storybook/web-components'
import type { ConfigCallback } from '@hydrofoil/shaperone-wc'
import { merge } from 'ts-deepmerge'
import type { InstanceConfigCallback } from '@hydrofoil/shaperone-wc/configure.js'
import { render } from './render.js'
import groups from '../shapes/groups.ttl?raw'
import textEditors from '../shapes/text-editors.ttl?raw'

interface StoryFactory {
(configure: ConfigCallback, overrides?: Partial<Omit<Story, 'render' | 'loaders'>>): Story
(configure?: InstanceConfigCallback, overrides?: Partial<Omit<Story, 'render' | 'loaders'>>): Story
}

export const defaultMeta: Meta = {
Expand Down Expand Up @@ -50,9 +50,11 @@ interface CreateStory {
data?: string
focusNode?: string
prefixes?: string[]
customPrefixes?: Record<string, string>
debug?: boolean
}

export function createStory({ name, prefixes, ...args }: CreateStory) {
export function createStory({ name, prefixes, customPrefixes, ...args }: CreateStory) {
const defaults = {
name,
args,
Expand All @@ -62,6 +64,10 @@ export function createStory({ name, prefixes, ...args }: CreateStory) {
defaults.args.prefixes = prefixes.join(',')
}

if (customPrefixes) {
defaults.args.customPrefixes = customPrefixes
}

return createStoryObj(defaults)
}

Expand Down
Loading

0 comments on commit e722b2f

Please sign in to comment.