| undefined;
+ disabled?: boolean;
+ readOnly?: boolean;
+}
+
+export default function ImageInput(props: Props) {
+ const {
+ value,
+ onChange,
+ index,
+ error: riskyError,
+ disabled,
+ readOnly,
+ } = props;
+
+ const onImageChange = useFormObject(index, onChange, defaultImageValue);
+
+ const error = getErrorObject(riskyError);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/manager-dashboard/app/views/NewProject/ImageInput/styles.css b/manager-dashboard/app/views/NewProject/ImageInput/styles.css
new file mode 100644
index 000000000..a6e6f1707
--- /dev/null
+++ b/manager-dashboard/app/views/NewProject/ImageInput/styles.css
@@ -0,0 +1,5 @@
+.image-input {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-medium);
+}
diff --git a/manager-dashboard/app/views/NewProject/index.tsx b/manager-dashboard/app/views/NewProject/index.tsx
index 8c378597c..cbc94b5ad 100644
--- a/manager-dashboard/app/views/NewProject/index.tsx
+++ b/manager-dashboard/app/views/NewProject/index.tsx
@@ -3,6 +3,7 @@ import {
_cs,
isDefined,
isNotDefined,
+ randomString,
} from '@togglecorp/fujs';
import {
useForm,
@@ -10,6 +11,7 @@ import {
createSubmitHandler,
analyzeErrors,
nonFieldError,
+ useFormArray,
} from '@togglecorp/toggle-form';
import {
getStorage,
@@ -29,7 +31,11 @@ import {
import {
MdOutlinePublishedWithChanges,
MdOutlineUnpublished,
+ MdAdd,
} from 'react-icons/md';
+import {
+ IoIosTrash,
+} from 'react-icons/io';
import { Link } from 'react-router-dom';
import UserContext from '#base/context/UserContext';
@@ -40,6 +46,7 @@ import TextInput from '#components/TextInput';
import NumberInput from '#components/NumberInput';
import SegmentInput from '#components/SegmentInput';
import GeoJsonFileInput from '#components/GeoJsonFileInput';
+import CocoFileInput, { CocoDatasetType } from '#components/CocoFileInput';
import TileServerInput, {
TILE_SERVER_BING,
TILE_SERVER_ESRI,
@@ -48,6 +55,7 @@ import TileServerInput, {
import InputSection from '#components/InputSection';
import Button from '#components/Button';
import NonFieldError from '#components/NonFieldError';
+import EmptyMessage from '#components/EmptyMessage';
import AnimatedSwipeIcon from '#components/AnimatedSwipeIcon';
import ExpandableContainer from '#components/ExpandableContainer';
import AlertBanner from '#components/AlertBanner';
@@ -60,6 +68,7 @@ import {
ProjectInputType,
PROJECT_TYPE_BUILD_AREA,
PROJECT_TYPE_FOOTPRINT,
+ PROJECT_TYPE_VALIDATE_IMAGE,
PROJECT_TYPE_COMPLETENESS,
PROJECT_TYPE_CHANGE_DETECTION,
PROJECT_TYPE_STREET,
@@ -73,6 +82,7 @@ import CustomOptionPreview from '#views/NewTutorial/CustomOptionInput/CustomOpti
import {
projectFormSchema,
ProjectFormType,
+ ImageType,
PartialProjectFormType,
projectInputTypeOptions,
filterOptions,
@@ -84,8 +94,10 @@ import {
getGroupSize,
validateAoiOnOhsome,
validateProjectIdOnHotTaskingManager,
+ MAX_IMAGES,
} from './utils';
import BasicProjectInfoForm from './BasicProjectInfoForm';
+import ImageInput from './ImageInput';
// eslint-disable-next-line postcss-modules/no-unused-class
import styles from './styles.css';
@@ -108,6 +120,7 @@ const defaultProjectFormValue: PartialProjectFormType = {
// maxTasksPerUser: -1,
inputType: PROJECT_INPUT_TYPE_UPLOAD,
filter: FILTER_BUILDINGS,
+ randomizeOrder: false,
panoOnly: false,
};
@@ -447,11 +460,67 @@ function NewProject(props: Props) {
})),
}))), [customOptionsFromValue]);
- const optionsError = React.useMemo(
+ const customOptionsError = React.useMemo(
() => getErrorObject(error?.customOptions),
[error?.customOptions],
);
+ const { images } = value;
+
+ const imagesError = React.useMemo(
+ () => getErrorObject(error?.images),
+ [error?.images],
+ );
+
+ const {
+ setValue: setImageValue,
+ removeValue: onImageRemove,
+ } = useFormArray<
+ 'images',
+ ImageType
+ >('images', setFieldValue);
+
+ const handleCocoImport = React.useCallback(
+ (val: CocoDatasetType | undefined) => {
+ if (isNotDefined(val)) {
+ setFieldValue(
+ [],
+ 'images',
+ );
+ return;
+ }
+ setFieldValue(
+ () => val.images.map((image) => ({
+ sourceIdentifier: String(image.id),
+ fileName: image.file_name,
+ url: image.flickr_url || image.coco_url,
+ })),
+ 'images',
+ );
+ },
+ [setFieldValue],
+ );
+
+ const handleAddImage = React.useCallback(
+ () => {
+ setFieldValue(
+ (oldValue: PartialProjectFormType['images']) => {
+ const safeOldValues = oldValue ?? [];
+
+ const newDefineOption: ImageType = {
+ sourceIdentifier: randomString(),
+ };
+
+ return [...safeOldValues, newDefineOption];
+ },
+ 'images',
+ );
+ },
+ [
+ setFieldValue,
+ ],
+ );
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noOp = () => {};
@@ -491,8 +560,84 @@ function NewProject(props: Props) {
disabled={submissionPending || projectTypeEmpty}
/>
+ {(value.projectType === PROJECT_TYPE_VALIDATE_IMAGE) && (
+
+ }
+ onClick={handleAddImage}
+ disabled={
+ submissionPending
+ || projectTypeEmpty
+ || (images && images.length >= MAX_IMAGES)
+ }
+ >
+ Add Image
+
+ >
+ )}
+ >
+
+
+ {(images && images.length > 0) ? (
+
+ {images.map((image, index) => (
+
+
+
+ )}
+ >
+
+
+ ))}
+
+ ) : (
+
+ )}
+
+ )}
{(
(value.projectType === PROJECT_TYPE_FOOTPRINT
+ || value.projectType === PROJECT_TYPE_VALIDATE_IMAGE
|| value.projectType === PROJECT_TYPE_STREET)
&& customOptions
&& customOptions.length > 0
@@ -501,7 +646,7 @@ function NewProject(props: Props) {
heading="Custom Options"
>
{(customOptions && customOptions.length > 0) ? (
@@ -516,7 +661,7 @@ function NewProject(props: Props) {
value={option}
index={index}
onChange={noOp}
- error={optionsError?.[option.value]}
+ error={customOptionsError?.[option.value]}
readOnly
/>
@@ -742,7 +887,7 @@ function NewProject(props: Props) {
value={value?.organizationId}
onChange={setFieldValue}
error={error?.organizationId}
- label="Mapillary Organization ID"
+ label="Mapillary Organization IidD"
hint="Provide a valid Mapillary organization ID to filter for images belonging to a specific organization. Empty indicates that no filter is set on organization."
disabled={submissionPending || projectTypeEmpty}
/>
@@ -763,6 +908,13 @@ function NewProject(props: Props) {
onChange={setFieldValue}
disabled={submissionPending || projectTypeEmpty}
/>
+
)}
diff --git a/manager-dashboard/app/views/NewProject/styles.css b/manager-dashboard/app/views/NewProject/styles.css
index cbfa76230..45aedf1bc 100644
--- a/manager-dashboard/app/views/NewProject/styles.css
+++ b/manager-dashboard/app/views/NewProject/styles.css
@@ -13,6 +13,14 @@
max-width: 70rem;
gap: var(--spacing-large);
+
+ .image-list {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ gap: var(--spacing-medium);
+ }
+
.custom-option-container {
display: flex;
gap: var(--spacing-large);
diff --git a/manager-dashboard/app/views/NewProject/utils.ts b/manager-dashboard/app/views/NewProject/utils.ts
index 1d237a4c8..e2ac2731f 100644
--- a/manager-dashboard/app/views/NewProject/utils.ts
+++ b/manager-dashboard/app/views/NewProject/utils.ts
@@ -34,6 +34,7 @@ import {
ProjectInputType,
PROJECT_TYPE_BUILD_AREA,
PROJECT_TYPE_FOOTPRINT,
+ PROJECT_TYPE_VALIDATE_IMAGE,
PROJECT_TYPE_CHANGE_DETECTION,
PROJECT_TYPE_COMPLETENESS,
PROJECT_TYPE_STREET,
@@ -68,13 +69,14 @@ export interface ProjectFormType {
projectImage: File; // image
verificationNumber: number;
groupSize: number;
+ maxTasksPerUser: number;
+
zoomLevel: number;
geometry?: GeoJSON.GeoJSON | string;
inputType?: ProjectInputType;
TMId?: string;
filter?: string;
filterText?: string;
- maxTasksPerUser: number;
tileServer: TileServer;
tileServerB?: TileServer;
customOptions?: CustomOptionsForProject;
@@ -83,9 +85,15 @@ export interface ProjectFormType {
endTimestamp?: string | null;
organizationId?: number;
creatorId?: number;
+ randomizeOrder?: boolean;
panoOnly?: boolean;
isPano?: boolean | null;
samplingThreshold?: number;
+ images?: {
+ sourceIdentifier: string;
+ fileName: string;
+ url: string;
+ }[];
}
export const PROJECT_INPUT_TYPE_UPLOAD = 'aoi_file';
@@ -114,9 +122,11 @@ export const filterOptions = [
export type PartialProjectFormType = PartialForm<
Omit & { projectImage?: File },
// NOTE: we do not want to change File and FeatureCollection to partials
- 'geometry' | 'projectImage' | 'value'
+ 'geometry' | 'projectImage' | 'value' | 'sourceIdentifier'
>;
+export type ImageType = NonNullable[number];
+
type ProjectFormSchema = ObjectSchema;
type ProjectFormSchemaFields = ReturnType;
@@ -126,6 +136,12 @@ type CustomOptionSchemaFields = ReturnType
type CustomOptionFormSchema = ArraySchema;
type CustomOptionFormSchemaMember = ReturnType;
+type PartialImages = NonNullable[number];
+type ImageSchema = ObjectSchema;
+type ImageSchemaFields = ReturnType
+type ImageFormSchema = ArraySchema;
+type ImageFormSchemaMember = ReturnType;
+
// FIXME: break this into multiple geometry conditions
const DEFAULT_MAX_FEATURES = 20;
// const DEFAULT_MAX_FEATURES = 10;
@@ -193,6 +209,8 @@ function validGeometryCondition(zoomLevel: number | undefined | null) {
return validGeometryConditionForZoom;
}
+export const MAX_IMAGES = 2000;
+
export const MAX_OPTIONS = 6;
export const MIN_OPTIONS = 2;
export const MAX_SUB_OPTIONS = 6;
@@ -274,53 +292,25 @@ export const projectFormSchema: ProjectFormSchema = {
lessThanOrEqualToCondition(250),
],
},
- tileServer: {
- fields: tileServerFieldsSchema,
- },
maxTasksPerUser: {
validations: [
integerCondition,
greaterThanCondition(0),
],
},
- dateRange: {
- required: false,
- },
- creatorId: {
- required: false,
- validations: [
- integerCondition,
- greaterThanCondition(0),
- ],
- },
- organizationId: {
- required: false,
- validations: [
- integerCondition,
- greaterThanCondition(0),
- ],
- },
- samplingThreshold: {
- required: false,
- validation: [
- greaterThanCondition(0),
- ],
- },
- panoOnly: {
- required: false,
- },
- isPano: {
- required: false,
- },
};
+ // Common
+
baseSchema = addCondition(
baseSchema,
value,
['projectType'],
['customOptions'],
(formValues) => {
- if (formValues?.projectType === PROJECT_TYPE_FOOTPRINT) {
+ if (formValues?.projectType === PROJECT_TYPE_FOOTPRINT
+ || formValues?.projectType === PROJECT_TYPE_VALIDATE_IMAGE
+ || formValues?.projectType === PROJECT_TYPE_STREET) {
return {
customOptions: {
keySelector: (key) => key.value,
@@ -383,8 +373,8 @@ export const projectFormSchema: ProjectFormSchema = {
const projectType = v?.projectType;
if (
projectType === PROJECT_TYPE_BUILD_AREA
- || projectType === PROJECT_TYPE_COMPLETENESS
|| projectType === PROJECT_TYPE_CHANGE_DETECTION
+ || projectType === PROJECT_TYPE_COMPLETENESS
) {
return {
zoomLevel: {
@@ -403,24 +393,6 @@ export const projectFormSchema: ProjectFormSchema = {
},
);
- baseSchema = addCondition(
- baseSchema,
- value,
- ['projectType'],
- ['inputType'],
- (v) => {
- const projectType = v?.projectType;
- if (projectType === PROJECT_TYPE_FOOTPRINT) {
- return {
- inputType: { required: true },
- };
- }
- return {
- inputType: { forceValue: nullValue },
- };
- },
- );
-
baseSchema = addCondition(
baseSchema,
value,
@@ -432,8 +404,8 @@ export const projectFormSchema: ProjectFormSchema = {
const zoomLevel = v?.zoomLevel;
if (
projectType === PROJECT_TYPE_BUILD_AREA
- || projectType === PROJECT_TYPE_COMPLETENESS
|| projectType === PROJECT_TYPE_CHANGE_DETECTION
+ || projectType === PROJECT_TYPE_COMPLETENESS
|| projectType === PROJECT_TYPE_STREET
|| (projectType === PROJECT_TYPE_FOOTPRINT && (
inputType === PROJECT_INPUT_TYPE_UPLOAD
@@ -478,6 +450,51 @@ export const projectFormSchema: ProjectFormSchema = {
},
);
+ baseSchema = addCondition(
+ baseSchema,
+ value,
+ ['projectType'],
+ ['tileServer'],
+ (v) => {
+ const projectType = v?.projectType;
+ if (
+ projectType === PROJECT_TYPE_BUILD_AREA
+ || projectType === PROJECT_TYPE_COMPLETENESS
+ || projectType === PROJECT_TYPE_CHANGE_DETECTION
+ || projectType === PROJECT_TYPE_FOOTPRINT
+ ) {
+ return {
+ tileServer: {
+ fields: tileServerFieldsSchema,
+ },
+ };
+ }
+ return {
+ tileServer: { forceValue: nullValue },
+ };
+ },
+ );
+
+ // Validate
+
+ baseSchema = addCondition(
+ baseSchema,
+ value,
+ ['projectType'],
+ ['inputType'],
+ (v) => {
+ const projectType = v?.projectType;
+ if (projectType === PROJECT_TYPE_FOOTPRINT) {
+ return {
+ inputType: { required: true },
+ };
+ }
+ return {
+ inputType: { forceValue: nullValue },
+ };
+ },
+ );
+
baseSchema = addCondition(
baseSchema,
value,
@@ -555,6 +572,108 @@ export const projectFormSchema: ProjectFormSchema = {
},
);
+ // Street
+
+ baseSchema = addCondition(
+ baseSchema,
+ value,
+ ['projectType'],
+ ['dateRange', 'creatorId', 'organizationId', 'samplingThreshold', 'panoOnly', 'isPano', 'randomizeOrder'],
+ (formValues) => {
+ if (formValues?.projectType === PROJECT_TYPE_STREET) {
+ return {
+ dateRange: {
+ required: false,
+ },
+ creatorId: {
+ required: false,
+ validations: [
+ integerCondition,
+ greaterThanCondition(0),
+ ],
+ },
+ organizationId: {
+ required: false,
+ validations: [
+ integerCondition,
+ greaterThanCondition(0),
+ ],
+ },
+ samplingThreshold: {
+ required: false,
+ validations: [
+ greaterThanCondition(0),
+ ],
+ },
+ panoOnly: {
+ required: false,
+ },
+ // FIXME: This is not used.
+ isPano: {
+ required: false,
+ },
+ randomizeOrder: {
+ required: false,
+ },
+ };
+ }
+ return {
+ dateRange: { forceValue: nullValue },
+ creatorId: { forceValue: nullValue },
+ organizationId: { forceValue: nullValue },
+ samplingThreshold: { forceValue: nullValue },
+ panoOnly: { forceValue: nullValue },
+ isPano: { forceValude: nullValue },
+ randomizeOrder: { forceValue: nullValue },
+ };
+ },
+ );
+
+ // Validate Image
+
+ baseSchema = addCondition(
+ baseSchema,
+ value,
+ ['projectType'],
+ ['images'],
+ (formValues) => {
+ // FIXME: Add "unique" constraint for sourceIdentifier and fileName
+ if (formValues?.projectType === PROJECT_TYPE_VALIDATE_IMAGE) {
+ return {
+ images: {
+ keySelector: (key) => key.sourceIdentifier,
+ validation: (values) => {
+ if (values && values.length > MAX_IMAGES) {
+ return `Too many images ${values.length}. Please do not exceed ${MAX_IMAGES} images.`;
+ }
+ return undefined;
+ },
+ member: (): ImageFormSchemaMember => ({
+ fields: (): ImageSchemaFields => ({
+ sourceIdentifier: {
+ required: true,
+ requiredValidation: requiredStringCondition,
+ },
+ fileName: {
+ required: true,
+ requiredValidation: requiredStringCondition,
+ },
+ url: {
+ required: true,
+ requiredValidation: requiredStringCondition,
+ validations: [urlCondition],
+ },
+ }),
+ }),
+ },
+ };
+ }
+ return {
+ images: { forceValue: nullValue },
+ };
+ },
+ );
+
return baseSchema;
},
};
@@ -583,6 +702,7 @@ export function getGroupSize(projectType: ProjectType | undefined) {
}
if (projectType === PROJECT_TYPE_FOOTPRINT
+ || projectType === PROJECT_TYPE_VALIDATE_IMAGE
|| projectType === PROJECT_TYPE_CHANGE_DETECTION
|| projectType === PROJECT_TYPE_STREET) {
return 25;
@@ -676,7 +796,7 @@ async function fetchAoiFromHotTaskingManager(projectId: number | string): (
let response;
try {
response = await fetch(
- `https://tasking-manager-tm4-production-api.hotosm.org/api/v2/projects/${projectId}/queries/aoi/?as_file=false`,
+ `https://tasking-manager-production-api.hotosm.org/api/v2/projects/${projectId}/queries/aoi/?as_file=false`,
);
} catch {
return {
diff --git a/manager-dashboard/app/views/NewTutorial/ImageInput/index.tsx b/manager-dashboard/app/views/NewTutorial/ImageInput/index.tsx
new file mode 100644
index 000000000..ca10b5806
--- /dev/null
+++ b/manager-dashboard/app/views/NewTutorial/ImageInput/index.tsx
@@ -0,0 +1,135 @@
+import React, { useMemo } from 'react';
+
+import {
+ SetValueArg,
+ Error,
+ useFormObject,
+ getErrorObject,
+} from '@togglecorp/toggle-form';
+import { isNotDefined, isDefined, unique } from '@togglecorp/fujs';
+import TextInput from '#components/TextInput';
+import SelectInput from '#components/SelectInput';
+import NumberInput from '#components/NumberInput';
+
+import {
+ ImageType,
+ PartialCustomOptionsType,
+} from '../utils';
+
+import styles from './styles.css';
+
+const defaultImageValue: ImageType = {
+ sourceIdentifier: '',
+};
+
+interface Props {
+ value: ImageType;
+ onChange: (value: SetValueArg, index: number) => void | undefined;
+ index: number;
+ error: Error | undefined;
+ disabled?: boolean;
+ readOnly?: boolean;
+ customOptions: PartialCustomOptionsType | undefined;
+}
+
+export default function ImageInput(props: Props) {
+ const {
+ value,
+ onChange,
+ index,
+ error: riskyError,
+ disabled,
+ readOnly,
+ customOptions,
+ } = props;
+
+ const flattenedOptions = useMemo(
+ () => {
+ const opts = customOptions?.flatMap(
+ (option) => ([
+ {
+ key: option.value,
+ label: option.title,
+ },
+ ...(option.subOptions ?? []).map(
+ (subOption) => ({
+ key: subOption.value,
+ label: subOption.description,
+ }),
+ ),
+ ]),
+ ) ?? [];
+
+ const validOpts = opts.map(
+ (option) => {
+ if (isNotDefined(option.key)) {
+ return undefined;
+ }
+ return {
+ ...option,
+ key: option.key,
+ };
+ },
+ ).filter(isDefined);
+ return unique(
+ validOpts,
+ (option) => option.key,
+ );
+ },
+ [customOptions],
+ );
+
+ const onImageChange = useFormObject(index, onChange, defaultImageValue);
+
+ const error = getErrorObject(riskyError);
+
+ return (
+
+
+
+
+
+ option.key}
+ labelSelector={(option) => option.label ?? `Option ${option.key}`}
+ options={flattenedOptions}
+ error={error?.referenceAnswer}
+ disabled={disabled || readOnly}
+ />
+
+ );
+}
diff --git a/manager-dashboard/app/views/NewTutorial/ImageInput/styles.css b/manager-dashboard/app/views/NewTutorial/ImageInput/styles.css
new file mode 100644
index 000000000..a6e6f1707
--- /dev/null
+++ b/manager-dashboard/app/views/NewTutorial/ImageInput/styles.css
@@ -0,0 +1,5 @@
+.image-input {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-medium);
+}
diff --git a/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/FootprintGeoJsonPreview/index.tsx b/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/FootprintGeoJsonPreview/index.tsx
index f381ff4f9..2ab9cbe36 100644
--- a/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/FootprintGeoJsonPreview/index.tsx
+++ b/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/FootprintGeoJsonPreview/index.tsx
@@ -15,7 +15,7 @@ import {
import styles from './styles.css';
// NOTE: the padding is selected wrt the size of the preview
-const footprintGeojsonPadding = [140, 140];
+const footprintGeojsonPadding: [number, number] = [140, 140];
interface Props {
className?: string;
diff --git a/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/ValidateImagePreview/index.tsx b/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/ValidateImagePreview/index.tsx
new file mode 100644
index 000000000..3dfa8fb98
--- /dev/null
+++ b/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/ValidateImagePreview/index.tsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import { _cs } from '@togglecorp/fujs';
+
+import MobilePreview from '#components/MobilePreview';
+import { IconKey, iconMap } from '#utils/common';
+
+import {
+ ImageType,
+ colorKeyToColorMap,
+ PartialCustomOptionsType,
+} from '../../utils';
+import styles from './styles.css';
+
+interface Props {
+ className?: string;
+ image?: ImageType;
+ previewPopUp?: {
+ title?: string;
+ description?: string;
+ icon?: IconKey;
+ }
+ customOptions: PartialCustomOptionsType | undefined;
+ lookFor: string | undefined;
+}
+
+export default function ValidateImagePreview(props: Props) {
+ const {
+ className,
+ previewPopUp,
+ customOptions,
+ lookFor,
+ image,
+ } = props;
+
+ const Comp = previewPopUp?.icon ? iconMap[previewPopUp.icon] : undefined;
+
+ return (
+ }
+ popupTitle={previewPopUp?.title || '{title}'}
+ popupDescription={previewPopUp?.description || '{description}'}
+ >
+
+
+ {customOptions?.map((option) => {
+ const Icon = option.icon
+ ? iconMap[option.icon]
+ : iconMap['flag-outline'];
+ return (
+
+
+ {Icon && (
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/ValidateImagePreview/styles.css b/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/ValidateImagePreview/styles.css
new file mode 100644
index 000000000..5f708d4a5
--- /dev/null
+++ b/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/ValidateImagePreview/styles.css
@@ -0,0 +1,36 @@
+.validate-image-preview {
+ .content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-large);
+
+ .image-preview {
+ position: relative;
+ width: 100%;
+ height: var(--height-mobile-preview-validate-image-content);
+ }
+
+ .options {
+ display: grid;
+ flex-grow: 1;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-gap: var(--spacing-large);
+
+ .option-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .option {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ width: 2.5rem;
+ height: 2.5rem;
+ font-size: var(--font-size-extra-large);
+ }
+ }
+ }
+ }
+}
diff --git a/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/index.tsx b/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/index.tsx
index 607434590..2ba9d05fe 100644
--- a/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/index.tsx
+++ b/manager-dashboard/app/views/NewTutorial/ScenarioPageInput/index.tsx
@@ -17,6 +17,8 @@ import {
PROJECT_TYPE_FOOTPRINT,
PROJECT_TYPE_CHANGE_DETECTION,
PROJECT_TYPE_COMPLETENESS,
+ PROJECT_TYPE_STREET,
+ PROJECT_TYPE_VALIDATE_IMAGE,
} from '#utils/common';
import TextInput from '#components/TextInput';
import Heading from '#components/Heading';
@@ -24,6 +26,7 @@ import SelectInput from '#components/SelectInput';
import SegmentInput from '#components/SegmentInput';
import {
+ ImageType,
TutorialTasksGeoJSON,
FootprintGeoJSON,
BuildAreaGeoJSON,
@@ -33,6 +36,7 @@ import {
import BuildAreaGeoJsonPreview from './BuildAreaGeoJsonPreview';
import FootprintGeoJsonPreview from './FootprintGeoJsonPreview';
import ChangeDetectionGeoJsonPreview from './ChangeDetectionGeoJsonPreview';
+import ValidateImagePreview from './ValidateImagePreview';
import styles from './styles.css';
type ScenarioType = {
@@ -77,6 +81,7 @@ interface Props {
index: number,
error: Error | undefined;
geoJson: TutorialTasksGeoJSON | undefined;
+ images: ImageType[] | undefined;
projectType: ProjectType | undefined;
urlA: string | undefined;
urlB: string | undefined;
@@ -93,6 +98,7 @@ export default function ScenarioPageInput(props: Props) {
index,
error: riskyError,
geoJson: geoJsonFromProps,
+ images,
urlA,
projectType,
urlB,
@@ -170,7 +176,21 @@ export default function ScenarioPageInput(props: Props) {
[geoJsonFromProps, scenarioId],
);
- const activeSegmentInput: ScenarioSegmentType['value'] = projectType && projectType !== PROJECT_TYPE_FOOTPRINT
+ const image = React.useMemo(
+ () => {
+ if (!images) {
+ return undefined;
+ }
+ return images.find((img) => img.screen === scenarioId);
+ },
+ [images, scenarioId],
+ );
+
+ const activeSegmentInput: ScenarioSegmentType['value'] = (
+ projectType
+ && projectType !== PROJECT_TYPE_FOOTPRINT
+ && projectType !== PROJECT_TYPE_VALIDATE_IMAGE
+ )
? activeSegmentInputFromState
: 'instructions';
@@ -213,7 +233,11 @@ export default function ScenarioPageInput(props: Props) {
disabled={disabled}
/>