Skip to content

Commit

Permalink
feat: Integrate form cards into object page (#249)
Browse files Browse the repository at this point in the history
* feat: create nde-large-card and tests

* fix: adapted to review

* feat: added basic forms to object root component WIP

* feat: add translations

* feat: integrated form machine WIP

* feat: removed collections from search page

* feat: added more translations and started input validation WIP

* feat: created seperate form components WIP

* test: updated tests WIP

* feat: added more input validation WIP

* Merge branch 'feat/integrate-form-cards-into-object-page' of github.com:netwerk-digitaal-erfgoed/solid-cbs into feat/integrate-form-cards-into-object-page

* test: added tests for new components 100%

* feat: added validation for dates and images WIP

* test: wrote multiple tests for coverage

* fix: fixed undefined input values WIP

* fix: renamed file

* test: fixed form machine tests WIP

* test: fixed final tests

Co-authored-by: Arthur Joppart <arthur@digita.ai>
  • Loading branch information
lem-onade and BelgianNoise committed Jun 1, 2021
1 parent 28ce5b9 commit cbfd9a3
Show file tree
Hide file tree
Showing 38 changed files with 1,846 additions and 207 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { FormSubmitter } from '../forms/form-submitter';
* @param event The event which triggered the validation.
* @returns Results of the validation.
*/
export const validator: FormValidator<Collection> = (context, event) => of([
export const validator: FormValidator<Collection> = async (context, event) => ([
...context.data && context.data.name ? [] : [ { field: 'name', message: 'demo-form.name.required' } ],
...context.data && context.data.uri ? [] : [ { field: 'uri', message: 'demo-form.uri.required' } ],
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('FormElementComponent', () => {

machine = interpret(
formMachine<Collection>(
(context: FormContext<Collection>, event: FormEvent): Observable<FormValidatorResult[]> => of([
async (context: FormContext<Collection>, event: FormEvent): Promise<FormValidatorResult[]> => ([
...context.data && context.data.name ? [] : [ { field: 'name', message: 'demo-form.name.required' } ],
...context.data && context.data.uri ? [] : [ { field: 'uri', message: 'demo-form.uri.required' } ],
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export class FormElementComponent<T> extends RxLitElement {

// Set the input field's default value.
const fieldData = data[this.field];
element.value = typeof fieldData === 'string' ? fieldData : '';
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));
Expand Down
3 changes: 1 addition & 2 deletions packages/solid-crs-components/lib/forms/form-validator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Observable } from 'rxjs';
import { FormValidatorResult } from './form-validator-result';
import { FormEvent } from './form.events';
import { FormContext } from './form.machine';

/**
* Validates the form and returns validator results.
*/
export type FormValidator<TData> = (context: FormContext<TData>, event: FormEvent) => Observable<FormValidatorResult[]>;
export type FormValidator<TData> = (context: FormContext<TData>, event: FormEvent) => Promise<FormValidatorResult[]>;
60 changes: 33 additions & 27 deletions packages/solid-crs-components/lib/forms/form.machine.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Collection } from '@netwerk-digitaal-erfgoed/solid-crs-core';
import { of } from 'rxjs';
import { interpret, Interpreter } from 'xstate';
import { FormEvent, FormEvents } from './form.events';
import { FormCleanlinessStates, FormContext, formMachine, FormRootStates, FormSubmissionStates, FormValidationStates } from './form.machine';
Expand All @@ -12,16 +11,15 @@ describe('FormMachine', () => {

machine = interpret<FormContext<Collection>>(
formMachine(
(context: FormContext<Collection>, event: FormEvent) => of([
async (context: FormContext<Collection>, event: FormEvent) => [
...context.data && context.data.name ? [] : [ { field: 'name', message: 'demo-form.name.required' } ],
...context.data && context.data.uri ? [] : [ { field: 'uri', message: 'demo-form.uri.required' } ],
]),
)
.withContext({
data: { uri: '', name: 'Test' },
original: { uri: '', name: 'Test' },
validation: [],
}),
],
).withContext({
data: { uri: '', name: 'Test' },
original: { uri: '', name: 'Test' },
validation: [],
}),
);

});
Expand All @@ -48,23 +46,31 @@ describe('FormMachine', () => {

}

// Validation rules should be set correctly
expect(machine.state.context.validation).toEqual(results);

// Data should be updated
expect(machine.state.context.data).toEqual(data);

// States should be updated
expect(machine.state.matches(
submission === FormSubmissionStates.SUBMITTED ?
FormSubmissionStates.SUBMITTED :
{
[FormSubmissionStates.NOT_SUBMITTED]:{
[FormRootStates.CLEANLINESS]: cleanliness,
[FormRootStates.VALIDATION]: validation,
},
},
)).toBeTruthy();
machine.onTransition((state) => {

if(state.matches(FormValidationStates.VALID || FormValidationStates.INVALID)){

// Validation rules should be set correctly
expect(machine.state.context.validation).toEqual(results);

// Data should be updated
expect(machine.state.context.data).toEqual(data);

// States should be updated
expect(machine.state.matches(
submission === FormSubmissionStates.SUBMITTED ?
FormSubmissionStates.SUBMITTED :
{
[FormSubmissionStates.NOT_SUBMITTED]:{
[FormRootStates.CLEANLINESS]: cleanliness,
[FormRootStates.VALIDATION]: validation,
},
},
)).toBeTruthy();

}

});

});

Expand Down Expand Up @@ -127,7 +133,7 @@ describe('FormMachine', () => {

machine = interpret<FormContext<Collection>>(
formMachine(
(context: FormContext<Collection>, event: FormEvent) => of([]),
async (context: FormContext<Collection>, event: FormEvent) => [],
submitter,
)
.withContext({
Expand Down
10 changes: 6 additions & 4 deletions packages/solid-crs-components/lib/forms/form.machine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createMachine } from 'xstate';
import { createMachine, send } from 'xstate';
import { map } from 'rxjs/operators';
import { State } from '../state/state';
import { FormValidatorResult } from './form-validator-result';
Expand Down Expand Up @@ -159,9 +159,11 @@ export const formMachine = <T>(
[FormValidationStates.VALIDATING]: {
entry: update,
invoke: {
src: (context, event) => validator(context, event).pipe(
map((results) => ({ type: FormEvents.FORM_VALIDATED, results })),
),
src: (context, event) => validator(context, event),
onDone: {
actions: send((context, event) =>
({ type: FormEvents.FORM_VALIDATED, results: event.data })),
},
},
on: {
[FormEvents.FORM_VALIDATED]: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { SidebarListComponent } from './sidebar-list.component';
import { SidebarListItemComponent } from './sidebar-list-item.component';

describe('SidebarListComponent', () => {

let component = new SidebarListComponent();

let firstListItem: SidebarListItemComponent;
let secondListItem: SidebarListItemComponent;
let thirdListItem: SidebarListItemComponent;

it('should be correctly instantiated', () => {

expect(component).toBeTruthy();
Expand All @@ -14,6 +19,17 @@ describe('SidebarListComponent', () => {

component = window.document.createElement('nde-sidebar-list') as SidebarListComponent;

firstListItem = window.document.createElement('nde-sidebar-list-item') as SidebarListItemComponent;
firstListItem.setAttribute('selected', '');
firstListItem.setAttribute('slot', 'item');
secondListItem = window.document.createElement('nde-sidebar-list-item') as SidebarListItemComponent;
firstListItem.setAttribute('slot', 'item');
thirdListItem = window.document.createElement('nde-sidebar-list-item') as SidebarListItemComponent;
firstListItem.setAttribute('slot', 'item');
component.appendChild(firstListItem);
component.appendChild(secondListItem);
component.appendChild(thirdListItem);

});

afterEach(() => {
Expand Down
6 changes: 3 additions & 3 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": 83.17,
"branches": 82.69,
"functions": 89.23,
"lines": 96.43,
"statements": 95.95
"lines": 96.92,
"statements": 96.38
}
},
"coveragePathIgnorePatterns": [
Expand Down
4 changes: 2 additions & 2 deletions packages/solid-crs-manage/lib/app-root.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export class AppRootComponent extends RxLitElement {
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,
type: 'http://schema.org/CreativeWork',
image: 'https://images.unsplash.com/photo-1615390164801-cf2e70f32b53?ixid=MnwxMjA3fDB8MHxwcm9maWxlLXBhZ2V8M3x8fGVufDB8fHx8&ixlib=rb-1.2.1&w=1000&q=80',
}
)).withContext({
alerts: [],
Expand Down
3 changes: 1 addition & 2 deletions packages/solid-crs-manage/lib/app.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Alert, FormActors, formMachine, FormValidatorResult, State } from '@net
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';
import { addAlert, addCollection, AppEvent, AppEvents, dismissAlert, removeSession, setCollections, setProfile, setSession } from './app.events';
import { SolidSession } from './common/solid/solid-session';
import { SolidService } from './common/solid/solid.service';
Expand Down Expand Up @@ -298,7 +297,7 @@ export const appMachine = (
{
id: FormActors.FORM_MACHINE,
src: formMachine<{ searchTerm: string }>(
(): Observable<FormValidatorResult[]> => of([]),
async (): Promise<FormValidatorResult[]> => [],
),
data: {
data: { searchTerm: '' },
Expand Down
13 changes: 12 additions & 1 deletion packages/solid-crs-manage/lib/app.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { AlertComponent, CardComponent, CollectionCardComponent, ContentHeaderComponent, FormElementComponent, ObjectCardComponent, SidebarComponent, SidebarItemComponent, SidebarListComponent, SidebarListItemComponent } from '@netwerk-digitaal-erfgoed/solid-crs-components';
import { AlertComponent, CardComponent, CollectionCardComponent, ContentHeaderComponent, FormElementComponent, LargeCardComponent, ObjectCardComponent, SidebarComponent, SidebarItemComponent, SidebarListComponent, SidebarListItemComponent } from '@netwerk-digitaal-erfgoed/solid-crs-components';
import { inspect } from '@xstate/inspect';
import { SearchRootComponent } from './features/search/search-root.component';
import { AppRootComponent } from './app-root.component';
import { AuthenticateRootComponent } from './features/authenticate/authenticate-root.component';
import { CollectionRootComponent } from './features/collection/collection-root.component';
import { ObjectRootComponent } from './features/object/object-root.component';
import { ObjectImageryComponent } from './features/object/components/object-imagery.component';
import './index';
import { ObjectDimensionsComponent } from './features/object/components/object-dimensions.component';
import { ObjectIdentificationComponent } from './features/object/components/object-identification.component';
import { ObjectRepresentationComponent } from './features/object/components/object-representation.component';
import { ObjectCreationComponent } from './features/object/components/object-creation.component';

/**
* Starts the xstate devtools
Expand All @@ -30,7 +35,13 @@ customElements.define('nde-form-element', FormElementComponent);
customElements.define('nde-content-header', ContentHeaderComponent);
customElements.define('nde-card', CardComponent);
customElements.define('nde-object-card', ObjectCardComponent);
customElements.define('nde-large-card', LargeCardComponent);
customElements.define('nde-collection-card', CollectionCardComponent);
customElements.define('nde-sidebar-list-item', SidebarListItemComponent);
customElements.define('nde-sidebar-list', SidebarListComponent);
customElements.define('nde-sidebar-item', SidebarItemComponent);
customElements.define('nde-object-imagery', ObjectImageryComponent);
customElements.define('nde-object-identification', ObjectIdentificationComponent);
customElements.define('nde-object-representation', ObjectRepresentationComponent);
customElements.define('nde-object-creation', ObjectCreationComponent);
customElements.define('nde-object-dimensions', ObjectDimensionsComponent);
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as client from '@netwerk-digitaal-erfgoed/solid-crs-client';
import { Collection } from '@netwerk-digitaal-erfgoed/solid-crs-core';
import { ArgumentError, Collection } from '@netwerk-digitaal-erfgoed/solid-crs-core';
import { CollectionSolidStore } from './collection-solid-store';

describe('CollectionSolidStore', () => {
Expand Down Expand Up @@ -307,4 +307,30 @@ describe('CollectionSolidStore', () => {

});

describe('search()', () => {

it.each([ null, undefined ])('should error when collections is %s', async (value) => {

await expect(service.search('searchterm', value)).rejects.toThrow(ArgumentError);

});

it.each([ null, undefined ])('should error when searchTerm is %s', async (value) => {

await expect(service.search(value, [ mockCollection ])).rejects.toThrow(ArgumentError);

});

it('should return filtered list', async () => {

const collections = [ mockCollection, mockCollection, { ...mockCollection, name: undefined } ];

const result = await service.search(mockCollection.name, collections);
expect(result).toBeTruthy();
expect(result.length).toEqual(2);

});

});

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

});

describe('getIssuer', () => {
describe('getIssuer()', () => {

it('should throw when webid is undefined', async () => {

Expand Down Expand Up @@ -41,7 +41,7 @@ describe('SolidMockService', () => {

});

describe('getSession', () => {
describe('getSession()', () => {

it('should throw when profiles is undefined', async () => {

Expand All @@ -52,7 +52,7 @@ describe('SolidMockService', () => {

});

describe('login', () => {
describe('login()', () => {

it('should throw when webId is undefined', async () => {

Expand All @@ -63,6 +63,7 @@ describe('SolidMockService', () => {
it('should throw when issuer is undefined', async () => {

(service as any).profiles = [ { webId: 'https://test.com', issuer: undefined } ];
service.getIssuer = jest.fn().mockReturnValueOnce(false);
await expect(service.login('https://test.com')).rejects.toThrow(ArgumentError);

});
Expand All @@ -76,4 +77,21 @@ describe('SolidMockService', () => {

});

describe('getProfile()', () => {

it('should throw when webId is undefined', async () => {

await expect(service.getProfile(undefined)).rejects.toThrow(ArgumentError);

});

it('should return a mock profile', async () => {

const uri = 'uri';
await expect(service.getProfile(uri)).resolves.toEqual(expect.objectContaining({ uri }));

});

});

});
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { formMachine, State, FormActors, FormValidatorResult } from '@netwerk-digitaal-erfgoed/solid-crs-components';
import { createMachine } from 'xstate';
import { send } from 'xstate/lib/actions';
import { catchError, map } from 'rxjs/operators';
import { from, Observable, of } from 'rxjs';
import { SolidSession } from '../../common/solid/solid-session';
import { SolidService } from '../../common/solid/solid.service';
import { AuthenticateEvent, AuthenticateEvents, handleSessionUpdate } from './authenticate.events';
Expand Down Expand Up @@ -80,11 +78,10 @@ export const authenticateMachine = (solid: SolidService) =>
/**
* Validates the form.
*/
(context): Observable<FormValidatorResult[]> =>
from(solid.getIssuer(context.data?.webId)).pipe(
map((result) => result ? [] : [ { field: 'webId', message: 'nde.features.authenticate.error.invalid-webid.invalid-url' } ]),
catchError((err: Error) => of([ { field: 'webId', message: err.message } ])),
),
async (context): Promise<FormValidatorResult[]> =>
solid.getIssuer(context.data?.webId)
.then((result) => result ? [] : [ { field: 'webId', message: 'nde.features.authenticate.error.invalid-webid.invalid-url' } ])
.catch((err: Error) => [ { field: 'webId', message: err.message } ]),
/**
* Redirects the user to the identity provider.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { formMachine,
FormEvents, State } from '@netwerk-digitaal-erfgoed/solid-crs-components';
import { assign, createMachine, sendParent } from 'xstate';
import { Collection, CollectionObject, CollectionObjectStore, CollectionStore } from '@netwerk-digitaal-erfgoed/solid-crs-core';
import { Observable, of } from 'rxjs';
import { AppEvents } from '../../app.events';
import { ObjectEvents } from '../object/object.events';
import { CollectionEvent, CollectionEvents } from './collection.events';
Expand Down Expand Up @@ -151,7 +150,7 @@ export const collectionMachine =
{
id: FormActors.FORM_MACHINE,
src: formMachine<{ name: string; description: string }>(
(): Observable<FormValidatorResult[]> => of([]),
async (): Promise<FormValidatorResult[]> => [],
async (c: FormContext<{ name: string; description: string }>) => c.data
),
data: (context) => ({
Expand Down
Loading

0 comments on commit cbfd9a3

Please sign in to comment.