44 isDefined ,
55 unique ,
66 isNotDefined ,
7+ isTruthyString ,
78} from '@togglecorp/fujs' ;
89import {
910 useForm ,
@@ -63,6 +64,7 @@ import {
6364 PROJECT_TYPE_COMPLETENESS ,
6465 PROJECT_TYPE_CHANGE_DETECTION ,
6566 PROJECT_TYPE_FOOTPRINT ,
67+ ProjectType ,
6668} from '#utils/common' ;
6769
6870import {
@@ -81,15 +83,14 @@ import {
8183 MAX_INFO_PAGES ,
8284 MAX_OPTIONS ,
8385 deleteKey ,
86+ TutorialTasksGeoJSON ,
8487} from './utils' ;
88+
8589import CustomOptionPreview from './CustomOptionInput/CustomOptionPreview' ;
8690import CustomOptionInput from './CustomOptionInput' ;
8791import ScenarioPageInput from './ScenarioPageInput' ;
8892import InformationPageInput from './InformationPageInput' ;
8993import styles from './styles.css' ;
90- import { TutorialTasksGeoJSON } from './utils' ;
91- import { FootprintProperties } from './utils' ;
92- import { BuildAreaProperties , ChangeDetectionProperties } from './utils' ;
9394
9495type CustomScreen = Omit < TutorialFormType [ 'scenarioPages' ] [ number ] , 'scenarioId' > ;
9596function sanitizeScreens ( scenarioPages : TutorialFormType [ 'scenarioPages' ] ) {
@@ -355,41 +356,138 @@ function NewTutorial(props: Props) {
355356 geoProps : GeoJSON . GeoJSON | undefined ,
356357 ) => {
357358 const tutorialTasks = geoProps as TutorialTasksGeoJSON ;
358- if ( ! tutorialTasks ) {
359- return ;
360- }
359+ function getGeoJSONError ( ) {
360+ if ( isNotDefined ( tutorialTasks . features ) || ! Array . isArray ( tutorialTasks . features ) ) {
361+ return 'GeoJson does not contain iterable properties' ;
362+ }
361363
362- const everyTaskHasScreenAndReferenceProperty = tutorialTasks . features . every (
363- ( feature ) => isDefined ( feature . properties . screen ) && isDefined ( feature . properties . reference ) ,
364- ) ;
364+ type ValidType = 'number' | 'string' | 'boolean' ;
365+ function checkSchema < T extends object > (
366+ obj : T ,
367+ schema : Record < string , ValidType | ValidType [ ] > ,
368+ ) {
369+ const schemaKeys = Object . keys ( schema ) ;
370+ const errors = schemaKeys . map (
371+ ( key ) => {
372+ const expectedType = schema [ key ] ;
373+
374+ const keySafe = key as keyof T ;
375+ const currentValue : unknown = obj [ keySafe ] ;
376+ const valueType = typeof currentValue ;
377+
378+ if ( Array . isArray ( expectedType ) ) {
379+ const indexOfType = expectedType . findIndex (
380+ ( type ) => type === valueType ,
381+ ) ;
382+ if ( indexOfType === - 1 ) {
383+ return `type of ${ key } expected to be one of type ${ expectedType . join ( ', ' ) } ` ;
384+ }
385+ } else if ( typeof currentValue !== expectedType ) {
386+ return `type of ${ key } expected to be of ${ expectedType } ` ;
387+ }
365388
366- if ( ! everyTaskHasScreenAndReferenceProperty ) {
367- setError ( ( prevValue ) => ( {
368- ...getErrorObject ( prevValue ) ,
369- tutorialTasks : 'GeoJson does not contain property "screen" or "reference"' ,
370- } ) ) ;
389+ return undefined ;
390+ } ,
391+ ) . filter ( isDefined ) ;
392+
393+ return errors ;
394+ }
395+
396+ if ( value ?. projectType === PROJECT_TYPE_FOOTPRINT ) {
397+ const errors = tutorialTasks . features . map (
398+ ( feature ) => checkSchema ( feature . properties , {
399+ id : [ 'string' , 'number' ] ,
400+ reference : 'number' ,
401+ screen : 'number' ,
402+ } ) . join ( ', ' ) ,
403+ ) . filter ( isTruthyString ) ;
404+
405+ if ( errors . length > 0 ) {
406+ return `Invalid GeoJson for Footprint: ${ errors [ 0 ] } (${ errors . length } total errors)` ;
407+ }
408+ }
409+
410+ if ( value ?. projectType === PROJECT_TYPE_CHANGE_DETECTION ) {
411+ const errors = tutorialTasks . features . map (
412+ ( feature ) => checkSchema ( feature . properties , {
413+ reference : 'number' ,
414+ screen : 'number' ,
415+ task_id : 'string' ,
416+ tile_x : 'number' ,
417+ tile_y : 'number' ,
418+ tile_z : 'number' ,
419+ } ) . join ( ', ' ) ,
420+ ) . filter ( isTruthyString ) ;
421+
422+ if ( errors . length > 0 ) {
423+ // NOTE: only showing errors first error
424+ return `Invalid GeoJson for Change Detection: ${ errors [ 0 ] } (${ errors . length } total errors)` ;
425+ }
426+ }
427+
428+ if ( value ?. projectType === PROJECT_TYPE_BUILD_AREA ) {
429+ const errors = tutorialTasks . features . map (
430+ ( feature ) => checkSchema ( feature . properties , {
431+ reference : 'number' ,
432+ screen : 'number' ,
433+ task_id : 'string' ,
434+ tile_x : 'number' ,
435+ tile_y : 'number' ,
436+ tile_z : 'number' ,
437+ } ) . join ( ', ' ) ,
438+ ) . filter ( isTruthyString ) ;
439+
440+ if ( errors . length > 0 ) {
441+ // NOTE: only showing errors first error
442+ return `Invalid GeoJson for Build Area: ${ errors [ 0 ] } (${ errors . length } total errors)` ;
443+ }
444+ }
371445
446+ if ( value ?. projectType === PROJECT_TYPE_COMPLETENESS ) {
447+ const errors = tutorialTasks . features . map (
448+ ( feature ) => checkSchema ( feature . properties , {
449+ reference : 'number' ,
450+ screen : 'number' ,
451+ task_id : 'string' ,
452+ tile_x : 'number' ,
453+ tile_y : 'number' ,
454+ tile_z : 'number' ,
455+ } ) . join ( ', ' ) ,
456+ ) . filter ( isTruthyString ) ;
457+
458+ if ( errors . length > 0 ) {
459+ return `Invalid GeoJson for Completeness: ${ errors [ 0 ] } (${ errors . length } total errors)` ;
460+ }
461+ }
462+ // TODO: validate references
463+
464+ return undefined ;
465+ }
466+
467+ if ( ! tutorialTasks ) {
468+ setFieldValue ( undefined , 'tutorialTasks' ) ;
469+ setFieldValue ( undefined , 'scenarioPages' ) ;
372470 return ;
373471 }
374472
375- if ( value ?. projectType === PROJECT_TYPE_COMPLETENESS
376- || value ?. projectType === PROJECT_TYPE_BUILD_AREA
377- || value ?. projectType === PROJECT_TYPE_CHANGE_DETECTION ) {
378-
379- tutorialTasks . features . every (
380- ( feature : BuildAreaProperties | ChangeDetectionProperties ) => isDefined ( feature . properties . task_id ) ,
381- ) ;
473+ const errors = getGeoJSONError ( ) ;
474+ if ( errors ) {
475+ setFieldValue ( undefined , 'tutorialTasks' ) ;
476+ setFieldValue ( undefined , 'scenarioPages' ) ;
477+ setError ( ( prevError ) => ( {
478+ ...getErrorObject ( prevError ) ,
479+ tutorialTasks : errors ,
480+ } ) ) ;
481+ return ;
382482 }
383483
384-
385484 setFieldValue ( tutorialTasks , 'tutorialTasks' ) ;
386485
387- // FIXME: we need to validate the geojson here
388-
389- const uniqueArray = tutorialTasks && unique (
390- tutorialTasks . features , ( ( geo ) => geo ?. properties . screen ) ,
486+ const uniqueArray = unique (
487+ tutorialTasks . features ,
488+ ( ( geo ) => geo ?. properties . screen ) ,
391489 ) ;
392- const sorted = uniqueArray ? .sort ( ( a , b ) => a . properties . screen - b . properties . screen ) ;
490+ const sorted = uniqueArray . sort ( ( a , b ) => a . properties . screen - b . properties . screen ) ;
393491 const tutorialTaskArray = sorted ?. map ( ( geo ) => (
394492 {
395493 scenarioId : geo . properties . screen ,
@@ -400,10 +498,7 @@ function NewTutorial(props: Props) {
400498 ) ) ;
401499
402500 setFieldValue ( tutorialTaskArray , 'scenarioPages' ) ;
403-
404- } , [ setFieldValue ] ) ;
405-
406- console . log ( formError ) ;
501+ } , [ setFieldValue , setError , value ?. projectType ] ) ;
407502
408503 const handleAddInformationPage = React . useCallback (
409504 ( template : InformationPageTemplateKey ) => {
@@ -480,6 +575,16 @@ function NewTutorial(props: Props) {
480575 customOptions,
481576 informationPages,
482577 } = value ;
578+
579+ const handleProjectTypeChange = React . useCallback (
580+ ( newValue : ProjectType | undefined ) => {
581+ setFieldValue ( undefined , 'tutorialTasks' ) ;
582+ setFieldValue ( undefined , 'scenarioPages' ) ;
583+ setFieldValue ( newValue , 'projectType' ) ;
584+ } ,
585+ [ setFieldValue ] ,
586+ ) ;
587+
483588 return (
484589 < div className = { _cs ( styles . newTutorial , className ) } >
485590 < Heading level = { 1 } >
@@ -491,7 +596,7 @@ function NewTutorial(props: Props) {
491596 >
492597 < SegmentInput
493598 name = { 'projectType' as const }
494- onChange = { setFieldValue }
599+ onChange = { handleProjectTypeChange }
495600 value = { value . projectType }
496601 label = "Project Type"
497602 hint = "Select the type of your project."
0 commit comments