Skip to content

Commit

Permalink
feat: Support collection object transfer (#267)
Browse files Browse the repository at this point in the history
* feat: collection field can be edited properly WIP

* feat: added dropdown for collections WIP

* feat: added dropdown for licenses WIP

* test: added tests WIP

* test: upped coverage in components package WIP

* test: updated coverage for manage

* fix: removed old form field
  • Loading branch information
lem-onade authored Jun 2, 2021
1 parent cbfd9a3 commit 5b37a82
Show file tree
Hide file tree
Showing 18 changed files with 195 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ArgumentError, Collection } from '@netwerk-digitaal-erfgoed/solid-crs-core';
import { Observable, of } from 'rxjs';
import { of } from 'rxjs';
import { interpret, Interpreter } from 'xstate';
import { FormElementComponent } from './form-element.component';
import { FormValidatorResult } from './form-validator-result';
Expand All @@ -24,7 +24,7 @@ describe('FormElementComponent', () => {
.withContext({
data: { uri: '', name: 'Test', description: 'description' },
original: { uri: '', name: 'Test', description: 'description' },
validation: [],
validation: [ { field: 'name', message: 'lorem' } ],
}),
);

Expand Down Expand Up @@ -84,7 +84,27 @@ describe('FormElementComponent', () => {

});

xit('should send SUBMITTED event when enter keypress', async (done) => {
it('should allow slotted select field', async () => {

component.field = 'description';
const select = window.document.createElement('select');
select.slot = 'input';
const option = window.document.createElement('option');
option.id = 'description';
select.appendChild(option);
component.appendChild(select);
component.removeChild(input);

window.document.body.appendChild(component);
await component.updateComplete;

expect((window.document.body.getElementsByTagName('nde-form-element')[0].shadowRoot.querySelector<HTMLSlotElement>('.field slot').assignedElements()[0] as HTMLSelectElement).children.length).toBe(1);

});

it('should send SUBMITTED event when enter keypress', async (done) => {

component.submitOnEnter = true;

machine.onEvent(((event) => {

Expand All @@ -96,6 +116,8 @@ describe('FormElementComponent', () => {

}));

machine.start();

window.document.body.appendChild(component);
await component.updateComplete;

Expand Down
25 changes: 19 additions & 6 deletions packages/solid-crs-components/lib/forms/form-element.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,17 +176,30 @@ export class FormElementComponent<T> extends RxLitElement {
}

this.inputs = slot.assignedNodes({ flatten: true })?.filter(
(element) => element instanceof HTMLInputElement
(element) => element instanceof HTMLInputElement || element instanceof HTMLSelectElement
).map((element) => element as HTMLInputElement);

this.inputs?.forEach((element) => {

// Set the input field's default value.
const fieldData = data[this.field];
element.value = fieldData && (typeof fieldData === 'string' || typeof fieldData === 'number') ? fieldData.toString() : '';

// Send event when input field's value changes.
element.addEventListener('input', debounce(() => actor.send({ type: FormEvents.FORM_UPDATED, value: element.value, field } as FormUpdatedEvent), this.debounceTimeout));
if (element instanceof HTMLSelectElement) {

// Set the input field's default value.
element.namedItem(fieldData && (typeof fieldData === 'string' || typeof fieldData === 'number') ? fieldData.toString() : '').selected = true;

// Send event when input field's value changes.
element.addEventListener('input', () => actor.send({ type: FormEvents.FORM_UPDATED, value: element.options[element.selectedIndex].id, field } as FormUpdatedEvent));

} else {

// Set the input field's default value.
element.value = fieldData && (typeof fieldData === 'string' || typeof fieldData === 'number') ? fieldData.toString() : '';

// Send event when input field's value changes.
element.addEventListener('input', debounce(() => actor.send({ type: FormEvents.FORM_UPDATED, value: element.value, field } as FormUpdatedEvent), this.debounceTimeout));

}

// Listen for Enter presses to submit
if (this.submitOnEnter) {
Expand Down Expand Up @@ -290,7 +303,7 @@ export class FormElementComponent<T> extends RxLitElement {
flex: 1 0;
border: var(--border-normal) solid var(--colors-foreground-normal);
}
.form-element .content .field ::slotted(input) {
.form-element .content .field ::slotted(input), .form-element .content .field ::slotted(select) {
padding: 0 var(--gap-normal);
flex: 1 0;
height: 44px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,17 @@ describe('SidebarItemComponent', () => {

});

it('should remove border and padding class when properties are set', async () => {

component.padding = false;
component.showBorder = false;

window.document.body.appendChild(component);
await component.updateComplete;

expect(window.document.body.getElementsByTagName('nde-sidebar-item')[0].shadowRoot.querySelectorAll('.padding').length).toEqual(0);
expect(window.document.body.getElementsByTagName('nde-sidebar-item')[0].shadowRoot.querySelectorAll('.border').length).toEqual(0);

});

});
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ describe('SidebarListComponent', () => {

});

it('should call select when select is clicked', async () => {
it('should set selected when item is clicked', async () => {

const event = document.createEvent('MouseEvent');
const el = document.createElement('nde-sidebar-list-item');
const el = document.createElement('nde-sidebar-list-item') as HTMLSlotElement;
el.name = 'item';

expect(el.hasAttribute('selected')).toBeFalsy();

Expand All @@ -58,6 +59,27 @@ describe('SidebarListComponent', () => {

});

it('should not set selected when title is clicked', async () => {

const event = document.createEvent('MouseEvent');
const el = document.createElement('nde-sidebar-list-item') as HTMLSlotElement;
el.name = 'title';

expect(el.hasAttribute('selected')).toBeFalsy();

const selectSpy = spyOn(component, 'select').and.callThrough();

window.document.body.appendChild(component);
await component.updateComplete;

event.composedPath = jest.fn(() => [ el ]);
component.select(event);

expect(selectSpy).toHaveBeenCalledTimes(1);
expect(el.hasAttribute('selected')).toBeFalsy();

});

it('should call select when select is clicked', async () => {

const selectSpy = spyOn(component, 'select');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class SidebarListComponent extends RxLitElement {

const element = event.composedPath().find((el: Element) => el.localName === 'nde-sidebar-list-item') as Element;

if(element && !element.hasAttribute('isTitle')){
if(element && (element as HTMLSlotElement).name === 'item'){

for(let i = 0; i < this.children.length; i++) {

Expand Down
8 changes: 4 additions & 4 deletions packages/solid-crs-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@
],
"coverageThreshold": {
"global": {
"branches": 82.69,
"functions": 89.23,
"lines": 96.92,
"statements": 96.38
"branches": 89.38,
"functions": 96.97,
"lines": 100,
"statements": 99.11
}
},
"coveragePathIgnorePatterns": [
Expand Down
2 changes: 1 addition & 1 deletion packages/solid-crs-manage/lib/app-root.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class AppRootComponent extends RxLitElement {
state: State<AppContext>;

/**
* The state of this component.
* A list of all collections.
*/
@internalProperty()
collections: Collection[];
Expand Down
3 changes: 3 additions & 0 deletions packages/solid-crs-manage/lib/app.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ export const appMachine = (
{
id: AppActors.OBJECT_MACHINE,
src: objectMachine(objectStore),
data: (context) => ({
collections: context.collections,
}),
onError: {
actions: send({ type: AppEvents.ERROR }),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,26 @@ describe('CollectionObjectSolidStore', () => {

client.getSolidDataset = jest.fn(async () => 'test-dataset');
client.getThing = jest.fn(() => 'test-thing');
client.getUrl = jest.fn(() => 'test-url');
client.getUrl = jest.fn(() => 'http://test-uri/');
client.setThing = jest.fn(() => 'test-thing');
client.removeThing = jest.fn(() => 'test-thing');
client.saveSolidDatasetAt = jest.fn(async () => 'test-dataset');
client.addUrl = jest.fn(() => 'test-url');
client.addStringNoLocale = jest.fn(() => 'test-url');
client.addStringWithLocale = jest.fn(() => 'test-url');
client.addInteger = jest.fn(() => 'test-url');

await expect(service.save(mockObject)).resolves.toEqual(mockObject);
const result = await service.save(mockObject)
;

expect(result).toEqual(expect.objectContaining({
description: mockObject.description,
image: mockObject.image,
name: mockObject.name,
type: mockObject.type,
}));

expect(result.uri).toMatch(/http:\/\/test-uri\/#.*/i);

});

Expand All @@ -195,6 +206,7 @@ describe('CollectionObjectSolidStore', () => {
client.getThing = jest.fn(() => 'test-thing');
client.getUrl = jest.fn(() => 'http://test-url/');
client.setThing = jest.fn(() => 'test-thing');
client.removeThing = jest.fn(() => 'test-thing');
client.saveSolidDatasetAt = jest.fn(async () => 'test-dataset');

const result = await service.save(mockObject);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,29 @@ export class CollectionObjectSolidStore implements CollectionObjectStore {
const distributionUri = getUrl(collectionThing, 'http://schema.org/distribution');
const distributionThing = getThing(catalogDataset, distributionUri);
const contentUrl = getUrl(distributionThing, 'http://schema.org/contentUrl');
const objectUri = object.uri || new URL(`#object-${v4()}`, contentUrl).toString();
let objectUri: string;

// check if the collection changed (aka contentUrl changed)
if (!object.uri || object.uri.includes(contentUrl)) {

// the collection's contentUrl matches the object's URI, or is not set (new object)
objectUri = object.uri || new URL(`#object-${v4()}`, contentUrl).toString();

} else {

// the collection's contentUrl changed
// -> move the object to that contentUrl
objectUri = new URL(`#object-${v4()}`, contentUrl).toString();

// -> delete object from old contentUrl
const oldDataset = await getSolidDataset(object.uri, { fetch });
const oldObjectThing = getThing(oldDataset, object.uri);
const oldDigitalObjectThing = getThing(oldDataset, CollectionObjectSolidStore.getDigitalObjectUri(object));
let updatedOldObjectsDataset = removeThing(oldDataset, oldObjectThing);
updatedOldObjectsDataset = removeThing(updatedOldObjectsDataset, oldDigitalObjectThing);
await saveSolidDatasetAt(object.uri, updatedOldObjectsDataset, { fetch });

}

// transform and save the object to the dataset of objects
const objectsDataset = await getSolidDataset(objectUri, { fetch });
Expand Down Expand Up @@ -159,7 +181,7 @@ export class CollectionObjectSolidStore implements CollectionObjectStore {
}

let objectThing = createThing({ url: object.uri });
const digitalObjectUri = object.mainEntityOfPage || CollectionObjectSolidStore.getDigitalObjectUri(object);
const digitalObjectUri = CollectionObjectSolidStore.getDigitalObjectUri(object);

// identification
objectThing = object.updated ? addStringNoLocale(objectThing, 'http://schema.org/dateModified', object.updated) : objectThing;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { html, property, PropertyValues, internalProperty, unsafeCSS, css, TemplateResult, CSSResult } from 'lit-element';
import { CollectionObject, Logger, Translator } from '@netwerk-digitaal-erfgoed/solid-crs-core';
import { Collection, CollectionObject, Logger, Translator } from '@netwerk-digitaal-erfgoed/solid-crs-core';
import { FormEvent } from '@netwerk-digitaal-erfgoed/solid-crs-components';
import { Interpreter, SpawnedActorRef, State } from 'xstate';
import { RxLitElement } from 'rx-lit';
Expand Down Expand Up @@ -50,6 +50,12 @@ export class ObjectIdentificationComponent extends RxLitElement {
@internalProperty()
state?: State<ObjectContext>;

/**
* A list of all collections
*/
@internalProperty()
collections?: Collection[];

/**
* Hook called on at every update after connection to the DOM.
*/
Expand Down Expand Up @@ -103,7 +109,9 @@ export class ObjectIdentificationComponent extends RxLitElement {
</nde-form-element>
<nde-form-element .actor="${this.formActor}" .translator="${this.translator}" field="collection">
<label slot="label" for="collection">${this.translator?.translate('nde.features.object.card.identification.field.collection')}</label>
<input type="text" slot="input" name="collection" value="${this.object.collection||''}" @click="${initializeFormMachine}"/>
<select slot="input" name="collection" @click="${initializeFormMachine}">
${this.collections.map((collection) => html`<option id="${collection.uri}" ?selected="${collection.uri === this.object.collection}">${collection.name}</option>`)}
</select>
</nde-form-element>
</div>
</nde-large-card>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ export class ObjectImageryComponent extends RxLitElement {
@internalProperty()
state?: State<ObjectContext>;

/**
* A list of licenses
*/
@internalProperty()
licenses?: { name: string; uri: string }[] = [
{
name: 'CC0 1.0',
uri: 'https://creativecommons.org/publicdomain/zero/1.0/deed.nl',
},
{
name: 'CC BY 4.0',
uri: 'https://creativecommons.org/licenses/by/4.0/deed.nl',
},
{
name: 'CC BY-SA 2.0',
uri: 'https://creativecommons.org/licenses/by-sa/2.0/be/deed.nl',
},
];

/**
* Hook called on at every update after connection to the DOM.
*/
Expand Down Expand Up @@ -88,7 +107,9 @@ export class ObjectImageryComponent extends RxLitElement {
</nde-form-element>
<nde-form-element .actor="${this.formActor}" .translator="${this.translator}" field="license">
<label slot="label" for="license">${this.translator?.translate('nde.features.object.card.image.field.license')}</label>
<input type="text" slot="input" name="license" value="${this.object.license||''}" @click="${initializeFormMachine}"/>
<select slot="input" name="license" @click="${initializeFormMachine}">
${this.licenses.map((license) => html`<option id="${license.uri}" ?selected="${license.uri === this.object.license}">${license.name}</option>`)}
</select>
</nde-form-element>
</div>
</nde-large-card>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ describe.each([

component.translator = new MemoryTranslator([], 'nl-NL');

component.collections = [ collection1 ];

});

afterEach(() => {
Expand Down Expand Up @@ -134,4 +136,3 @@ describe.each([
});

});

Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ describe('ObjectRootComponent', () => {

component.updated(map);

expect(component.subscribe).toHaveBeenCalledTimes(4);
expect(component.subscribe).toHaveBeenCalledTimes(5);

});

Expand Down
Loading

0 comments on commit 5b37a82

Please sign in to comment.