Skip to content

Commit

Permalink
feat: support local terms (#452)
Browse files Browse the repository at this point in the history
* feat: first implementation of basic terms WIP

* test: updated tests

* chore: added translations
  • Loading branch information
lem-onade authored Aug 30, 2021
1 parent 51138e0 commit 98a896c
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { CheckboxChecked, CheckboxUnchecked, Dropdown, Empty, Search, Theme } from '@netwerk-digitaal-erfgoed/solid-crs-theme';
import { CheckboxChecked, CheckboxUnchecked, Dropdown, Empty, Plus, Search, Theme } from '@netwerk-digitaal-erfgoed/solid-crs-theme';
import { html, unsafeCSS, css, TemplateResult, CSSResult, property, query, internalProperty, PropertyValues, queryAll } from 'lit-element';
import { RxLitElement } from 'rx-lit';
import { Interpreter } from 'xstate';
import { interpret, Interpreter } from 'xstate';
import { unsafeSVG } from 'lit-html/directives/unsafe-svg';
import { Alert, FormActors, FormContext, FormEvents } from '@netwerk-digitaal-erfgoed/solid-crs-components';
import { Alert, FormActors, FormContext, FormEvents, formMachine, FormSubmittedEvent } from '@netwerk-digitaal-erfgoed/solid-crs-components';
import { ArgumentError, Logger, Term, TermSource, Translator } from '@netwerk-digitaal-erfgoed/solid-crs-core';
import { map } from 'rxjs/operators';
import { from } from 'rxjs';
import { v4 } from 'uuid';
import { AppEvents } from '../../../app.events';
import { ClickedCancelTermEvent } from '../object.events';
import { ClickedSubmitEvent, ClickedTermEvent, QueryUpdatedEvent } from './term.events';
import { ClickedAddEvent, ClickedSubmitEvent, ClickedTermEvent, QueryUpdatedEvent } from './term.events';
import { TermContext, TermStates } from './term.machine';

/**
Expand Down Expand Up @@ -47,6 +48,18 @@ export class TermSearchComponent extends RxLitElement {
@internalProperty()
public formActor: Interpreter<FormContext<{ query: string; sources: TermSource[] }>>;

/**
* The form machine used by the form actor
*/
@internalProperty()
formMachineLocalTerm = formMachine<Term>();

/**
* The actor responsible for form validation in this component.
*/
@internalProperty()
formActorLocalTerm = interpret(this.formMachineLocalTerm, { devTools: true });

/**
* The field that for which Terms are being edited
*/
Expand All @@ -71,12 +84,6 @@ export class TermSearchComponent extends RxLitElement {
@internalProperty()
selectedTerms: Term[];

/**
* A the user's query input
*/
@internalProperty()
query: string;

/**
* The user's selected sources
*/
Expand All @@ -94,6 +101,9 @@ export class TermSearchComponent extends RxLitElement {
@query('nde-form-element.term input')
public searchInput: HTMLInputElement;

@query('nde-form-element input')
public localTermInput: HTMLInputElement;

/**
* Hook called on every update after connection to the DOM.
*/
Expand Down Expand Up @@ -137,14 +147,14 @@ export class TermSearchComponent extends RxLitElement {

groupSearchResults(searchResults: Term[]): { [key: string]: Term[] } {

return searchResults?.reduce<{ [key: string]: Term[] }>((searchResultsMap, term) => {
return searchResults.length > 0 ? searchResults?.reduce<{ [key: string]: Term[] }>((searchResultsMap, term) => {

const key = 'source';
(searchResultsMap[term[key]] = searchResultsMap[term[key]] || []).push(term);

return searchResultsMap;

}, {});
}, {}) : {};

}

Expand Down Expand Up @@ -181,29 +191,50 @@ export class TermSearchComponent extends RxLitElement {
// Create an alert components for each alert.
const alerts = this.alerts?.map((alert) => html`<nde-alert .logger='${this.logger}' .translator='${this.translator}' .alert='${alert}' @dismiss="${this.handleDismiss}"></nde-alert>`);

if (this.formActor) {
this.formActor?.onEvent((event) => {

this.formActor.onEvent((event) => {
if (event.type === FormEvents.FORM_VALIDATED) {

if (event.type === FormEvents.FORM_VALIDATED) {
const selectedSources = Array.from(this.sourceCheckboxes)
.filter((checkbox: HTMLInputElement) => checkbox.checked)
.map((checkbox: HTMLInputElement) => checkbox.id);

const selectedSources = Array.from(this.sourceCheckboxes)
.filter((checkbox: HTMLInputElement) => checkbox.checked)
.map((checkbox: HTMLInputElement) => checkbox.id);
this.actor.send(new QueryUpdatedEvent(this.searchInput?.value, selectedSources));

this.actor.send(new QueryUpdatedEvent(this.searchInput?.value, selectedSources));
}

}
});

});
this.actor?.onEvent((event) => {

}
if (event instanceof ClickedAddEvent){

const uri = `#${v4()}`;

this.formMachineLocalTerm = formMachine<Term>().withContext({
data: {
name: '',
uri,
},
original: {
name: '',
uri,
},
});

this.formActorLocalTerm = interpret(this.formMachineLocalTerm, { devTools: true });
this.formActorLocalTerm.onDone((context) => this.actor.send(new ClickedTermEvent(context.data.data)));
this.formActorLocalTerm.start();

}

});

const loading = !this.actor?.state.matches(TermStates.IDLE);

return html`
${ (loading || !this.sources) && !alerts ? html`<nde-progress-bar></nde-progress-bar>` : html`` }
${ (loading || !this.sources) && !alerts ? html`<nde-progress-bar></nde-progress-bar>` : '' }
${ alerts }
Expand Down Expand Up @@ -245,6 +276,28 @@ export class TermSearchComponent extends RxLitElement {
<button type="button" class="cancel gray" @click="${() => this.actor.parent.send(new ClickedCancelTermEvent())}">${this.translator.translate('term.search.cancel')}</button>
</div>
</form>
<a id="create-term" @click="${() => this.actor.send(new ClickedAddEvent())}">${this.translator.translate('term.add-local-term')}</a>
<!-- show local term input -->
${ this.actor?.state?.matches(TermStates.CREATING) ? html `
<nde-large-card
id="create-term-card"
class="term-card"
.showImage="${false}"
.showContent="${false}"
>
<nde-form-element slot="title" class="title inverse" .showLabel="${false}" hideValidation debounceTimeout="0" .actor="${this.formActorLocalTerm}" .translator="${this.translator}" field="name">
<input type="text" slot="input" class="name" placeholder="${this.translator.translate('term.title-placeholder')}"/>
</nde-form-element>
<div slot="subtitle">${this.translator.translate('term.description-placeholder')}</div>
<div slot="icon" @click=${() => this.formActorLocalTerm.send(new FormSubmittedEvent())}>
${unsafeSVG(Plus)}
</div>
</nde-large-card>
`: ''}
<!-- show selected terms -->
${this.selectedTerms?.length > 0 ? html`
Expand All @@ -264,14 +317,14 @@ export class TermSearchComponent extends RxLitElement {
${unsafeSVG(CheckboxChecked)}
</div>
<div slot="content">
${ term.description?.length > 0 ? html`<p>${this.translator.translate('term.field.description')}: ${ term.description }</p>` : html``}
${ term.alternateName?.length > 0 ? html`<p>${this.translator.translate('term.field.alternateName')}: ${ term.alternateName.join(', ') }</p>` : html``}
${ term.broader?.length > 0 ? html`<p>${this.translator.translate('term.field.broader')}: ${ term.broader.map((broader) => broader.name).join(', ') }</p>` : html``}
${ term.narrower?.length > 0 ? html`<p>${this.translator.translate('term.field.narrower')}: ${ term.narrower.map((narrower) => narrower.name).join(', ') }</p>` : html``}
${ term.hiddenName?.length > 0 ? html`<p>${this.translator.translate('term.field.hiddenName')}: ${ term.hiddenName }</p>` : html``}
${ term.description?.length > 0 ? html`<p>${this.translator.translate('term.field.description')}: ${ term.description }</p>` : ''}
${ term.alternateName?.length > 0 ? html`<p>${this.translator.translate('term.field.alternateName')}: ${ term.alternateName.join(', ') }</p>` : ''}
${ term.broader?.length > 0 ? html`<p>${this.translator.translate('term.field.broader')}: ${ term.broader.map((broader) => broader.name).join(', ') }</p>` : ''}
${ term.narrower?.length > 0 ? html`<p>${this.translator.translate('term.field.narrower')}: ${ term.narrower.map((narrower) => narrower.name).join(', ') }</p>` : ''}
${ term.hiddenName?.length > 0 ? html`<p>${this.translator.translate('term.field.hiddenName')}: ${ term.hiddenName }</p>` : ''}
</div>
</nde-large-card>`)}
</div>` : html``}
</div>` : ''}
<!-- show search results -->
${this.searchResultsMap && Object.keys(this.searchResultsMap).length > 0 ? html`
Expand All @@ -292,25 +345,25 @@ export class TermSearchComponent extends RxLitElement {
${ this.selectedTerms?.find((selected) => selected.uri === term.uri) ? unsafeSVG(CheckboxChecked) : unsafeSVG(CheckboxUnchecked)}
</div>
<div slot="content">
${ term.description?.length > 0 ? html`<p>${this.translator.translate('term.field.description')}: ${ term.description }</p>` : html``}
${ term.alternateName?.length > 0 ? html`<p>${this.translator.translate('term.field.alternateName')}: ${ term.alternateName.join(', ') }</p>` : html``}
${ term.broader?.length > 0 ? html`<p>${this.translator.translate('term.field.broader')}: ${ term.broader.map((broader) => broader.name).join(', ') }</p>` : html``}
${ term.narrower?.length > 0 ? html`<p>${this.translator.translate('term.field.narrower')}: ${ term.narrower.map((narrower) => narrower.name).join(', ') }</p>` : html``}
${ term.hiddenName?.length > 0 ? html`<p>${this.translator.translate('term.field.hiddenName')}: ${ term.hiddenName }</p>` : html``}
${ term.description?.length > 0 ? html`<p>${this.translator.translate('term.field.description')}: ${ term.description }</p>` : ''}
${ term.alternateName?.length > 0 ? html`<p>${this.translator.translate('term.field.alternateName')}: ${ term.alternateName.join(', ') }</p>` : ''}
${ term.broader?.length > 0 ? html`<p>${this.translator.translate('term.field.broader')}: ${ term.broader.map((broader) => broader.name).join(', ') }</p>` : ''}
${ term.narrower?.length > 0 ? html`<p>${this.translator.translate('term.field.narrower')}: ${ term.narrower.map((narrower) => narrower.name).join(', ') }</p>` : ''}
${ term.hiddenName?.length > 0 ? html`<p>${this.translator.translate('term.field.hiddenName')}: ${ term.hiddenName }</p>` : ''}
</div>
</nde-large-card>
`)}
`)}
</div>
` : html``}
` : ''}
${(this.searchResultsMap && Object.keys(this.searchResultsMap).length < 1)
? html`
<div class='empty'>
${unsafeSVG(Empty)}
<p>${this.translator?.translate('term.no-search-results')}</p>
</div>
` : html``}
` : ''}
`;

Expand Down Expand Up @@ -339,6 +392,11 @@ export class TermSearchComponent extends RxLitElement {
:host > * {
margin-bottom: var(--gap-large);
}
a {
cursor: pointer;
text-decoration: underline;
color: var(--colors-primary-light);
}
nde-progress-bar {
position: absolute;
width: 100%;
Expand Down Expand Up @@ -392,6 +450,14 @@ export class TermSearchComponent extends RxLitElement {
nde-form-element {
margin: 0;
}
nde-large-card nde-form-element input {
height: var(--gap-normal);
padding: 0;
line-height: var(--gap-normal);
overflow: hidden;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-normal);
}
.empty {
width: 100%;
height: 100%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Alert, FormActors, FormUpdatedEvent, FormValidatedEvent, LargeCardCompo
import { ArgumentError, Term } from '@netwerk-digitaal-erfgoed/solid-crs-core';
import { interpret, Interpreter } from 'xstate';
import { AppEvents } from '../../../app.events';
import { ClickedCancelTermEvent, ObjectEvents } from '../object.events';
import { ClickedCancelTermEvent } from '../object.events';
import { TermSearchComponent } from './term-search.component';
import { ClickedSubmitEvent, ClickedTermEvent, QueryUpdatedEvent } from './term.events';
import { TermContext, termMachine, TermStates } from './term.machine';
Expand Down Expand Up @@ -41,6 +41,7 @@ describe('TermSearchComponent', () => {
.withContext({
termService: termService as any,
selectedTerms: [],
searchResults: [],
}));

component = window.document.createElement('nde-term-search') as TermSearchComponent;
Expand Down Expand Up @@ -333,6 +334,30 @@ describe('TermSearchComponent', () => {

});

it('should show new term input field when create button is clicked', async () => {

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

if (machine.state.matches(TermStates.IDLE)) {

const button = window.document.body.getElementsByTagName('nde-term-search')[0].shadowRoot.querySelector<HTMLAnchorElement>('#create-term');
expect(button).toBeTruthy();
button.click();

}

if (machine.state.matches(TermStates.CREATING)) {

await component.updateComplete;
const card = window.document.body.getElementsByTagName('nde-term-search')[0].shadowRoot.querySelector<HTMLAnchorElement>('#create-term-card input');
expect(card).toBeTruthy();

}

});

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

it('should group search results', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { EventObject } from 'xstate';
export enum TermEvents {
CLICKED_SUBMIT = '[TermEvent: Clicked Submit]',
CLICKED_TERM = '[TermEvent: Clicked Term]',
CLICKED_ADD = '[TermEvent: Clicked Add]',
QUERY_UPDATED = '[TermEvent: Query Updated]',
}

Expand All @@ -29,6 +30,15 @@ export class ClickedTermEvent implements EventObject {

}

/**
* Fired when the user clicks a Term search result.
*/
export class ClickedAddEvent implements EventObject {

public type: TermEvents.CLICKED_ADD = TermEvents.CLICKED_ADD;

}

/**
* Fired when the user changes the input of the Term search inputs.
*/
Expand All @@ -44,4 +54,5 @@ export class QueryUpdatedEvent implements EventObject {
*/
export type TermEvent = ClickedSubmitEvent
| ClickedTermEvent
| QueryUpdatedEvent;
| QueryUpdatedEvent
| ClickedAddEvent;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { interpret, Interpreter } from 'xstate';
import { ClickedSubmitEvent, ClickedTermEvent, QueryUpdatedEvent } from './term.events';
import { ClickedAddEvent, ClickedSubmitEvent, ClickedTermEvent, QueryUpdatedEvent } from './term.events';
import { TermContext, termMachine, TermStates } from './term.machine';

describe('TermMachine', () => {
Expand Down Expand Up @@ -139,4 +139,31 @@ describe('TermMachine', () => {

});

it('should transition back to IDLE when ClickedTermEvent is fired when CREATING', async (done) => {

let clickedOnce = false;

machine.onTransition((state) => {

if (clickedOnce && state.matches(TermStates.IDLE)) {

done();

} else if (state.matches(TermStates.IDLE)) {

machine.send(new ClickedAddEvent());
clickedOnce = true;

} else if (state.matches(TermStates.CREATING)) {

machine.send(new ClickedTermEvent(term));

}

});

machine.start();

});

});
Loading

0 comments on commit 98a896c

Please sign in to comment.