Skip to content

Commit 7aad50c

Browse files
committed
fix: properly init default child focus nodes
1 parent d651299 commit 7aad50c

File tree

10 files changed

+189
-9
lines changed

10 files changed

+189
-9
lines changed

.changeset/tall-rice-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hydrofoil/shaperone-core": patch
3+
---
4+
5+
Ensure default values are populated in resource graph
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { dash } from '@tpluscode/rdf-ns-builders'
1+
import { dash, rdfs, schema } from '@tpluscode/rdf-ns-builders'
22
import { html, SingleEditorComponent } from '@hydrofoil/shaperone-wc'
3+
import type { GraphPointer } from 'clownface'
4+
5+
function label(object?: GraphPointer) {
6+
return object?.out([rdfs.label, schema.name]).toArray()[0] || object?.value || ''
7+
}
38

49
export const shapeLink: SingleEditorComponent = {
510
editor: dash.DetailsEditor,
611

712
render({ value }, { focusOnObjectNode }) {
8-
return html`<a href="javascript:void(0)" @click="${focusOnObjectNode}">Edit ${value.object?.value}</a>`
13+
return html`<a href="javascript:void(0)" @click="${focusOnObjectNode}">Edit ${label(value.object)}</a>`
914
},
1015
}

packages/core/models/editors/lib/match.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ function valuePlaceholder(shape: PropertyShape): GraphPointer {
2828
case 'http://www.w3.org/ns/shacl#IRIOrLiteral':
2929
return shape.pointer.namedNode('')
3030
default: {
31+
if (shape.class) {
32+
return shape.pointer.blankNode()
33+
}
3134
if (shape.languageIn.length || shape.datatype?.equals(xsd.langString)) {
3235
return shape.pointer.literal('', shape.languageIn[0] || 'en')
3336
}

packages/core/models/forms/reducers/replaceFocusNodes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { FormState } from '../index'
55

66
type StackAction = {appendToStack?: true} | {replaceStack?: true}
77

8-
type Params = Parameters<typeof initialiseFocusNode>[0] & StackAction & BaseParams
8+
export type Params = Parameters<typeof initialiseFocusNode>[0] & StackAction & BaseParams
99

1010
export const createFocusNodeState = formStateReducer((state: FormState, { focusNode, ...rest }: Params) => produce(state, (draft) => {
1111
draft.focusNodes[focusNode.value] = initialiseFocusNode({

packages/core/models/forms/reducers/updateObject.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Term } from 'rdf-js'
22
import type { PropertyShape } from '@rdfine/shacl'
33
import produce from 'immer'
4-
import { MultiPointer } from 'clownface'
4+
import { GraphPointer, MultiPointer } from 'clownface'
55
import { BaseParams, formStateReducer } from '../../index'
66
import type { FormState, PropertyObjectState } from '../index'
77
import type { FocusNode } from '../../../index'
@@ -54,3 +54,26 @@ export const setPropertyObjects = formStateReducer((state: FormState, { focusNod
5454
}
5555
})
5656
}))
57+
58+
export interface SetObjectValueParams extends BaseParams {
59+
focusNode: FocusNode
60+
property: PropertyShape
61+
object: PropertyObjectState
62+
value: GraphPointer
63+
editors: EditorsState
64+
}
65+
66+
export const setObjectValue = formStateReducer((state: FormState, { focusNode, property, object, value, editors }: SetObjectValueParams) => produce(state, (draft) => {
67+
const focusNodeState = draft.focusNodes[focusNode.value]
68+
const propertyState = focusNodeState.properties.find(p => p.shape.equals(property))
69+
70+
const objectState = propertyState?.objects.find(o => o.key === object.key)
71+
if (!objectState) {
72+
return
73+
}
74+
75+
const suitableEditors = editors.matchSingleEditors({ shape: property, object: value })
76+
objectState.editors = suitableEditors
77+
objectState.selectedEditor = suitableEditors[0]?.term
78+
objectState.object = value
79+
}))
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { defaultValue } from '../../lib/defaultValue'
2+
import { getPathProperty } from '../../lib/property'
3+
import { notify } from '../../lib/notify'
4+
import type { Store } from '../../../../state'
5+
import { Params } from '../../../forms/reducers/replaceFocusNodes'
6+
7+
export default function createFocusNodeState(store: Store) {
8+
return function ({ form, focusNode }: Params) {
9+
const dispatch = store.getDispatch()
10+
const { editors } = store.getState()
11+
12+
for (const property of store.getState().forms.get(form)?.focusNodes[focusNode.value].properties || []) {
13+
let shouldNotify = false
14+
for (const object of property.objects) {
15+
if (!object.object) {
16+
const [value] = defaultValue(property.shape, focusNode)?.toArray() || []
17+
18+
if (value) {
19+
shouldNotify = true
20+
focusNode.addOut(getPathProperty(property.shape)!.id, value)
21+
dispatch.forms.setObjectValue({
22+
form,
23+
focusNode,
24+
property: property.shape,
25+
object,
26+
value,
27+
editors,
28+
})
29+
}
30+
}
31+
}
32+
33+
if (shouldNotify) {
34+
notify({
35+
form,
36+
focusNode,
37+
property: property.shape,
38+
store,
39+
})
40+
}
41+
}
42+
}
43+
}

packages/core/models/resources/effects/forms/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import addFormField from './addFormField'
33
import updateObject from './updateObject'
44
import removeObject from './removeObject'
55
import replaceObject from './setPropertyObjects'
6+
import createFocusNodeState from './createFocusNodeState'
67

78
export default function (store: Store) {
89
return {
910
'forms/addFormField': addFormField(store),
1011
'forms/updateObject': updateObject(store),
1112
'forms/removeObject': removeObject(store),
1213
'forms/setPropertyObjects': replaceObject(store),
14+
'forms/createFocusNodeState': createFocusNodeState(store),
1315
}
1416
}

packages/core/models/resources/lib/defaultValue.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ export function defaultValue(property: PropertyShape, focusNode: FocusNode): Mul
1111
return focusNode.node(property.defaultValue)
1212
}
1313

14-
if (property.class && (nodeKind?.equals(sh.IRI) || nodeKind?.equals(sh.BlankNode) || nodeKind?.equals(sh.BlankNodeOrIRI))) {
14+
const propertyClass = property.class
15+
if (propertyClass || nodeKind?.equals(sh.IRI) || nodeKind?.equals(sh.BlankNode) || nodeKind?.equals(sh.BlankNodeOrIRI)) {
1516
const resourceNode: GraphPointer<ResourceIdentifier> = nodeKind?.equals(sh.IRI) ? focusNode.namedNode('') : focusNode.blankNode()
16-
const propertyClass = property.class
1717
if (propertyClass && !property.get(dash.editor)?.equals(dash.InstancesSelectEditor)) {
1818
resourceNode.addOut(rdf.type, propertyClass.id)
1919
}

packages/core/test/models/editors/lib/match.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it } from 'mocha'
22
import { PropertyShape } from '@rdfine/shacl'
3-
import { dash } from '@tpluscode/rdf-ns-builders'
3+
import { dash, schema } from '@tpluscode/rdf-ns-builders'
44
import { NamedNode } from 'rdf-js'
55
import { expect } from 'chai'
66
import { testStore } from '../../forms/util'
@@ -77,6 +77,22 @@ describe('models/editors/lib/match', () => {
7777
// then
7878
expect(matches[2].term).to.deep.eq(dash.EnumSelectEditor)
7979
})
80+
81+
it('matches blank nodes when property has sh:class', () => {
82+
// given
83+
shape.class = schema.Person as any
84+
singleEditors(
85+
[dash.TextFieldEditor, (prop, node) => (node.term.termType === 'BlankNode' ? 0 : 10)],
86+
[dash.DetailsEditor, (prop, node) => (node.term.termType === 'BlankNode' ? 10 : 0)],
87+
)
88+
89+
// when
90+
const matches = matchSingleEditors.call(editors, { shape })
91+
92+
// then
93+
expect(matches).to.have.length(1)
94+
expect(matches[0].term).to.deep.eq(dash.DetailsEditor)
95+
})
8096
})
8197

8298
describe('matchMultiEditors', () => {

packages/core/test/models/forms/reducers/updateObject.test.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import ns from '@rdf-esm/namespace'
33
import cf from 'clownface'
44
import $rdf from 'rdf-ext'
55
import { expect } from 'chai'
6-
import { setPropertyObjects } from '../../../../models/forms/reducers/updateObject'
7-
import { testFocusNodeState, testState, testStore } from '../util'
6+
import * as sinon from 'sinon'
7+
import { setObjectValue, setPropertyObjects } from '../../../../models/forms/reducers/updateObject'
8+
import { RecursivePartial, testFocusNodeState, testObjectState, testState, testStore } from '../util'
89
import { propertyShape } from '../../../util'
10+
import { Store } from '../../../../state'
11+
import { FormState } from '../../../../models/forms'
912

1013
const ex = ns('http://example.com/')
1114

@@ -53,4 +56,84 @@ describe('core/models/forms/reducers/updateObject', () => {
5356
expect(values).to.include.members(['bar1', 'bar2', 'bar3'])
5457
})
5558
})
59+
60+
describe('setObjectValue', () => {
61+
let store: Store
62+
let form: symbol
63+
let formState: {
64+
focusNodes: RecursivePartial<FormState['focusNodes']>
65+
focusStack: FormState['focusStack']
66+
}
67+
68+
beforeEach(() => {
69+
({ form, store } = testStore())
70+
formState = store.getState().forms.get(form)!
71+
})
72+
73+
it('recalculates editors', () => {
74+
// given
75+
const graph = cf({ dataset: $rdf.dataset() })
76+
const property = propertyShape()
77+
const object = testObjectState(graph.literal('foo'))
78+
const { editors } = store.getState()
79+
editors.matchSingleEditors = sinon.stub().returns([])
80+
const focusNode = graph.blankNode()
81+
formState.focusNodes = {
82+
[focusNode.value]: {
83+
properties: [{
84+
shape: property,
85+
objects: [object],
86+
}],
87+
},
88+
}
89+
90+
// when
91+
const value = graph.literal('bar')
92+
setObjectValue(store.getState().forms, {
93+
form,
94+
focusNode,
95+
editors,
96+
object,
97+
property,
98+
value,
99+
})
100+
101+
// then
102+
expect(editors.matchSingleEditors).to.have.been.calledWith({
103+
shape: property,
104+
object: value,
105+
})
106+
})
107+
108+
it('sets new value', () => {
109+
// given
110+
const graph = cf({ dataset: $rdf.dataset() })
111+
const property = propertyShape()
112+
const object = testObjectState(graph.literal('foo'))
113+
const focusNode = graph.blankNode()
114+
formState.focusNodes = {
115+
[focusNode.value]: {
116+
properties: [{
117+
shape: property,
118+
objects: [object],
119+
}],
120+
},
121+
}
122+
123+
// when
124+
const value = graph.literal('bar')
125+
const afterState = setObjectValue(store.getState().forms, {
126+
form,
127+
focusNode,
128+
editors: store.getState().editors,
129+
object,
130+
property,
131+
value,
132+
})
133+
134+
// then
135+
const objectAfter = afterState.get(form)?.focusNodes[focusNode.value].properties[0].objects[0]
136+
expect(objectAfter?.object?.term).to.deep.eq($rdf.literal('bar'))
137+
})
138+
})
56139
})

0 commit comments

Comments
 (0)