Skip to content
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

Support collection object transfer #267

Merged
merged 7 commits into from
Jun 2, 2021
Merged
Show file tree
Hide file tree
Changes from 6 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
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 @@ -90,6 +109,12 @@ export class ObjectImageryComponent extends RxLitElement {
<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}"/>
</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.identification.field.license')}</label>
<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>
` : html``;
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