Skip to content

Commit

Permalink
Enable link validation: (metaphacts#159)
Browse files Browse the repository at this point in the history
- change ValidationApi to be able to validate outbound links;
 - add ExampleValidationApi (see RDF example);
 - display link validation errors;
 - move element validation errors to authoring status line from template;
 - fix HashMap duplicate insertion.

Co-authored-by: Alexey Morozov <[email protected]>
  • Loading branch information
Daniel Razdiakonov and AlexeyMz committed Oct 25, 2018
1 parent 5d4a975 commit f80f23b
Show file tree
Hide file tree
Showing 15 changed files with 489 additions and 281 deletions.
11 changes: 10 additions & 1 deletion images/font-awesome/exclamation-triangle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/examples/rdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as ReactDOM from 'react-dom';

import { Workspace, WorkspaceProps, RDFDataProvider, GroupTemplate } from '../index';

import { ExampleMetadataApi } from './resources/exampleMetadataApi';
import { ExampleMetadataApi, ExampleValidationApi } from './resources/exampleMetadataApi';
import { onPageLoad, tryLoadLayoutFromLocalStorage, saveLayoutToLocalStorage } from './common';

const N3Parser: any = require('rdf-parser-n3');
Expand Down Expand Up @@ -53,6 +53,7 @@ const props: WorkspaceProps & ClassAttributes<Workspace> = {
console.log('Authoring state:', state);
},
metadataApi: new ExampleMetadataApi(),
validationApi: new ExampleValidationApi(),
viewOptions: {
onIriClick: iri => window.open(iri),
groupBy: [
Expand Down
34 changes: 31 additions & 3 deletions src/examples/resources/exampleMetadataApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
ElementModel, ElementTypeIri, LinkTypeIri, PropertyTypeIri, MetadataApi, DiagramModel, CancellationToken,
LinkModel,
ElementModel, LinkModel, ElementTypeIri, LinkTypeIri, PropertyTypeIri, MetadataApi, CancellationToken,
AuthoringKind, LinkChange, ValidationApi, ValidationEvent, ElementError, LinkError, isLinkConnectedToElement,
} from '../../index';

const owlPrefix = 'http://www.w3.org/2002/07/owl#';
Expand All @@ -15,7 +15,7 @@ const schema = {
subPropertyOf: rdfsPrefix + 'subPropertyOf' as LinkTypeIri,
};

const METADATA_DELAY: number = 0; /* ms */
const METADATA_DELAY: number = 500; /* ms */
function delay(): Promise<void> {
if (METADATA_DELAY === 0) {
return Promise.resolve();
Expand Down Expand Up @@ -83,3 +83,31 @@ export class ExampleMetadataApi implements MetadataApi {
return true;
}
}

export class ExampleValidationApi implements ValidationApi {
async validate(event: ValidationEvent): Promise<Array<ElementError | LinkError>> {
const errors: Array<ElementError | LinkError> = [];
if (event.target.types.indexOf(schema.class) >= 0) {
event.state.events
.filter((e): e is LinkChange =>
e.type === AuthoringKind.ChangeLink &&
!e.before &&
isLinkConnectedToElement(e.after, event.target.id)
).forEach(newLinkEvent => {
errors.push({
type: 'link',
target: newLinkEvent.after,
message: 'Cannot add any new link from a Class',
});
errors.push({
type: 'element',
target: event.target.id,
message: 'Cannot create link from a Class',
});
});
}

await delay();
return errors;
}
}
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export * from './ontodia/customization/props';
export * from './ontodia/customization/templates';

export * from './ontodia/data/model';
export { MetadataApi } from './ontodia/data/metadataApi';
export { ValidationApi, ElementError, LinkError } from './ontodia/data/validationApi';
export * from './ontodia/data/metadataApi';
export * from './ontodia/data/validationApi';
export * from './ontodia/data/provider';
export * from './ontodia/data/demo/provider';
export { RdfNode, RdfIri, RdfLiteral, Triple } from './ontodia/data/sparql/sparqlModels';
Expand All @@ -36,6 +36,7 @@ export * from './ontodia/editor/authoringState';
export {
EditorOptions, EditorEvents, EditorController, PropertyEditor, PropertyEditorOptions,
} from './ontodia/editor/editorController';
export { ValidationState, ElementValidation, LinkValidation } from './ontodia/editor/validation';

export {
LayoutData, LayoutElement, LayoutLink, SerializedDiagram,
Expand Down
32 changes: 0 additions & 32 deletions src/ontodia/customization/templates/standard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { isEncodedBlank } from '../../data/sparql/blankNodes';
import { TemplateProps } from '../props';
import { getProperty } from './utils';

import { formatLocalizedLabel } from '../../diagram/model';

import { AuthoredEntity, AuthoredEntityContext } from '../../editor/authoredEntity';
import { AuthoringState } from '../../editor/authoringState';

Expand Down Expand Up @@ -45,7 +43,6 @@ export class StandardTemplate extends Component<TemplateProps, {}> {
</div>
<div className={`${CLASS_NAME}__label`} title={label}>{label}</div>
</div>
{editor.inAuthoringMode ? this.renderValidationStatus(context) : null}
</div>
</div>
{isExpanded ? (
Expand Down Expand Up @@ -155,35 +152,6 @@ export class StandardTemplate extends Component<TemplateProps, {}> {
return getProperty(props, FOAF_NAME) || label;
}

private renderValidationStatus({editor, view}: AuthoredEntityContext) {
const validation = editor.validationState.elements.get(this.props.iri);
if (!validation) {
return null;
}
const title = validation.errors.map(error => {
if (error.linkType) {
const {id, label} = view.model.createLinkType(error.linkType);
const source = formatLocalizedLabel(id, label, view.getLanguage());
return `${source}: ${error.message}`;
} else if (error.propertyType) {
const {id, label} = view.model.createProperty(error.propertyType);
const source = formatLocalizedLabel(id, label, view.getLanguage());
return `${source}: ${error.message}`;
} else {
return error.message;
}
}).join('\n');
return (
<div className={`${CLASS_NAME}__validation`} title={title}>
{validation.loading
? <HtmlSpinner width={15} height={17} />
: <div className={`${CLASS_NAME}__invalid-icon`} />}
{(!validation.loading && validation.errors.length > 0)
? validation.errors.length : undefined}
</div>
);
}

private renderActions(context: AuthoredEntityContext) {
const {canEdit, canDelete, onEdit, onDelete} = context;
const SPINNER_WIDTH = 15;
Expand Down
34 changes: 20 additions & 14 deletions src/ontodia/data/validationApi.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import { ElementModel, LinkModel, LinkTypeIri, PropertyTypeIri } from './model';
import { DiagramModel } from '../diagram/model';
import { AuthoringState } from '../editor/authoringState';
import { CancellationToken } from '../viewUtils/async';

import { ElementModel, LinkModel, ElementIri, PropertyTypeIri } from './model';

export interface ElementError {
message: string;
linkType?: LinkTypeIri;
propertyType?: PropertyTypeIri;
readonly type: 'element';
readonly target: ElementIri;
readonly message: string;
readonly propertyType?: PropertyTypeIri;
}

export interface LinkError {
message: string;
readonly type: 'link';
readonly target: LinkModel;
readonly message: string;
}

export interface ValidationApi {
/**
* Validates element model
*/
validateElement(element: ElementModel, state: AuthoringState, ct: CancellationToken): Promise<ElementError[]>;
export interface ValidationEvent {
readonly target: ElementModel;
readonly outboundLinks: ReadonlyArray<LinkModel>;
readonly model: DiagramModel;
readonly state: AuthoringState;
readonly cancellation: CancellationToken;
}

export interface ValidationApi {
/**
* Validates link model
* Validate element and its outbound links.
*/
validateLink(
link: LinkModel, source: ElementModel, target: ElementModel, ct: CancellationToken
): Promise<LinkError[]>;
validate(e: ValidationEvent): Promise<Array<ElementError | LinkError>>;
}
69 changes: 17 additions & 52 deletions src/ontodia/editor/authoredEntity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ import * as React from 'react';

import { TemplateProps } from '../customization/props';

import { ElementIri, LinkTypeIri, PropertyTypeIri, ElementModel, LocalizedString } from '../data/model';
import { ElementModel } from '../data/model';

import { DiagramView } from '../diagram/view';
import { PaperAreaContextTypes, PaperAreaContextWrapper } from '../diagram/paperArea';

import { Cancellation } from '../viewUtils/async';
import { EventObserver } from '../viewUtils/events';
import { KeyedObserver, observeLinkTypes, observeProperties } from '../viewUtils/keyedObserver';
import { Listener } from '../viewUtils/events';

import { WorkspaceContextTypes, WorkspaceContextWrapper } from '../workspace/workspaceContext';

import { AuthoringKind, AuthoringState } from './authoringState';
import { EditorController } from './editorController';
import { AuthoringState } from './authoringState';
import { EditorController, EditorEvents } from './editorController';

export interface AuthoredEntityProps {
templateProps: TemplateProps;
Expand Down Expand Up @@ -42,10 +41,7 @@ export class AuthoredEntity extends React.Component<AuthoredEntityProps, State>
static contextTypes = {...PaperAreaContextTypes, ...WorkspaceContextTypes};
context: PaperAreaContextWrapper & WorkspaceContextWrapper;

private readonly listener = new EventObserver();
private queryCancellation = new Cancellation();
private linkTypesObserver: KeyedObserver<LinkTypeIri>;
private propertiesObserver: KeyedObserver<PropertyTypeIri>;

constructor(props: AuthoredEntityProps, context: any) {
super(props, context);
Expand All @@ -54,26 +50,11 @@ export class AuthoredEntity extends React.Component<AuthoredEntityProps, State>

componentDidMount() {
const {editor} = this.context.ontodiaWorkspace;
const {templateProps} = this.props;
const iri = templateProps.data.id;
this.listener.listen(editor.events, 'changeAuthoringState', ({previous}) => {
const current = editor.authoringState;
if (current.index.elements.get(iri) !== previous.index.elements.get(iri)) {
this.queryAllowedActions();
}
});
this.linkTypesObserver = observeLinkTypes(
editor.model, 'changeLabel', () => this.forceUpdate()
);
this.propertiesObserver = observeProperties(
editor.model, 'changeLabel', () => this.forceUpdate()
);
this.observeTypes();
editor.events.on('changeAuthoringState', this.onChangeAuthoringState);
this.queryAllowedActions();
}

componentDidUpdate(prevProps: AuthoredEntityProps) {
this.observeTypes();
const shouldUpdateAllowedActions = !(
this.props.templateProps.data === prevProps.templateProps.data &&
this.props.templateProps.isExpanded === prevProps.templateProps.isExpanded
Expand All @@ -83,40 +64,31 @@ export class AuthoredEntity extends React.Component<AuthoredEntityProps, State>
}
}

private observeTypes() {
componentWillUnmount() {
const {editor} = this.context.ontodiaWorkspace;
const iri = this.props.templateProps.data.id;
const validation = editor.validationState.elements.get(iri);
if (validation) {
this.linkTypesObserver.observe(
validation.errors.map(error => error.linkType).filter(type => type)
);
this.propertiesObserver.observe(
validation.errors.map(error => error.propertyType).filter(type => type)
);
} else {
this.linkTypesObserver.observe([]);
this.propertiesObserver.observe([]);
}
editor.events.off('changeAuthoringState', this.onChangeAuthoringState);
this.queryCancellation.abort();
}

componentWillUnmount() {
this.listener.stopListening();
this.linkTypesObserver.stopListening();
this.propertiesObserver.stopListening();
this.queryCancellation.abort();
private onChangeAuthoringState: Listener<EditorEvents, 'changeAuthoringState'> = e => {
const {source: editor, previous} = e;
const iri = this.props.templateProps.data.id;
const current = editor.authoringState;
if (current.index.elements.get(iri) !== previous.index.elements.get(iri)) {
this.queryAllowedActions();
}
}

private queryAllowedActions() {
const {isExpanded, elementId, data} = this.props.templateProps;
const {isExpanded, data} = this.props.templateProps;
// only fetch whether it's allowed to edit when expanded
if (!isExpanded) { return; }
this.queryCancellation.abort();
this.queryCancellation = new Cancellation();

const {editor} = this.context.ontodiaWorkspace;

if (!editor.metadataApi || isDeletedElement(editor.authoringState, data.id)) {
if (!editor.metadataApi || AuthoringState.isDeletedElement(editor.authoringState, data.id)) {
this.setState({canEdit: false, canDelete: false});
} else {
this.queryCanEdit(data);
Expand All @@ -126,7 +98,6 @@ export class AuthoredEntity extends React.Component<AuthoredEntityProps, State>

private queryCanEdit(data: ElementModel) {
const {editor} = this.context.ontodiaWorkspace;
const {elementId} = this.props.templateProps;
const signal = this.queryCancellation.signal;
this.setState({canEdit: undefined});
editor.metadataApi.canEditElement(data, signal).then(canEdit => {
Expand All @@ -137,7 +108,6 @@ export class AuthoredEntity extends React.Component<AuthoredEntityProps, State>

private queryCanDelete(data: ElementModel) {
const {editor} = this.context.ontodiaWorkspace;
const {elementId} = this.props.templateProps;
const signal = this.queryCancellation.signal;
this.setState({canDelete: undefined});
editor.metadataApi.canDeleteElement(data, signal).then(canDelete => {
Expand Down Expand Up @@ -171,8 +141,3 @@ export class AuthoredEntity extends React.Component<AuthoredEntityProps, State>
editor.deleteEntity(data.id);
}
}

function isDeletedElement(state: AuthoringState, target: ElementIri) {
const event = state.index.elements.get(target);
return event && event.type === AuthoringKind.DeleteElement;
}
Loading

0 comments on commit f80f23b

Please sign in to comment.