import { ElementIri , LinkModel , hashLink , sameLink } from '../data/model' ; import { ValidationApi , ValidationEvent , ElementError , LinkError } from '../data/validationApi' ; import { CancellationToken } from '../viewUtils/async' ; import { HashMap , ReadonlyHashMap , cloneMap } from '../viewUtils/collections' ; import { AuthoringState } from './authoringState' ; import { EditorController } from './editorController' ; export interface ValidationState { readonly elements : ReadonlyMap < ElementIri , ElementValidation > ; readonly links : ReadonlyHashMap < LinkModel , LinkValidation > ; } export interface ElementValidation { readonly loading : boolean ; readonly errors : ReadonlyArray < ElementError > ; } export interface LinkValidation { readonly loading : boolean ; readonly errors : ReadonlyArray < LinkError > ; } export namespace ValidationState { export const empty = createMutable ( ) ; export const emptyElement = { loading : false , errors : [ ] } ; export const emptyLink = { loading : false , errors : [ ] } ; export function createMutable ( ) { return { elements : new Map < ElementIri , ElementValidation > ( ) , links : new HashMap < LinkModel , LinkValidation > ( hashLink , sameLink ) , } ; } export function setElementErrors ( state , target , errors ) { const elements = cloneMap ( state . elements ) ; if ( errors . length > 0 ) { elements . set ( target , { loading : false , errors } ) ; } else { elements . delete ( target ) ; } return { ... state , elements } ; } export function setLinkErrors ( state , target , errors ) { const links = state . links . clone ( ) ; if ( errors . length > 0 ) { links . set ( target , { loading : false , errors } ) ; } else { links . delete ( target ) ; } return { ... state , links } ; } } export function changedElementsToValidate ( previousAuthoring , editor ) { const currentAuthoring = editor . authoringState ; const links = new HashMap < LinkModel , true > ( hashLink , sameLink ) ; previousAuthoring . links . forEach ( ( e , model ) => links . set ( model , true ) ) ; currentAuthoring . links . forEach ( ( e , model ) => links . set ( model , true ) ) ; const toValidate = new Set < ElementIri > ( ) ; links . forEach ( ( value , linkModel ) => { const current = currentAuthoring . links . get ( linkModel ) ; const previous = previousAuthoring . links . get ( linkModel ) ; if ( current !== previous ) { toValidate . add ( linkModel . sourceId ) ; } } ) ; for ( const element of editor . model . elements ) { const current = currentAuthoring . elements . get ( element . iri ) ; const previous = previousAuthoring . elements . get ( element . iri ) ; if ( current !== previous ) { toValidate . add ( element . iri ) ; if ( ( current || previous ) . deleted ) { for ( const link of element . links ) { if ( link . data . sourceId !== element . iri ) { toValidate . add ( link . data . sourceId ) ; } } } } } return toValidate ; } export function validateElements ( targets , validationApi , editor , cancellationToken ) { const previousState = editor . validationState ; const newState = ValidationState . createMutable ( ) ; for ( const element of editor . model . elements ) { if ( newState . elements . has ( element . iri ) ) { continue ; } const outboundLinks = element . links . reduce ( ( acc , link ) => { if ( link . sourceId === element . id ) { acc . push ( link . data ) ; } return acc ; } , [ ] ) ; if ( targets . has ( element . iri ) ) { const event = { target : element . data , outboundLinks , state : editor . authoringState , model : editor . model , cancellation : cancellationToken , } ; const result = CancellationToken . mapCancelledToNull ( cancellationToken , validationApi . validate ( event ) ) ; const loadingElement = { loading : true , errors : [ ] } ; const loadingLink = { loading : true , errors : [ ] } ; newState . elements . set ( element . iri , loadingElement ) ; outboundLinks . forEach ( link => newState . links . set ( link , loadingLink ) ) ; processValidationResult ( result , loadingElement , loadingLink , event , editor ) ; } else { newState . elements . set ( element . iri , previousState . elements . get ( element . iri ) ) ; for ( const link of outboundLinks ) { newState . links . set ( link , previousState . links . get ( link ) ) ; } } } editor . setValidationState ( newState ) ; } async function processValidationResult ( result , previousElement , previousLink , e , editor ) { let allErrors ; try { allErrors = await result ; if ( allErrors === null ) { return ; } } catch ( err ) { console . error ( `Failed to validate element` , e . target , err ) ; allErrors = [ { type : 'element' , target : e . target . id , message : `Failed to validate element` } ] ; } const elementErrors = [ ] ; const linkErrors = new HashMap < LinkModel , LinkError [ ] > ( hashLink , sameLink ) ; e . outboundLinks . forEach ( link => linkErrors . set ( link , [ ] ) ) ; for ( const error of allErrors ) { if ( error . type === 'element' && error . target === e . target . id ) { elementErrors . push ( error ) ; } else if ( error . type === 'link' && linkErrors . has ( error . target ) ) { linkErrors . get ( error . target ) . push ( error ) ; } } let state = editor . validationState ; if ( state . elements . get ( e . target . id ) === previousElement ) { state = ValidationState . setElementErrors ( state , e . target . id , elementErrors ) ; } linkErrors . forEach ( ( errors , link ) => { if ( state . links . get ( link ) === previousLink ) { state = ValidationState . setLinkErrors ( state , link , errors ) ; } } ) ; editor . setValidationState ( state ) ; }