Skip to content

Commit 92f21db

Browse files
committed
feat(validate_image): validations for coco file
- create a input for coco file - add validations while importing coco file - add max validation for images - always show coco import on create project - add validation on forms for no. of images - add a select input for option selection in image reference - add warning if undefined option used in image reference
1 parent fe5c18c commit 92f21db

File tree

8 files changed

+214
-105
lines changed

8 files changed

+214
-105
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React from 'react';
2+
import * as t from 'io-ts';
3+
import { isRight } from 'fp-ts/Either';
4+
5+
import JsonFileInput, { Props as JsonFileInputProps } from '#components/JsonFileInput';
6+
7+
const Image = t.type({
8+
id: t.number,
9+
// width: t.number,
10+
// height: t.number,
11+
file_name: t.string,
12+
// license: t.union([t.number, t.undefined]),
13+
flickr_url: t.union([t.string, t.undefined]),
14+
coco_url: t.union([t.string, t.undefined]),
15+
// date_captured: DateFromISOString,
16+
});
17+
18+
const CocoDataset = t.type({
19+
// info: Info,
20+
// licenses: t.array(License),
21+
images: t.array(Image),
22+
// annotations: t.array(Annotation),
23+
// categories: t.array(Category)
24+
});
25+
export type CocoDatasetType = t.TypeOf<typeof CocoDataset>
26+
27+
interface Props<N> extends Omit<JsonFileInputProps<N, object>, 'onChange' | 'value'> {
28+
value: CocoDatasetType | undefined;
29+
maxLength: number;
30+
onChange: (newValue: CocoDatasetType | undefined, name: N) => void;
31+
}
32+
function CocoFileInput<N>(props: Props<N>) {
33+
const {
34+
name,
35+
onChange,
36+
error,
37+
maxLength,
38+
...otherProps
39+
} = props;
40+
41+
const [
42+
internalErrorMessage,
43+
setInternalErrorMessage,
44+
] = React.useState<string>();
45+
46+
const handleChange = React.useCallback(
47+
(val) => {
48+
const result = CocoDataset.decode(val);
49+
if (!isRight(result)) {
50+
// eslint-disable-next-line no-console
51+
console.error('Invalid COCO format', result.left);
52+
setInternalErrorMessage('Invalid COCO format');
53+
return;
54+
}
55+
if (result.right.images.length > maxLength) {
56+
setInternalErrorMessage(`Too many images ${result.right.images.length} uploaded. Please do not exceed ${maxLength} images.`);
57+
return;
58+
}
59+
const uniqueIdentifiers = new Set(result.right.images.map((item) => item.id));
60+
if (uniqueIdentifiers.size < result.right.images.length) {
61+
setInternalErrorMessage('Each image should have a unique id.');
62+
return;
63+
}
64+
setInternalErrorMessage(undefined);
65+
onChange(result.right, name);
66+
},
67+
[onChange, maxLength, name],
68+
);
69+
70+
return (
71+
<JsonFileInput
72+
name={name}
73+
onChange={handleChange}
74+
error={internalErrorMessage ?? error}
75+
{...otherProps}
76+
/>
77+
);
78+
}
79+
80+
export default CocoFileInput;

manager-dashboard/app/components/JsonFileInput/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function readUploadedFileAsText(inputFile: File) {
2323
const ONE_MB = 1024 * 1024;
2424
const DEFAULT_MAX_FILE_SIZE = ONE_MB;
2525

26-
interface Props<N, T> extends Omit<FileInputProps<N>, 'value' | 'onChange' | 'accept'> {
26+
export interface Props<N, T> extends Omit<FileInputProps<N>, 'value' | 'onChange' | 'accept'> {
2727
maxFileSize?: number;
2828
value: T | undefined | null;
2929
onChange: (newValue: T | undefined, name: N) => void;

manager-dashboard/app/views/NewProject/index.tsx

Lines changed: 24 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ import {
3737
IoIosTrash,
3838
} from 'react-icons/io';
3939
import { Link } from 'react-router-dom';
40-
import * as t from 'io-ts';
41-
import { isRight } from 'fp-ts/Either';
4240

4341
import UserContext from '#base/context/UserContext';
4442
import projectTypeOptions from '#base/configs/projectTypes';
@@ -48,7 +46,7 @@ import TextInput from '#components/TextInput';
4846
import NumberInput from '#components/NumberInput';
4947
import SegmentInput from '#components/SegmentInput';
5048
import GeoJsonFileInput from '#components/GeoJsonFileInput';
51-
import JsonFileInput from '#components/JsonFileInput';
49+
import CocoFileInput, { CocoDatasetType } from '#components/CocoFileInput';
5250
import TileServerInput, {
5351
TILE_SERVER_BING,
5452
TILE_SERVER_ESRI,
@@ -57,6 +55,7 @@ import TileServerInput, {
5755
import InputSection from '#components/InputSection';
5856
import Button from '#components/Button';
5957
import NonFieldError from '#components/NonFieldError';
58+
import EmptyMessage from '#components/EmptyMessage';
6059
import AnimatedSwipeIcon from '#components/AnimatedSwipeIcon';
6160
import ExpandableContainer from '#components/ExpandableContainer';
6261
import AlertBanner from '#components/AlertBanner';
@@ -103,25 +102,6 @@ import ImageInput from './ImageInput';
103102
// eslint-disable-next-line postcss-modules/no-unused-class
104103
import styles from './styles.css';
105104

106-
const Image = t.type({
107-
id: t.number,
108-
// width: t.number,
109-
// height: t.number,
110-
file_name: t.string,
111-
// license: t.union([t.number, t.undefined]),
112-
flickr_url: t.union([t.string, t.undefined]),
113-
coco_url: t.union([t.string, t.undefined]),
114-
// date_captured: DateFromISOString,
115-
});
116-
const CocoDataset = t.type({
117-
// info: Info,
118-
// licenses: t.array(License),
119-
images: t.array(Image),
120-
// annotations: t.array(Annotation),
121-
// categories: t.array(Category)
122-
});
123-
// type CocoDatasetType = t.TypeOf<typeof CocoDataset>
124-
125105
const defaultProjectFormValue: PartialProjectFormType = {
126106
// projectType: PROJECT_TYPE_BUILD_AREA,
127107
projectNumber: 1,
@@ -501,34 +481,24 @@ function NewProject(props: Props) {
501481
>('images', setFieldValue);
502482

503483
const handleCocoImport = React.useCallback(
504-
(val) => {
505-
const result = CocoDataset.decode(val);
506-
if (!isRight(result)) {
507-
// eslint-disable-next-line no-console
508-
console.error('Invalid COCO format', result.left);
509-
setError((err) => ({
510-
...getErrorObject(err),
511-
[nonFieldError]: 'Invalid COCO format',
512-
}));
513-
return;
514-
}
515-
if (result.right.images.length > MAX_IMAGES) {
516-
setError((err) => ({
517-
...getErrorObject(err),
518-
[nonFieldError]: `Too many images ${result.right.images.length} uploaded. Please do not exceed ${MAX_IMAGES} images.`,
519-
}));
484+
(val: CocoDatasetType | undefined) => {
485+
if (isNotDefined(val)) {
486+
setFieldValue(
487+
[],
488+
'images',
489+
);
520490
return;
521491
}
522492
setFieldValue(
523-
() => result.right.images.map((image) => ({
493+
() => val.images.map((image) => ({
524494
sourceIdentifier: String(image.id),
525495
fileName: image.file_name,
526496
url: image.flickr_url || image.coco_url,
527497
})),
528498
'images',
529499
);
530500
},
531-
[setFieldValue, setError],
501+
[setFieldValue],
532502
);
533503

534504
const handleAddImage = React.useCallback(
@@ -613,6 +583,17 @@ function NewProject(props: Props) {
613583
<NonFieldError
614584
error={imagesError}
615585
/>
586+
<CocoFileInput
587+
name={undefined}
588+
value={undefined}
589+
onChange={handleCocoImport}
590+
maxLength={MAX_IMAGES}
591+
disabled={
592+
submissionPending
593+
|| projectTypeEmpty
594+
}
595+
label="Import COCO file"
596+
/>
616597
{(images && images.length > 0) ? (
617598
<div className={styles.imageList}>
618599
{images.map((image, index) => (
@@ -647,15 +628,9 @@ function NewProject(props: Props) {
647628
))}
648629
</div>
649630
) : (
650-
<JsonFileInput<undefined, object>
651-
name={undefined}
652-
onChange={handleCocoImport}
653-
disabled={
654-
submissionPending
655-
|| projectTypeEmpty
656-
}
657-
label="Import COCO file"
658-
value={undefined}
631+
<EmptyMessage
632+
title="Start adding images"
633+
description="Add images using COCO file or manually add images"
659634
/>
660635
)}
661636
</InputSection>

manager-dashboard/app/views/NewProject/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,11 +638,16 @@ export const projectFormSchema: ProjectFormSchema = {
638638
['images'],
639639
(formValues) => {
640640
// FIXME: Add "unique" constraint for sourceIdentifier and fileName
641-
// FIXME: Add max length constraint
642641
if (formValues?.projectType === PROJECT_TYPE_VALIDATE_IMAGE) {
643642
return {
644643
images: {
645644
keySelector: (key) => key.sourceIdentifier,
645+
validation: (values) => {
646+
if (values && values.length > MAX_IMAGES) {
647+
return `Too many images ${values.length}. Please do not exceed ${MAX_IMAGES} images.`;
648+
}
649+
return undefined;
650+
},
646651
member: (): ImageFormSchemaMember => ({
647652
fields: (): ImageSchemaFields => ({
648653
sourceIdentifier: {

manager-dashboard/app/views/NewTutorial/ImageInput/index.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import React from 'react';
1+
import React, { useMemo } from 'react';
22

33
import {
44
SetValueArg,
55
Error,
66
useFormObject,
77
getErrorObject,
88
} from '@togglecorp/toggle-form';
9+
import { isNotDefined, isDefined, unique } from '@togglecorp/fujs';
910
import TextInput from '#components/TextInput';
11+
import SelectInput from '#components/SelectInput';
1012
import NumberInput from '#components/NumberInput';
1113

1214
import {
1315
ImageType,
16+
PartialCustomOptionsType,
1417
} from '../utils';
1518

1619
import styles from './styles.css';
@@ -26,6 +29,7 @@ interface Props {
2629
error: Error<ImageType> | undefined;
2730
disabled?: boolean;
2831
readOnly?: boolean;
32+
customOptions: PartialCustomOptionsType | undefined;
2933
}
3034

3135
export default function ImageInput(props: Props) {
@@ -36,8 +40,45 @@ export default function ImageInput(props: Props) {
3640
error: riskyError,
3741
disabled,
3842
readOnly,
43+
customOptions,
3944
} = props;
4045

46+
const flattenedOptions = useMemo(
47+
() => {
48+
const opts = customOptions?.flatMap(
49+
(option) => ([
50+
{
51+
key: option.value,
52+
label: option.title,
53+
},
54+
...(option.subOptions ?? []).map(
55+
(subOption) => ({
56+
key: subOption.value,
57+
label: subOption.description,
58+
}),
59+
),
60+
]),
61+
) ?? [];
62+
63+
const validOpts = opts.map(
64+
(option) => {
65+
if (isNotDefined(option.key)) {
66+
return undefined;
67+
}
68+
return {
69+
...option,
70+
key: option.key,
71+
};
72+
},
73+
).filter(isDefined);
74+
return unique(
75+
validOpts,
76+
(option) => option.key,
77+
);
78+
},
79+
[customOptions],
80+
);
81+
4182
const onImageChange = useFormObject(index, onChange, defaultImageValue);
4283

4384
const error = getErrorObject(riskyError);
@@ -78,12 +119,14 @@ export default function ImageInput(props: Props) {
78119
disabled={disabled}
79120
readOnly
80121
/>
81-
{/* FIXME: Use select input */}
82-
<NumberInput
122+
<SelectInput
83123
label="Reference Answer"
84124
value={value?.referenceAnswer}
85125
name={'referenceAnswer' as const}
86126
onChange={onImageChange}
127+
keySelector={(option) => option.key}
128+
labelSelector={(option) => option.label ?? `Option ${option.key}`}
129+
options={flattenedOptions}
87130
error={error?.referenceAnswer}
88131
disabled={disabled || readOnly}
89132
/>

manager-dashboard/app/views/NewTutorial/ScenarioPageInput/ValidateImagePreview/styles.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
.image-preview {
88
position: relative;
9-
border: 1px solid red;
109
width: 100%;
1110
height: var(--height-mobile-preview-validate-image-content);
1211
}

0 commit comments

Comments
 (0)