From 28ce5b96f69599f46a05c95ebf88d41c35097077 Mon Sep 17 00:00:00 2001 From: Stijn Taelemans Date: Fri, 28 May 2021 10:45:01 +0200 Subject: [PATCH] feat: Update collection model and store (#227) * fix: ellipsis on overflowing list items * fix: fixed login page logo styling * fix: added overflow hidden to content header title and subtitle * fix: fixed scrolling * chore: fixed lint issues * feat: updated model WIP * feat: started work on transformer WIP * chore: fixed build errors WIP * test: fixed tests in manage WIP * feat: added basic transformers for collection objects WIP * feat: support saving of objects WIP * test: updated tests * feat: implemented delete for objects * fix: changed references, bnodes to literals * chore: removed unnecessary properties * chore: aligned model with form doc * test: fixed low coverage * test: upped coverage * chore: remove commented onError Co-authored-by: Wouter Janssens --- packages/solid-crs-client/lib/index.ts | 2 +- .../lib/collections/object-card.component.ts | 2 +- .../lib/demo/demo-card.component.ts | 3 +- .../lib/styles.module.css | 2 +- .../collection-object-memory-store.spec.ts | 12 +- .../lib/collections/collection-object.ts | 133 +++++++- .../solid-crs-core/lib/stores/resource.ts | 4 + packages/solid-crs-core/package.json | 4 +- .../lib/app-root.component.ts | 10 +- packages/solid-crs-manage/lib/app.events.ts | 9 +- packages/solid-crs-manage/lib/app.machine.ts | 10 +- .../collection-object-solid-store.spec.ts | 291 ++++++++++++++++-- .../solid/collection-object-solid-store.ts | 241 +++++++++++++-- .../authenticate-root.component.spec.ts | 2 +- .../features/collection/collection.events.ts | 9 +- .../collection/collection.machine.spec.ts | 37 ++- .../features/collection/collection.machine.ts | 246 ++++++++------- .../lib/features/object/object.events.ts | 7 +- packages/solid-crs-manage/lib/i8n/nl-NL.json | 10 + packages/solid-crs-manage/package.json | 10 +- .../leapeeters/heritage-objects/data-1$.ttl | 268 +++++----------- 21 files changed, 914 insertions(+), 398 deletions(-) diff --git a/packages/solid-crs-client/lib/index.ts b/packages/solid-crs-client/lib/index.ts index 77f3fdc5..bc4e3bd0 100644 --- a/packages/solid-crs-client/lib/index.ts +++ b/packages/solid-crs-client/lib/index.ts @@ -1,2 +1,2 @@ export { login, handleIncomingRedirect, logout, fetch, getDefaultSession } from '@inrupt/solid-client-authn-browser'; -export { getSolidDataset, getThing, getStringNoLocale, getStringNoLocaleAll, getUrl, getStringByLocaleAll, getStringWithLocale, getStringWithLocaleAll, getThingAll, getUrlAll, removeAll, removeThing, saveSolidDatasetAt, setThing, removeUrl, Thing, addStringWithLocale, addStringNoLocale, addUrl, addDatetime, getDatetime, createThing, asUrl, overwriteFile, deleteFile, ThingPersisted } from '@inrupt/solid-client'; +export { getSolidDataset, getThing, getStringNoLocale, getStringNoLocaleAll, getUrl, getStringByLocaleAll, getStringWithLocale, getStringWithLocaleAll, getThingAll, getUrlAll, removeAll, removeThing, saveSolidDatasetAt, setThing, removeUrl, Thing, addStringWithLocale, addStringNoLocale, addUrl, addDatetime, getDatetime, createThing, asUrl, overwriteFile, deleteFile, ThingPersisted, getInteger, addInteger } from '@inrupt/solid-client'; diff --git a/packages/solid-crs-components/lib/collections/object-card.component.ts b/packages/solid-crs-components/lib/collections/object-card.component.ts index ed6a16c4..bd5bba1e 100644 --- a/packages/solid-crs-components/lib/collections/object-card.component.ts +++ b/packages/solid-crs-components/lib/collections/object-card.component.ts @@ -46,7 +46,7 @@ export class ObjectCardComponent extends LitElement { */ render() { - const timeAgo = getFormattedTimeAgo(this.object.updated, this.translator); + const timeAgo = getFormattedTimeAgo(+this.object.updated, this.translator); return html` diff --git a/packages/solid-crs-components/lib/demo/demo-card.component.ts b/packages/solid-crs-components/lib/demo/demo-card.component.ts index 8eabbfd6..946622d0 100644 --- a/packages/solid-crs-components/lib/demo/demo-card.component.ts +++ b/packages/solid-crs-components/lib/demo/demo-card.component.ts @@ -78,7 +78,8 @@ export class DemoNDECardComponent extends LitElement { image: 'https://images.unsplash.com/photo-1615390164801-cf2e70f32b53?ixid=MnwxMjA3fDB8MHxwcm9maWxlLXBhZ2V8M3x8fGVufDB8fHx8&ixlib=rb-1.2.1&w=1000&q=80', subject: 'Wel Degelijk Geen Molen', type: 'type', - updated: 1620216600000, + updated: '1620216600000', + collection: undefined, } as CollectionObject; const obj2 = { ...obj1, name: undefined } as CollectionObject; diff --git a/packages/solid-crs-components/lib/styles.module.css b/packages/solid-crs-components/lib/styles.module.css index ce98806f..1412d4ce 100644 --- a/packages/solid-crs-components/lib/styles.module.css +++ b/packages/solid-crs-components/lib/styles.module.css @@ -1,7 +1,7 @@ @import '../node_modules/@netwerk-digitaal-erfgoed/solid-crs-theme/dist/style.css'; html { - height: 100% + height: 100%; } body { diff --git a/packages/solid-crs-core/lib/collections/collection-object-memory-store.spec.ts b/packages/solid-crs-core/lib/collections/collection-object-memory-store.spec.ts index fcfc41d1..054a55e3 100644 --- a/packages/solid-crs-core/lib/collections/collection-object-memory-store.spec.ts +++ b/packages/solid-crs-core/lib/collections/collection-object-memory-store.spec.ts @@ -20,7 +20,7 @@ describe('CollectionObjectMemoryStore', () => { image: null, subject: null, type: null, - updated: 0, + updated: undefined, collection: 'collection-uri-1', }, { @@ -30,7 +30,7 @@ describe('CollectionObjectMemoryStore', () => { image: null, subject: null, type: null, - updated: 0, + updated: undefined, collection: 'collection-uri-1', }, { @@ -40,7 +40,7 @@ describe('CollectionObjectMemoryStore', () => { image: null, subject: null, type: null, - updated: 0, + updated: undefined, collection: 'collection-uri-2', }, ]; @@ -51,6 +51,8 @@ describe('CollectionObjectMemoryStore', () => { service = new CollectionObjectMemoryStore(resources); + jest.clearAllMocks(); + }); it('should be correctly instantiated', () => { @@ -80,7 +82,7 @@ describe('CollectionObjectMemoryStore', () => { image: null, subject: null, type: null, - updated: 0, + updated: undefined, collection: 'collection-uri-1', }, { @@ -90,7 +92,7 @@ describe('CollectionObjectMemoryStore', () => { image: null, subject: null, type: null, - updated: 0, + updated: undefined, collection: 'collection-uri-1', } ]); diff --git a/packages/solid-crs-core/lib/collections/collection-object.ts b/packages/solid-crs-core/lib/collections/collection-object.ts index d1746a30..d24f01b7 100644 --- a/packages/solid-crs-core/lib/collections/collection-object.ts +++ b/packages/solid-crs-core/lib/collections/collection-object.ts @@ -4,9 +4,27 @@ import { Resource } from '../stores/resource'; * Represents a digitally archived objects. */ export interface CollectionObject extends Resource { + + // IDENTIFICATION + + /** + * The type of the object. + */ + type: string; + /** * The name of the object. */ + additionalType?: string; + + /** + * The identifier of the object. + */ + identifier?: string; + + /** + * The title of the object. + */ name: string; /** @@ -15,27 +33,124 @@ export interface CollectionObject extends Resource { description: string; /** - * An image of the object. + * The collection to which the object belongs. */ - image: string; + collection: string; + + /** + * The maintainer of the object. + */ + maintainer?: string; + + // CREATION + + /** + * The creator of the object. + */ + creator?: string; + + /** + * The creation date of the object. + */ + dateCreated?: string; + + /** + * The creation location of the object. + */ + locationCreated?: string; + + /** + * The material of the object. + */ + material?: string; + + // REPRESENTATION /** * The subject of the object. */ - subject: string; + subject?: string; /** - * The type of the object. + * The location of the object. */ - type: string; + location?: string; /** - * The timestamp representing when the object was updated. + * The person of the object. */ - updated: number; + person?: string; /** - * The collection to which the object belongs. + * The organization of the object. */ - collection: string; + organization?: string; + + /** + * The event of the object. + */ + event?: string; + + // ACQUISITION + + // TBD + + // DIMENSIONS + + /** + * The height of the object. + */ + height?: number; + + /** + * The height unit of the object. + */ + heightUnit?: string; + + /** + * The width of the object. + */ + width?: number; + + /** + * The width unit of the object. + */ + widthUnit?: string; + + /** + * The depth of the object. + */ + depth?: number; + + /** + * The depth unit of the object. + */ + depthUnit?: string; + + /** + * The weight of the object. + */ + weight?: number; + + /** + * The weight unit of the object. + */ + weightUnit?: string; + + // OTHER + + /** + * An image of the object. + */ + image: string; + + /** + * A link to the digital representation of this object. + */ + mainEntityOfPage?: string; + + /** + * The license of this object + */ + license?: string; } diff --git a/packages/solid-crs-core/lib/stores/resource.ts b/packages/solid-crs-core/lib/stores/resource.ts index 0532d893..95f3bb4b 100644 --- a/packages/solid-crs-core/lib/stores/resource.ts +++ b/packages/solid-crs-core/lib/stores/resource.ts @@ -6,4 +6,8 @@ export interface Resource { * The identifier of the resource. */ uri: string; + /** + * When the resource was last updated + */ + updated?: string; } diff --git a/packages/solid-crs-core/package.json b/packages/solid-crs-core/package.json index 005ecdd0..1203b0a0 100644 --- a/packages/solid-crs-core/package.json +++ b/packages/solid-crs-core/package.json @@ -63,7 +63,7 @@ "statements": 87.56, "lines": 87.5, "functions": 89.19 - } + } }, "coveragePathIgnorePatterns": [ "/dist/", @@ -74,4 +74,4 @@ "displayName": "core", "preset": "@digita-ai/jest-config" } -} \ No newline at end of file +} diff --git a/packages/solid-crs-manage/lib/app-root.component.ts b/packages/solid-crs-manage/lib/app-root.component.ts index 210bff7d..fd770757 100644 --- a/packages/solid-crs-manage/lib/app-root.component.ts +++ b/packages/solid-crs-manage/lib/app-root.component.ts @@ -57,12 +57,20 @@ export class AppRootComponent extends RxLitElement { new CollectionSolidStore(), new CollectionObjectSolidStore(), { - uri: null, + uri: undefined, name: this.translator.translate('nde.features.collections.new-collection-name'), description: this.translator.translate('nde.features.collections.new-collection-description'), objectsUri: undefined, distribution: undefined, }, + { + uri: undefined, + name: this.translator.translate('nde.features.object.new-object-name'), + description: this.translator.translate('nde.features.object.new-object-description'), + collection: undefined, + type: undefined, + image: undefined, + } )).withContext({ alerts: [], }), { devTools: true }, diff --git a/packages/solid-crs-manage/lib/app.events.ts b/packages/solid-crs-manage/lib/app.events.ts index 04260ca6..9c56f917 100644 --- a/packages/solid-crs-manage/lib/app.events.ts +++ b/packages/solid-crs-manage/lib/app.events.ts @@ -4,9 +4,9 @@ import { DoneInvokeEvent } from 'xstate'; import { assign, choose, send } from 'xstate/lib/actions'; import { AppContext } from './app.machine'; import { SolidSession } from './common/solid/solid-session'; -import { ClickedDeleteEvent, SavedCollectionEvent, SelectedCollectionEvent } from 'features/collection/collection.events'; +import { ClickedDeleteCollectionEvent, SavedCollectionEvent, SelectedCollectionEvent } from 'features/collection/collection.events'; import { SearchUpdatedEvent } from 'features/search/search.events'; -import { SelectedObjectEvent } from 'features/object/object.events'; +import { ClickedDeleteObjectEvent, SelectedObjectEvent } from 'features/object/object.events'; /** * Event references for the application root, with readable log format. @@ -91,12 +91,13 @@ export type AppEvent = | DismissAlertEvent | AddAlertEvent | SelectedCollectionEvent - | ClickedDeleteEvent + | ClickedDeleteCollectionEvent | ClickedCreateCollectionEvent | CollectionsLoadedEvent | SearchUpdatedEvent | SavedCollectionEvent - | SelectedObjectEvent; + | SelectedObjectEvent + | ClickedDeleteObjectEvent; /** * Actions for the alerts component. diff --git a/packages/solid-crs-manage/lib/app.machine.ts b/packages/solid-crs-manage/lib/app.machine.ts index 2a69d02f..cb103912 100644 --- a/packages/solid-crs-manage/lib/app.machine.ts +++ b/packages/solid-crs-manage/lib/app.machine.ts @@ -1,5 +1,5 @@ import { Alert, FormActors, formMachine, FormValidatorResult, State } from '@netwerk-digitaal-erfgoed/solid-crs-components'; -import { Collection, CollectionObjectStore, CollectionStore } from '@netwerk-digitaal-erfgoed/solid-crs-core'; +import { Collection, CollectionObjectStore, CollectionStore, CollectionObject } from '@netwerk-digitaal-erfgoed/solid-crs-core'; import { createMachine } from 'xstate'; import { assign, forwardTo, log, send } from 'xstate/lib/actions'; import { Observable, of } from 'rxjs'; @@ -106,7 +106,8 @@ export const appMachine = ( solid: SolidService, collectionStore: CollectionStore, objectStore: CollectionObjectStore, - template: Collection, + collectionTemplate: Collection, + objectTemplate: CollectionObject, ) => createMachine>({ id: AppActors.APP_MACHINE, @@ -167,7 +168,7 @@ export const appMachine = ( invoke: [ { id: AppActors.COLLECTION_MACHINE, - src: collectionMachine(collectionStore, objectStore), + src: collectionMachine(collectionStore, objectStore, objectTemplate), data: (context, event) => ({ collection: context.selected, }), @@ -355,6 +356,7 @@ export const appMachine = ( [AppEvents.CLICKED_CREATE_COLLECTION]: AppDataStates.CREATING, [AppEvents.LOGGED_IN]: AppDataStates.REFRESHING, [CollectionEvents.CLICKED_DELETE]: AppDataStates.REFRESHING, + [ObjectEvents.CLICKED_DELETE]: AppDataStates.REFRESHING, [CollectionEvents.SAVED_COLLECTION]: AppDataStates.REFRESHING, }, }, @@ -397,7 +399,7 @@ export const appMachine = ( /** * Save collection to the store. */ - src: () => collectionStore.save(template), // TODO: Update + src: () => collectionStore.save(collectionTemplate), // TODO: Update onDone: { target: AppDataStates.IDLE, actions: [ diff --git a/packages/solid-crs-manage/lib/common/solid/collection-object-solid-store.spec.ts b/packages/solid-crs-manage/lib/common/solid/collection-object-solid-store.spec.ts index 8d13bf96..0756f9b9 100644 --- a/packages/solid-crs-manage/lib/common/solid/collection-object-solid-store.spec.ts +++ b/packages/solid-crs-manage/lib/common/solid/collection-object-solid-store.spec.ts @@ -1,22 +1,38 @@ import * as client from '@netwerk-digitaal-erfgoed/solid-crs-client'; +import { getInteger, getStringNoLocale, getStringWithLocale, getUrl } from '@netwerk-digitaal-erfgoed/solid-crs-client'; +import { Collection, CollectionObject } from '@netwerk-digitaal-erfgoed/solid-crs-core'; import { CollectionObjectSolidStore } from './collection-object-solid-store'; describe('CollectionObjectSolidStore', () => { let service: CollectionObjectSolidStore; - const mockCollection = { - uri: 'test-uri', - name: 'test-name', - description: 'test-description', - objectsUri: 'test-url', - }; + let mockCollection: Collection; + let mockObject: CollectionObject; beforeEach(() => { service = new CollectionObjectSolidStore(); - jest.clearAllMocks(); + jest.resetAllMocks(); + + mockCollection = { + uri: 'http://test.uri/', + name: 'test-name', + description: 'test-description', + objectsUri: 'http://test.url', + distribution: undefined, + }; + + mockObject = { + uri: 'http://test.uri/', + collection: mockCollection.uri, + name: 'test-name', + description: 'test-description', + type: 'http://test.type', + image: 'http://test.image', + mainEntityOfPage: 'http://test.uri/', + }; }); @@ -45,7 +61,7 @@ describe('CollectionObjectSolidStore', () => { it('should return empty list when no object was found', async () => { client.getSolidDataset = jest.fn(async () => 'test-dataset'); - client.getThingAll = jest.fn(() => null); + client.getThingAll = jest.fn(() => []); await expect(service.getObjectsForCollection(mockCollection)).resolves.toEqual([]); @@ -53,19 +69,21 @@ describe('CollectionObjectSolidStore', () => { it('should return collection objects', async () => { + let objectThing = client.createThing({ url: mockObject.uri }); + objectThing = client.addUrl(objectThing, 'http://schema.org/isPartOf', mockCollection.uri); + + client.getUrl = jest.fn(() => mockCollection.uri); client.getSolidDataset = jest.fn(async () => 'test-dataset'); - client.getThingAll = jest.fn(() => [ 'test-thing' ]); - client.getStringWithLocale = jest.fn(() => 'test-string'); - client.asUrl = jest.fn(() => 'test-url'); + client.getThingAll = jest.fn(() => [ objectThing ]); + client.getThing = jest.fn(() => objectThing); - const result = await service.getObjectsForCollection(mockCollection) -; + const result = await service.getObjectsForCollection(mockCollection); + + expect(result.length).toBeTruthy(); expect(result[0]).toEqual(expect.objectContaining({ - uri: mockCollection.objectsUri, + uri: mockObject.uri, collection: mockCollection.uri, - name: 'test-string', - description: 'test-string', })); }); @@ -86,6 +104,9 @@ describe('CollectionObjectSolidStore', () => { client.getThing = jest.fn(() => 'test-thing'); client.getUrl = jest.fn(() => 'test-url'); client.getStringWithLocale = jest.fn(() => 'test-string'); + client.getStringNoLocale = jest.fn(() => 'test-string'); + client.getInteger = jest.fn(() => 1); + client.asUrl = jest.fn(() => 'test-url'); await expect(service.get('test-url')).resolves.toEqual( expect.objectContaining({ @@ -111,9 +132,24 @@ describe('CollectionObjectSolidStore', () => { describe('delete()', () => { - it('should throw', async () => { + it.each([ null, undefined ])('should error when object is %s', async (value) => { + + await expect(service.delete(value)).rejects.toThrow('Argument object should be set'); + + }); + + it('should return collection when deleted', () => { + + client.getSolidDataset = jest.fn(async () => 'test-dataset'); + client.getThing = jest.fn(() => 'test-thing'); + client.removeUrl = jest.fn(() => 'test-thing'); + client.setThing = jest.fn(() => 'test-dataset'); + client.removeThing = jest.fn(() => 'test-thing'); + client.getUrl = jest.fn(() => 'test-url'); + client.saveSolidDatasetAt = jest.fn(async () => 'test-dataset'); + client.deleteFile = jest.fn(async () => 'test-file'); - await expect(service.delete(undefined)).rejects.toThrow(); + expect(service.delete(mockObject)).resolves.toEqual(mockObject); }); @@ -121,9 +157,224 @@ describe('CollectionObjectSolidStore', () => { describe('save()', () => { - it('should throw', async () => { + it.each([ null, undefined ])('should error when object is %s', async (value) => { + + await expect(service.save(value)).rejects.toThrow('Argument object should be set'); + + }); + + it('should error when object does not contain collection uri', async () => { + + delete mockObject.collection; + + await expect(service.save(mockObject)).rejects.toThrow('The object must be linked to a collection'); + + }); + + it('should return object when saved', async () => { + + client.getSolidDataset = jest.fn(async () => 'test-dataset'); + client.getThing = jest.fn(() => 'test-thing'); + client.getUrl = jest.fn(() => 'test-url'); + client.setThing = 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); + + }); + + it('should return object with new uri when it was not set', async () => { + + delete mockObject.uri; + + client.getSolidDataset = jest.fn(async () => 'test-dataset'); + client.getThing = jest.fn(() => 'test-thing'); + client.getUrl = jest.fn(() => 'http://test-url/'); + client.setThing = jest.fn(() => 'test-thing'); + client.saveSolidDatasetAt = jest.fn(async () => 'test-dataset'); + + const result = await service.save(mockObject); + + expect(result).toEqual(expect.objectContaining({ ...mockObject })); + expect(result.uri).toBeTruthy(); + + }); + + }); + + describe('toThing()', () => { + + it('should error when object is null', () => { + + expect(() => CollectionObjectSolidStore.toThing(null)).toThrow('Argument object should be set'); + + }); + + it('should not add undefined properties to thing', () => { + + const mockObject2 = { uri: mockObject.uri } as CollectionObject; + + const { object: result, digitalObject } = CollectionObjectSolidStore.toThing(mockObject2); + + expect(getStringNoLocale(result, 'http://schema.org/dateModified')).toBeFalsy(); + expect(getUrl(result, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type')).toBeFalsy(); + expect(getUrl(result, 'http://schema.org/additionalType')).toBeFalsy(); + expect(getStringNoLocale(result, 'http://schema.org/identifier')).toBeFalsy(); + expect(getStringWithLocale(result, 'http://schema.org/name', 'nl')).toBeFalsy(); + expect(getStringWithLocale(result, 'http://schema.org/description', 'nl')).toBeFalsy(); + expect(getUrl(result, 'http://schema.org/isPartOf')).toBeFalsy(); + expect(getUrl(result, 'http://schema.org/maintainer')).toBeFalsy(); + expect(getStringNoLocale(result, 'http://schema.org/creator')).toBeFalsy(); + expect(getStringNoLocale(result, 'http://schema.org/locationCreated')).toBeFalsy(); + expect(getStringNoLocale(result, 'http://schema.org/material')).toBeFalsy(); + expect(getStringNoLocale(result, 'http://schema.org/dateCreated')).toBeFalsy(); + expect(getStringNoLocale(result, 'http://schema.org/DefinedTerm')).toBeFalsy(); + expect(getStringNoLocale(result, 'http://schema.org/Place')).toBeFalsy(); + expect(getStringNoLocale(result, 'http://schema.org/Person')).toBeFalsy(); + expect(getStringNoLocale(result, 'http://schema.org/Organization')).toBeFalsy(); + expect(getStringNoLocale(result, 'http://schema.org/Event')).toBeFalsy(); + expect(getInteger(result, 'http://schema.org/height')).toBeFalsy(); + expect(getInteger(result, 'http://schema.org/width')).toBeFalsy(); + expect(getInteger(result, 'http://schema.org/depth')).toBeFalsy(); + expect(getInteger(result, 'http://schema.org/weight')).toBeFalsy(); + expect(getUrl(result, 'http://schema.org/image')).toBeFalsy(); + expect(getUrl(result, 'http://schema.org/mainEntityOfPage')).toBeFalsy(); + expect(getUrl(result, 'http://schema.org/license')).toBeFalsy(); + + }); + + it('should convert object properties to thing', () => { + + const mockObject2 = { + ...mockObject, + updated: 'test', + type: 'http://test.url/', + additionalType: 'http://test.url/', + identifier: 'test', + name: 'test', + description: 'test', + collection: 'http://test.url/', + maintainer: 'http://test.url/', + creator: 'http://test.url/', + locationCreated: 'http://test.url/', + material: 'http://test.url/', + dateCreated: 'test', + subject: 'subject', + location: 'location', + person: 'person', + organization: 'organization', + event: 'event', + height: 2, + width: 2, + depth: 2, + weight: 2, + image: 'http://test.url/', + mainEntityOfPage: 'http://test.url', + license: 'http://test.url', + } as CollectionObject; + + const result = CollectionObjectSolidStore.toThing(mockObject2); + + expect(result).toBeTruthy(); + + }); + + }); + + describe('fromThing()', () => { + + it('should error when object is null', () => { + + expect(() => CollectionObjectSolidStore.fromThing(null, client.createThing({ url: mockObject.uri }))).toThrow(); + + }); + + it('should error when digitalObject is null', () => { + + expect(() => CollectionObjectSolidStore.fromThing(client.createThing({ url: mockObject.uri }), null)).toThrow(); + + }); + + it('should set properties to undefined when not in Thing', () => { + + client.getUrl = jest.fn(() => undefined); + client.getStringNoLocale = jest.fn(() => undefined); + client.getStringWithLocale = jest.fn(() => undefined); + client.asUrl = jest.fn(() => undefined); + + const objectThing = client.createThing({ url: mockObject.uri }); + + const result = CollectionObjectSolidStore.fromThing(objectThing, objectThing); + + expect(result.updated).toEqual(undefined); + expect(result.type).toEqual(undefined); + expect(result.additionalType).toEqual(undefined); + expect(result.identifier).toEqual(undefined); + expect(result.name).toEqual(undefined); + expect(result.description).toEqual(undefined); + expect(result.collection).toEqual(undefined); + expect(result.maintainer).toEqual(undefined); + expect(result.creator).toEqual(undefined); + expect(result.locationCreated).toEqual(undefined); + expect(result.material).toEqual(undefined); + expect(result.dateCreated).toEqual(undefined); + expect(result.image).toEqual(undefined); + expect(result.mainEntityOfPage).toEqual(undefined); + expect(result.subject).toEqual(undefined); + + }); + + }); + + describe('search()', () => { + + it.each([ null, undefined ])('should error when objects is %s', async (value) => { + + await expect(service.search('searchterm', value)).rejects.toThrow('Argument objects should be set'); + + }); + + it.each([ null, undefined ])('should error when searchTerm is %s', async (value) => { + + await expect(service.search(value, [ mockObject ])).rejects.toThrow('Argument searchTerm should be set'); + + }); + + it('should return filtered list', async () => { + + const objects = [ mockObject, mockObject, { ...mockObject, name: undefined } ]; + + const result = await service.search(mockObject.name, objects); + expect(result).toBeTruthy(); + expect(result.length).toEqual(2); + + }); + + }); + + describe('getDigitalObjectUri()', () => { + + it('should error when object is null', () => { + + expect(() => CollectionObjectSolidStore.getDigitalObjectUri(null)).toThrow(); + + }); + + it('should error when object uri is not set', () => { + + delete mockObject.uri; + + expect(() => CollectionObjectSolidStore.getDigitalObjectUri(mockObject)).toThrow(); + + }); + + it('should return correct uri', () => { - await expect(service.save(undefined)).rejects.toThrow(); + expect(CollectionObjectSolidStore.getDigitalObjectUri(mockObject)).toEqual(`${mockObject.uri}-digital`); }); diff --git a/packages/solid-crs-manage/lib/common/solid/collection-object-solid-store.ts b/packages/solid-crs-manage/lib/common/solid/collection-object-solid-store.ts index 6a99bf23..e21de6b3 100644 --- a/packages/solid-crs-manage/lib/common/solid/collection-object-solid-store.ts +++ b/packages/solid-crs-manage/lib/common/solid/collection-object-solid-store.ts @@ -1,6 +1,8 @@ -import { getUrl, getSolidDataset, getThing, getStringWithLocale, getThingAll, asUrl, ThingPersisted, fetch } from '@netwerk-digitaal-erfgoed/solid-crs-client'; +import { getUrl, getSolidDataset, getThing, getStringWithLocale, getThingAll, asUrl, ThingPersisted, fetch, createThing, addStringNoLocale, addUrl, addStringWithLocale, getStringNoLocale, saveSolidDatasetAt, setThing, removeThing, getInteger, addInteger, Thing } from '@netwerk-digitaal-erfgoed/solid-crs-client'; import { CollectionObject, CollectionObjectStore, Collection, ArgumentError, fulltextMatch } from '@netwerk-digitaal-erfgoed/solid-crs-core'; +import { v4 } from 'uuid'; + export class CollectionObjectSolidStore implements CollectionObjectStore { /** @@ -24,26 +26,20 @@ export class CollectionObjectSolidStore implements CollectionObjectStore { } - const objectThings = getThingAll(dataset); // a list of CollectionObject Things + const objectThings = getThingAll(dataset).filter((thing: Thing) => + getUrl(thing, 'http://schema.org/isPartOf') === collection.uri); // a list of CollectionObject Things - if (!objectThings) { + if (!objectThings || objectThings.length === 0) { return []; } - const objects = objectThings.map((objectThing: ThingPersisted) => ({ - uri: asUrl(objectThing), - collection: collection.uri, - name: getStringWithLocale(objectThing, 'http://schema.org/name', 'nl'), - description: getStringWithLocale(objectThing, 'http://schema.org/description', 'nl'), - type: undefined, - subject: undefined, - image: undefined, - updated: undefined, - } as CollectionObject)); - - return objects.filter((object: CollectionObject) => object.collection === collection.uri); + return objectThings.map((objectThing: ThingPersisted) => + CollectionObjectSolidStore.fromThing( + objectThing, + getThing(dataset, getUrl(objectThing, 'http://schema.org/mainEntityOfPage')) + )); } @@ -62,18 +58,11 @@ export class CollectionObjectSolidStore implements CollectionObjectStore { const dataset = await getSolidDataset(uri, { fetch }); - const collectionThing = getThing(dataset, uri); + const objectThing = getThing(dataset, uri); + const digitalObjectUri = getUrl(objectThing, 'http://schema.org/mainEntityOfPage'); + const digitalObjectThing = getThing(dataset, digitalObjectUri); - return { - uri, - collection: getUrl(collectionThing, 'http://schema.org/isPartOf'), - name: getStringWithLocale(collectionThing, 'http://schema.org/name', 'nl'), - description: getStringWithLocale(collectionThing, 'http://schema.org/description', 'nl'), - type: undefined, - subject: undefined, - image: undefined, - updated: undefined, - } as CollectionObject; + return CollectionObjectSolidStore.fromThing(objectThing, digitalObjectThing); } @@ -88,13 +77,27 @@ export class CollectionObjectSolidStore implements CollectionObjectStore { } /** - * Deletes a single Collection from a pod + * Deletes a single Collection from a pod\ * * @param resource The Collection to delete */ - async delete(resource: CollectionObject): Promise { + async delete(object: CollectionObject): Promise { - throw new Error('Method not implemented.'); + if (!object) { + + throw new ArgumentError('Argument object should be set', object); + + } + + // retrieve the objects dataset + const objectDataset = await getSolidDataset(object.uri, { fetch }); + // remove things from objects dataset + let updatedDataset = removeThing(objectDataset, object.uri); + updatedDataset = removeThing(updatedDataset, `${object.uri}-digital`); + // save the dataset + await saveSolidDatasetAt(object.uri, updatedDataset, { fetch }); + + return object; } @@ -103,9 +106,164 @@ export class CollectionObjectSolidStore implements CollectionObjectStore { * * @param resource The Collection to save */ - async save(resource: CollectionObject): Promise { + async save(object: CollectionObject): Promise { - throw new Error('Method not implemented.'); + if (!object) { + + throw new ArgumentError('Argument object should be set', object); + + } + + if (!object.collection) { + + throw new ArgumentError('The object must be linked to a collection', object); + + } + + // retrieve the catalog + const catalogDataset = await getSolidDataset(object.collection, { fetch }); + + // find out where to save this object based on its collection + const collectionThing = getThing(catalogDataset, object.collection); + 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(); + + // transform and save the object to the dataset of objects + const objectsDataset = await getSolidDataset(objectUri, { fetch }); + + const { object: objectThing, digitalObject: digitalObjectThing } + = CollectionObjectSolidStore.toThing({ ...object, uri: objectUri }); + + let updatedObjectsDataset = setThing(objectsDataset, objectThing); + updatedObjectsDataset = setThing(updatedObjectsDataset, digitalObjectThing); + await saveSolidDatasetAt(objectUri, updatedObjectsDataset, { fetch }); + + return { ...object, uri: objectUri }; + + } + + /** + * Converts a CollectionObject to Things + * + * @param object The CollectionObject to convert + * @returns The main object and digital object as Things + */ + static toThing(object: CollectionObject): { object: ThingPersisted; digitalObject: ThingPersisted } { + + if (!object) { + + throw new ArgumentError('Argument object should be set', object); + + } + + let objectThing = createThing({ url: object.uri }); + const digitalObjectUri = object.mainEntityOfPage || CollectionObjectSolidStore.getDigitalObjectUri(object); + + // identification + objectThing = object.updated ? addStringNoLocale(objectThing, 'http://schema.org/dateModified', object.updated) : objectThing; + objectThing = object.type ? addUrl(objectThing, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', object.type) : objectThing; + objectThing = object.additionalType ? addUrl(objectThing, 'http://schema.org/additionalType', object.additionalType) : objectThing; + objectThing = object.identifier ? addStringNoLocale(objectThing, 'http://schema.org/identifier', object.identifier) : objectThing; + objectThing = object.name ? addStringWithLocale(objectThing, 'http://schema.org/name', object.name, 'nl') : objectThing; + objectThing = object.description ? addStringWithLocale(objectThing, 'http://schema.org/description', object.description, 'nl') : objectThing; + objectThing = object.collection ? addUrl(objectThing, 'http://schema.org/isPartOf', object.collection) : objectThing; + objectThing = object.maintainer ? addUrl(objectThing, 'http://schema.org/maintainer', object.maintainer) : objectThing; + + // creation + objectThing = object.creator ? addStringNoLocale(objectThing, 'http://schema.org/creator', object.creator) : objectThing; + objectThing = object.locationCreated ? addStringNoLocale(objectThing, 'http://schema.org/locationCreated', object.locationCreated) : objectThing; + objectThing = object.material ? addStringNoLocale(objectThing, 'http://schema.org/material', object.material) : objectThing; + objectThing = object.dateCreated ? addStringNoLocale(objectThing, 'http://schema.org/dateCreated', object.dateCreated) : objectThing; + + // representation + objectThing = object.subject ? addStringNoLocale(objectThing, 'http://schema.org/DefinedTerm', object.subject) : objectThing; + objectThing = object.location ? addStringNoLocale(objectThing, 'http://schema.org/Place', object.location) : objectThing; + objectThing = object.person ? addStringNoLocale(objectThing, 'http://schema.org/Person', object.person) : objectThing; + objectThing = object.organization ? addStringNoLocale(objectThing, 'http://schema.org/Organization', object.organization) : objectThing; + objectThing = object.event ? addStringNoLocale(objectThing, 'http://schema.org/Event', object.event) : objectThing; + + // dimensions + objectThing = object.height ? addInteger(objectThing, 'http://schema.org/height', object.height) : objectThing; + objectThing = object.width ? addInteger(objectThing, 'http://schema.org/width', object.width) : objectThing; + objectThing = object.depth ? addInteger(objectThing, 'http://schema.org/depth', object.depth) : objectThing; + objectThing = object.weight ? addInteger(objectThing, 'http://schema.org/weight', object.weight) : objectThing; + + // other + objectThing = addUrl(objectThing, 'http://schema.org/mainEntityOfPage', digitalObjectUri); + + // digital object + let digitalObjectThing = createThing({ url: digitalObjectUri }); + + digitalObjectThing = addUrl(digitalObjectThing, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'http://schema.org/ImageObject'); + digitalObjectThing = object.image ? addUrl(digitalObjectThing, 'http://schema.org/contentUrl', object.image) : digitalObjectThing; + digitalObjectThing = object.license ? addUrl(digitalObjectThing, 'http://schema.org/license', object.license) : digitalObjectThing; + digitalObjectThing = addUrl(digitalObjectThing, 'http://schema.org/mainEntity', object.uri); + + return { object: objectThing, digitalObject: digitalObjectThing }; + + } + + /** + * Creates a CollectionObject from a ThingPersisted + * + * @param object The ThingPersisted to convert + * @returns a CollectionObject + */ + static fromThing(object: ThingPersisted, digitalObject: ThingPersisted): CollectionObject { + + if (!object) { + + throw new ArgumentError('Argument object should be set', object); + + } + + if (!digitalObject) { + + throw new ArgumentError('Argument digitalObject should be set', digitalObject); + + } + + return { + // identification + uri: asUrl(object), + updated: getStringNoLocale(object, 'http://schema.org/dateModified') || undefined, + type: getUrl(object, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type') || undefined, + additionalType: getUrl(object, 'http://schema.org/additionalType') || undefined, + identifier: getStringNoLocale(object, 'http://schema.org/identifier') || undefined, + name: getStringWithLocale(object, 'http://schema.org/name', 'nl') || undefined, + description: getStringWithLocale(object, 'http://schema.org/description', 'nl') || undefined, + collection: getUrl(object, 'http://schema.org/isPartOf') || undefined, + maintainer: getUrl(object, 'http://schema.org/maintainer') || undefined, + + // creation + creator: getStringNoLocale(object, 'http://schema.org/creator') || undefined, + locationCreated: getStringNoLocale(object, 'http://schema.org/locationCreated') || undefined, + material: getStringNoLocale(object, 'http://schema.org/material') || undefined, + dateCreated: getStringNoLocale(object, 'http://schema.org/dateCreated') || undefined, + + // representation + subject: getStringNoLocale(object, 'http://schema.org/DefinedTerm') || undefined, + location: getStringNoLocale(object, 'http://schema.org/Place') || undefined, + person: getStringNoLocale(object, 'http://schema.org/Person') || undefined, + organization: getStringNoLocale(object, 'http://schema.org/Organization') || undefined, + event: getStringNoLocale(object, 'http://schema.org/Event') || undefined, + + // dimensions + height: getInteger(object, 'http://schema.org/height') || undefined, + width: getInteger(object, 'http://schema.org/width') || undefined, + depth: getInteger(object, 'http://schema.org/depth') || undefined, + weight: getInteger(object, 'http://schema.org/weight') || undefined, + + // other + mainEntityOfPage: asUrl(digitalObject) || undefined, + + // digital object + image: getUrl(digitalObject, 'http://schema.org/contentUrl') || undefined, + license: getUrl(digitalObject, 'http://schema.org/license') || undefined, + + } as CollectionObject; } @@ -134,4 +292,25 @@ export class CollectionObjectSolidStore implements CollectionObjectStore { } + /** + * Retrieves the URI of the digital object for a given CollectionObject + */ + static getDigitalObjectUri(object: CollectionObject): string { + + if (!object) { + + throw new ArgumentError('Argument object should be set.', object); + + } + + if (!object.uri) { + + throw new ArgumentError('Argument object uri should be set.', object); + + } + + return `${object.uri}-digital`; + + } + } diff --git a/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.spec.ts b/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.spec.ts index e7046485..407d2fe3 100644 --- a/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.spec.ts +++ b/packages/solid-crs-manage/lib/features/authenticate/authenticate-root.component.spec.ts @@ -38,7 +38,7 @@ describe('AuthenticateRootComponent', () => { image: null, subject: null, type: null, - updated: 0, + updated: undefined, collection: 'collection-uri-1', }, ]); diff --git a/packages/solid-crs-manage/lib/features/collection/collection.events.ts b/packages/solid-crs-manage/lib/features/collection/collection.events.ts index 8867d75f..0a7bb5d5 100644 --- a/packages/solid-crs-manage/lib/features/collection/collection.events.ts +++ b/packages/solid-crs-manage/lib/features/collection/collection.events.ts @@ -2,7 +2,7 @@ import { Alert, Event, FormSubmittedEvent } from '@netwerk-digitaal-erfgoed/soli import { Collection } from '@netwerk-digitaal-erfgoed/solid-crs-core'; import { sendParent } from 'xstate'; import { AppEvents } from '../../app.events'; -import { SelectedObjectEvent } from 'features/object/object.events'; +import { ClickedDeleteObjectEvent, SelectedObjectEvent } from '../object/object.events'; /** * Event references for the collection machine, with readable log format. @@ -24,7 +24,7 @@ export enum CollectionEvents { /** * Fired when the user clicks the delete collection button. */ -export interface ClickedDeleteEvent extends Event { +export interface ClickedDeleteCollectionEvent extends Event { type: CollectionEvents.CLICKED_DELETE; collection: Collection; } @@ -77,14 +77,15 @@ export interface SavedCollectionEvent extends Event { */ export type CollectionEvent = SelectedCollectionEvent - | ClickedDeleteEvent + | ClickedDeleteCollectionEvent | ClickedEditEvent | ClickedSaveEvent | CancelledEditEvent | ClickedCreateObjectEvent | FormSubmittedEvent | SelectedObjectEvent - | SavedCollectionEvent; + | SavedCollectionEvent + | ClickedDeleteObjectEvent; /** * Adds an alert to the machine's parent. diff --git a/packages/solid-crs-manage/lib/features/collection/collection.machine.spec.ts b/packages/solid-crs-manage/lib/features/collection/collection.machine.spec.ts index 96cb3c0b..357d700b 100644 --- a/packages/solid-crs-manage/lib/features/collection/collection.machine.spec.ts +++ b/packages/solid-crs-manage/lib/features/collection/collection.machine.spec.ts @@ -12,12 +12,16 @@ describe('CollectionMachine', () => { uri: 'collection-uri-1', name: 'Collection 1', description: 'This is collection 1', + objectsUri: 'http://test.uri/', + distribution: 'http://test.uri/', }; const collection2 = { uri: 'collection-uri-2', name: 'Collection 2', description: 'This is collection 2', + objectsUri: 'http://test.uri/', + distribution: 'http://test.uri/', }; let machine: Interpreter; @@ -71,14 +75,19 @@ describe('CollectionMachine', () => { } + if(state.matches(CollectionStates.IDLE)) { + + machine.send(CollectionEvents.CLICKED_EDIT); + + } + }); machine.start(); - machine.send(CollectionEvents.CLICKED_EDIT); }); - it('should transition to deleting when clicked edit was emitted', async (done) => { + it('should transition to deleting when clicked delete was emitted', async (done) => { machine.onTransition((state) => { @@ -88,10 +97,15 @@ describe('CollectionMachine', () => { } + if(state.matches(CollectionStates.IDLE)) { + + machine.send(CollectionEvents.CLICKED_DELETE); + + } + }); machine.start(); - machine.send(CollectionEvents.CLICKED_DELETE); }); @@ -134,11 +148,19 @@ describe('CollectionMachine', () => { machine.onTransition((state) => { - if(state.matches(CollectionStates.IDLE) && state.context?.collection) { + if(state.matches(CollectionStates.IDLE)) { - expect(collectionStore.save).toHaveBeenCalledTimes(1); + machine.send(CollectionEvents.CLICKED_EDIT); - expect(collectionStore.save).toHaveBeenCalledWith(collection1); + } + + if(state.matches(CollectionStates.EDITING)) { + + machine.send(CollectionEvents.CLICKED_SAVE); + + } + + if(state.matches(CollectionStates.SAVING)) { done(); @@ -147,9 +169,6 @@ describe('CollectionMachine', () => { }); machine.start(); - machine.send(CollectionEvents.SELECTED_COLLECTION, { collection: collection1 }); - - machine.send(CollectionEvents.CLICKED_SAVE); }); diff --git a/packages/solid-crs-manage/lib/features/collection/collection.machine.ts b/packages/solid-crs-manage/lib/features/collection/collection.machine.ts index dadf1c66..41c60292 100644 --- a/packages/solid-crs-manage/lib/features/collection/collection.machine.ts +++ b/packages/solid-crs-manage/lib/features/collection/collection.machine.ts @@ -42,149 +42,173 @@ export enum CollectionStates { EDITING = '[CollectionsState: Editing]', DELETING = '[CollectionsState: Deleting]', DETERMINING_COLLECTION = '[CollectionsState: Determining collection]', + CREATING_OBJECT = '[CollectionsState: Creating object]', } /** * The collection machine. */ -export const collectionMachine = (collectionStore: CollectionStore, objectStore: CollectionObjectStore) => - createMachine>({ - id: CollectionActors.COLLECTION_MACHINE, - context: { }, - initial: CollectionStates.DETERMINING_COLLECTION, - on: { - [CollectionEvents.CLICKED_EDIT]: CollectionStates.EDITING, - [CollectionEvents.CLICKED_DELETE]: CollectionStates.DELETING, - [CollectionEvents.CLICKED_SAVE]: CollectionStates.SAVING, - [CollectionEvents.CANCELLED_EDIT]: CollectionStates.IDLE, - [CollectionEvents.SELECTED_COLLECTION]: { - actions: assign({ - collection: (context, event) => event.collection, - }), - target: CollectionStates.DETERMINING_COLLECTION, +export const collectionMachine = + (collectionStore: CollectionStore, objectStore: CollectionObjectStore, objectTemplate: CollectionObject) => + createMachine>({ + id: CollectionActors.COLLECTION_MACHINE, + context: { }, + initial: CollectionStates.DETERMINING_COLLECTION, + on: { + [CollectionEvents.SELECTED_COLLECTION]: { + actions: assign({ + collection: (context, event) => event.collection, + }), + target: CollectionStates.DETERMINING_COLLECTION, + }, }, - }, - states: { + states: { /** * Loads the objects associated with the current collection. */ - [CollectionStates.LOADING]: { - invoke: { - src: (context) => - objectStore.getObjectsForCollection(context.collection), - onDone: { + [CollectionStates.LOADING]: { + + invoke: { + src: (context) => + objectStore.getObjectsForCollection(context.collection), + onDone: { /** * When done, assign objects to the context and transition to idle. */ - actions: assign({ - objects: (context, event) => event.data, - }), - target: CollectionStates.IDLE, - }, - onError: { + actions: assign({ + objects: (context, event) => event.data, + }), + target: CollectionStates.IDLE, + }, + onError: { /** * Notify the parent machine when something goes wrong. */ - actions: sendParent((context, event) => ({ type: AppEvents.ERROR, data: event.data })), + actions: sendParent((context, event) => ({ type: AppEvents.ERROR, data: event.data })), + }, }, }, - }, - /** - * Determining collection - */ - [CollectionStates.DETERMINING_COLLECTION]: { - always: [ - { - target: CollectionStates.LOADING, - cond: (context, event) => context?.collection ? true : false, - }, - { - target: CollectionStates.IDLE, - }, - ], - }, - /** - * Objects for the current collection are loaded. - */ - [CollectionStates.IDLE]: { - on: { - [ObjectEvents.SELECTED_OBJECT]: { - actions: sendParent((context, event) => event), - }, + /** + * Determining collection + */ + [CollectionStates.DETERMINING_COLLECTION]: { + always: [ + { + target: CollectionStates.LOADING, + cond: (context, event) => context?.collection ? true : false, + }, + { + target: CollectionStates.IDLE, + }, + ], }, - }, - /** - * Saving changesto the collection's metadata. - */ - [CollectionStates.SAVING]: { - invoke: { - src: (context) => collectionStore.save(context.collection), - onDone: { - target: CollectionStates.DETERMINING_COLLECTION, - actions: [ - sendParent(() => ({ type: CollectionEvents.SAVED_COLLECTION })), - ], + /** + * Objects for the current collection are loaded. + */ + [CollectionStates.IDLE]: { + on: { + [ObjectEvents.SELECTED_OBJECT]: { + actions: sendParent((context, event) => event), + }, + [CollectionEvents.CLICKED_EDIT]: CollectionStates.EDITING, + [CollectionEvents.CLICKED_CREATE_OBJECT]: CollectionStates.CREATING_OBJECT, + [CollectionEvents.CLICKED_DELETE]: CollectionStates.DELETING, + [ObjectEvents.CLICKED_DELETE]: { + actions: sendParent((context, event) => event), + }, }, - onError: { - actions: sendParent(AppEvents.ERROR), + }, + /** + * Saving changesto the collection's metadata. + */ + [CollectionStates.SAVING]: { + invoke: { + src: (context) => collectionStore.save(context.collection), + onDone: { + target: CollectionStates.DETERMINING_COLLECTION, + actions: [ + sendParent(() => ({ type: CollectionEvents.SAVED_COLLECTION })), + ], + }, + onError: { + actions: sendParent(AppEvents.ERROR), + }, }, }, - }, - /** - * Editing the collection metadata. - */ - [CollectionStates.EDITING]: { - invoke: [ + /** + * Editing the collection metadata. + */ + [CollectionStates.EDITING]: { + on: { + [CollectionEvents.CLICKED_SAVE]: CollectionStates.SAVING, + [CollectionEvents.CANCELLED_EDIT]: CollectionStates.IDLE, + [FormEvents.FORM_SUBMITTED]: CollectionStates.SAVING, + }, + invoke: [ /** * Invoke a form machine which controls the form. */ - { - id: FormActors.FORM_MACHINE, - src: formMachine<{ name: string; description: string }>( - (): Observable => of([]), - async (c: FormContext<{ name: string; description: string }>) => c.data - ), - data: (context) => ({ - data: { name: context.collection.name, description: context.collection.description }, - original: { name: context.collection.name, description: context.collection.description }, - }), + { + id: FormActors.FORM_MACHINE, + src: formMachine<{ name: string; description: string }>( + (): Observable => of([]), + async (c: FormContext<{ name: string; description: string }>) => c.data + ), + data: (context) => ({ + data: { name: context.collection.name, description: context.collection.description }, + original: { name: context.collection.name, description: context.collection.description }, + }), + onDone: { + target: CollectionStates.SAVING, + actions: [ + assign((context, event) => ({ + collection: { + ...context.collection, + name: event.data.data.name, + description: event.data.data.description, + }, + })), + ], + }, + onError: { + target: CollectionStates.IDLE, + }, + }, + ], + }, + /** + * Deleting the current collection. + */ + [CollectionStates.DELETING]: { + invoke: { + src: (context) => collectionStore.delete(context.collection), onDone: { - target: CollectionStates.SAVING, + target: CollectionStates.IDLE, actions: [ - assign((context, event) => ({ - collection: { - ...context.collection, - name: event.data.data.name, - description: event.data.data.description, - }, - })), + sendParent((context) => ({ type: CollectionEvents.CLICKED_DELETE, collection: context.collection })), ], }, onError: { - target: CollectionStates.IDLE, + actions: sendParent(AppEvents.ERROR), }, }, - ], - on: { - [FormEvents.FORM_SUBMITTED]: CollectionStates.SAVING, }, - }, - /** - * Deleting the current collection. - */ - [CollectionStates.DELETING]: { - invoke: { - src: (context) => collectionStore.delete(context.collection), - onDone: { - target: CollectionStates.IDLE, - actions: [ - sendParent((context) => ({ type: CollectionEvents.CLICKED_DELETE, collection: context.collection })), - ], - }, - onError: { - actions: sendParent(AppEvents.ERROR), + /** + * Creating a new object. + */ + [CollectionStates.CREATING_OBJECT]: { + invoke: { + /** + * Save object to the store. + */ + src: (context) => objectStore.save({ ...objectTemplate, collection: context.collection.uri }), + onDone: { + target: CollectionStates.LOADING, + }, + onError: { + actions: sendParent(AppEvents.ERROR), + }, }, }, }, - }, - }); + }); diff --git a/packages/solid-crs-manage/lib/features/object/object.events.ts b/packages/solid-crs-manage/lib/features/object/object.events.ts index 93ee567c..2c95a051 100644 --- a/packages/solid-crs-manage/lib/features/object/object.events.ts +++ b/packages/solid-crs-manage/lib/features/object/object.events.ts @@ -15,7 +15,7 @@ export enum ObjectEvents { /** * Fired when the user clicks the delete object button. */ -export interface ClickedDeleteEvent extends Event { +export interface ClickedDeleteObjectEvent extends Event { type: ObjectEvents.CLICKED_DELETE; object: CollectionObject; } @@ -52,9 +52,10 @@ export interface CancelledEditEvent extends Event { /** * Events for the object machine. */ -export type ObjectEvent = ClickedDeleteEvent +export type ObjectEvent = ClickedDeleteObjectEvent | ClickedEditEvent | ClickedSaveEvent | CancelledEditEvent | SelectedObjectEvent -| FormSubmittedEvent; +| FormSubmittedEvent +| ClickedDeleteObjectEvent; diff --git a/packages/solid-crs-manage/lib/i8n/nl-NL.json b/packages/solid-crs-manage/lib/i8n/nl-NL.json index 4d6dd42c..8a667a3f 100644 --- a/packages/solid-crs-manage/lib/i8n/nl-NL.json +++ b/packages/solid-crs-manage/lib/i8n/nl-NL.json @@ -149,6 +149,16 @@ "locale": "nl-NL", "value": "Collecties" }, + { + "key": "nde.features.object.new-object-name", + "locale": "nl-NL", + "value": "Nieuw object" + }, + { + "key": "nde.features.object.new-object-description", + "locale": "nl-NL", + "value": "Beschrijving van dit object" + }, { "key": "nde.features.object.sidebar.title", "locale": "nl-NL", diff --git a/packages/solid-crs-manage/package.json b/packages/solid-crs-manage/package.json index 69cfe3b2..233127af 100644 --- a/packages/solid-crs-manage/package.json +++ b/packages/solid-crs-manage/package.json @@ -82,10 +82,10 @@ ], "coverageThreshold": { "global": { - "statements": 88.81, - "branches": 82.39, - "lines": 89.17, - "functions": 73.45 + "statements": 90.23, + "branches": 88.25, + "lines": 90.54, + "functions": 74.03 } }, "automock": false, @@ -105,4 +105,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/solid-crs-pods/data/leapeeters/heritage-objects/data-1$.ttl b/packages/solid-crs-pods/data/leapeeters/heritage-objects/data-1$.ttl index 35297d9f..371e0ba1 100644 --- a/packages/solid-crs-pods/data/leapeeters/heritage-objects/data-1$.ttl +++ b/packages/solid-crs-pods/data/leapeeters/heritage-objects/data-1$.ttl @@ -1,201 +1,99 @@ @prefix rdf: . @prefix schema: . -# Heritage object: description of a 'real' heritage object - <#object-1> - ######################################## - # Group: "Identificatie" - ######################################## - # Field: "Type" - # To discuss: which types should we (initially) support? For example: schema:Photograph, schema:VisualArtwork, schema:Drawing - rdf:type schema:Photograph ; + # IDENTIFICATION - # Field: "Objectnaam" + rdf:type schema:Photograph ; schema:additionalType ; - # To discuss: the type of the external resource can be anything. Should we make this explicit? E.g.: - # schema:additionalType [ - # rdf:type schema:DefinedTerm ; - # schema:name "bidprentjes" ; - # schema:sameAs - # ] ; - - # Field: "Objectnummer" schema:identifier "SK-A-1115" ; - - # Field: "Titel" schema:name "De Slag bij Waterloo"@nl ; - - # Field: "Korte beschrijving" schema:description "De slag bij Waterloo, 18 juni 1815"@nl ; + schema:isPartOf ; + schema:maintainer ; + + + # CREATION + + schema:creator "Jan Willem Pieneman" ; + schema:locationCreated "Delft" ; + schema:material "olieverf" ; + schema:dateCreated "1824-07-24" ; + + + # REPRESENTATION + + schema:DefinedTerm "veldslagen" ; + schema:Place "Waterloo" ; + schema:Person "Arthur Wellesley of Wellington" ; + schema:Event "Slag bij Waterloo" ; + + + # DIMENSIONS + + schema:height 56 ; + schema:width 82 ; + schema:depth 2 ; + schema:weight 120 ; + + + # OTHER + + schema:mainEntityOfPage <#object-1-digital> . + +<#object-1-digital> + rdf:type schema:ImageObject ; + schema:contentUrl ; + schema:license ; + schema:mainEntity <#object-1> . - # Field: "Collectie" - schema:isPartOf ; - # Field: "Bewaarinstelling". - # To discusss: should we use this property? It is proposed, not official. Or should the 'schema:maintainer' be part of the dataset description? + +<#object-2> + + # IDENTIFICATION + + rdf:type schema:Photograph ; + schema:additionalType ; + schema:identifier "SK-A-1115" ; + schema:name "De Slag bij Waterloo"@nl ; + schema:description "De slag bij Waterloo, 18 juni 1815"@nl ; + schema:isPartOf ; schema:maintainer ; - ######################################## - # Group: "Vervaardiging" - ######################################## - - # Field: "Vervaardiger" - schema:creator ; - # To discuss: the type of the external resource may not be a schema:Person or schema:Organization. Should we make this explicit? E.g.: - # schema:creator [ - # rdf:type schema:Person ; - # schema:name "Jan Willem Pieneman" ; - # schema:sameAs - # ] ; - - # Field: "Plaats" - schema:locationCreated ; - # To discuss: the type of the external resource may not be a schema:Place. Should we make this explicit? E.g.: - # schema:locationCreated [ - # rdf:type schema:Place ; - # schema:name "Delft" ; - # schema:sameAs - # ] ; - - # Field: "Materiaal" - schema:material ; - # To discuss: the type of the external resource can be anything. Should we make this explicit? E.g.: - # schema:material [ - # rdf:type schema:DefinedTerm ; - # schema:name "olieverf" ; - # schema:sameAs - # ] ; - - # Field: "Einddatum" - # To discuss: Schema.org doesn't have properties for the 'Begindatum' and 'Einddatum' of creation. How to handle this? + + # CREATION + + schema:creator "Jan Willem Pieneman" ; + schema:locationCreated "Delft" ; + schema:material "olieverf" ; schema:dateCreated "1824-07-24" ; - # To discuss: rather than using the properties above we can also use actions (see also 'Group: Verwerving'): - # schema:potentialAction [ - # rdf:type schema:CreateAction ; - # schema:agent ; # Field: "Vervaardiger" - # schema:location ; # Field: "Plaats" - # schema:startTime "1824-02-12" ; # Field: "Begindatum" - # schema:endTime "1824-07-24" # Field: "Einddatum" - # ] ; - - ######################################## - # Group: "Voorstelling" - ######################################## - - # Field: "Onderwerp" - schema:about [ - rdf:type schema:DefinedTerm ; - schema:name "veldslagen" ; - schema:sameAs - ] ; - - # Field: "Locatie" - # To discuss: or use 'schema:contentLocation'? - schema:about [ - rdf:type schema:Place ; - schema:name "Waterloo" ; - schema:sameAs - ] ; - - # Field: "Persoon of instelling" - schema:about [ - rdf:type schema:Person ; # Or: schema:Organization - schema:name "Arthur Wellesley of Wellington" ; - schema:sameAs - ] ; - - # Field: "Gebeurtenis" - schema:about [ - rdf:type schema:Event ; - schema:name "Slag bij Waterloo" ; - schema:sameAs - ] ; - - ######################################## - # Group: "Verwerving" - ######################################## - - # To discuss: can we register this information, but not publish it? - # It may contain sensitive data, e.g. prices and names of persons. How to handle this? - schema:potentialAction [ - # Field: "Type" - # To discuss: is there a specific class for denoting a 'Verwerving' in Schema.org? E.g. schema:TransferAction, schema:TradeAction? - rdf:type schema:Action ; - - # Field: "Methode" - schema:additionalType ; - # To discuss: the type of the external resource can be anything. Should we make this explicit? E.g.: - # schema:additionalType [ - # rdf:type schema:DefinedTerm ; - # schema:name "schenkingen" ; - # schema:sameAs - # ] ; - - # Field: "Datum" - schema:startTime "1986-06-17" ; - - # Field: "Datum" (same value as 'schema:startTime') - schema:endTime "1986-06-17" ; - - # Field: "Bron" - schema:agent [ - rdf:type schema:Person ; - schema:name "Gijsbert van Loosdrecht" - ] ; - - # Field: "Aankoopprijs" - schema:price 1000 ; - - # Field: "Valuta aankoopprijs" (ISO-421) - schema:priceCurrency "EUR" - ] ; - - ######################################## - # Group: "Afmetingen" - ######################################## - - # To discuss: Schema.org doesn't support all dimensions of the 'Basisregistratie', such as 'scale' and 'diameter'. How to handle these? - # Alternatively, we could use a generic 'schema:size' property, with a textual string (e.g. 'hoogte: 56,7cm; breedte: 82,3cm') - - # Field: "Dimensie": "hoogte" - schema:height [ - rdf:type: schema:QuantitativeValue ; - schema:unitCode: "CMT" ; # Field: "Eenheid" (UN/CEFACT Common Code) - schema:value: 567 # Field: "Waarde" - ] ; - - # Field: "Dimensie": "breedte" - schema:width [ - rdf:type: schema:QuantitativeValue ; - schema:unitCode: "CMT" ; # Field: "Eenheid" (UN/CEFACT Common Code) - schema:value: 823 # Field: "Waarde" - ] ; - - # Field: "Dimensie": "diepte" - schema:depth [ - rdf:type: schema:QuantitativeValue ; - schema:unitCode: "CMT" ; # Field: "Eenheid" (UN/CEFACT Common Code) - schema:value: 25 # Field: "Waarde" - ] ; - - # Field: "Dimensie": "gewicht" - schema:weight [ - rdf:type: schema:QuantitativeValue ; - schema:unitCode: "KGM" ; # Field: "Eenheid" (UN/CEFACT Common Code) - schema:value: 1200 # Field: "Waarde" - ] ; - - ######################################## - # Group: "Overig" - ######################################## - - # To discuss: Schema.org doesn't have properties for denoting the 'Toestand' and 'Huidige standplaats' of things. - # How to handle these? Perhaps omit these properties for the time being? - - # For linking the heritage object to the digital object - schema:mainEntityOfPage . - # To discuss: the type pointed to in 'schema:mainEntityOfPage' can be anything. Is it useful to have a 'convenience property' to the image? E.g.: - # schema:image . + + # REPRESENTATION + + schema:DefinedTerm "veldslagen" ; + schema:Place "Waterloo" ; + schema:Person "Arthur Wellesley of Wellington" ; + schema:Organization "Onbekend" ; + schema:Event "Slag bij Waterloo" ; + + + # DIMENSIONS + + schema:height 56 ; + schema:width 82 ; + schema:depth 2 ; + schema:weight 120 ; + + + # OTHER + + schema:mainEntityOfPage <#object-2-digital> . + +<#object-2-digital> + rdf:type schema:ImageObject ; + schema:contentUrl ; + schema:license ; + schema:mainEntity <#object-2> .