diff --git a/app/components/domain/CompareScenarioPreview/index.tsx b/app/components/domain/CompareScenarioPreview/index.tsx index bfad1c7c..a7458924 100644 --- a/app/components/domain/CompareScenarioPreview/index.tsx +++ b/app/components/domain/CompareScenarioPreview/index.tsx @@ -67,6 +67,16 @@ function CompareScenarioPreview(props: Props) { return createGeoJsonFromTiles(tiles); }, [scenario]); + const tileServerPropertySafe = useMemo( + () => removeNull(tileServerProperty), + [tileServerProperty], + ); + + const tileServerBPropertySafe = useMemo( + () => removeNull(tileServerBProperty), + [tileServerBProperty], + ); + return (
@@ -91,7 +101,7 @@ function CompareScenarioPreview(props: Props) { tileSize={280} className={styles.mapContainer} geoJson={generatedGeojson} - baseTileServer={removeNull(tileServerBProperty)} + baseTileServer={tileServerBPropertySafe} geoJsonLayerOptions={layerOptions} disablePan /> diff --git a/app/components/domain/FindScenarioPreview/index.tsx b/app/components/domain/FindScenarioPreview/index.tsx index d7675cca..ea06c10a 100644 --- a/app/components/domain/FindScenarioPreview/index.tsx +++ b/app/components/domain/FindScenarioPreview/index.tsx @@ -66,6 +66,11 @@ function FindScenarioPreview(props: Props) { return createGeoJsonFromTiles(tiles); }, [scenario]); + const tileServerPropertySafe = useMemo( + () => removeNull(tileServerProperty), + [tileServerProperty], + ); + return ( diff --git a/app/components/domain/LocateFeaturesScenarioPreview/index.tsx b/app/components/domain/LocateFeaturesScenarioPreview/index.tsx new file mode 100644 index 00000000..372104cb --- /dev/null +++ b/app/components/domain/LocateFeaturesScenarioPreview/index.tsx @@ -0,0 +1,150 @@ +import { useMemo } from 'react'; +import { + _cs, + isNotDefined, +} from '@togglecorp/fujs'; +import { + PartialForm, + removeNull, +} from '@togglecorp/toggle-form'; +import { FillLayerSpecification } from 'maplibre-gl'; + +import GeoJsonPreview from '#components/domain/GeoJsonPreview'; +import Icon from '#components/domain/Icon'; +import { PreviewItem } from '#components/domain/TutorialPreviewScreenSelectInput'; +import ListLayout from '#components/ListLayout'; +import MobilePreview from '#components/MobilePreview'; +import { + ProjectRasterTileServerConfig, + SubGridSizeEnum, + TutorialScenarioPageCreateInput, +} from '#generated/types/graphql'; +import { subgridSizeToValueMap } from '#utils/common'; +import { createSubGridGeoJsonFromTile } from '#utils/geo'; + +import { PartialCustomOptionInputFields } from '../CustomOptionInput/schema'; + +import styles from './styles.module.css'; + +interface Props { + className?: string; + tileServerProperty: ProjectRasterTileServerConfig | undefined; + projectInstruction: string | undefined | null; + scenario: PartialForm | undefined; + preview: PreviewItem | undefined; + subgridSize: SubGridSizeEnum | undefined; + customOptions: PartialCustomOptionInputFields[] | undefined; +} + +function LocateFeaturesScenarioPreview(props: Props) { + const { + className, + scenario, + projectInstruction, + tileServerProperty, + preview, + subgridSize, + customOptions, + } = props; + + // @ts-expect-error potentially wrong typing + const layerOptions: Omit | undefined = useMemo(() => { + if (isNotDefined(customOptions)) { + return undefined; + } + + const colorAndValueMapping = customOptions.map((option) => ([ + option.value!, + option.iconColor!, + ])).flat(); + + return { + type: 'fill', + paint: { + // @ts-expect-error potentially wrong typing + 'fill-color': [ + 'match', + ['get', 'reference'], + ...colorAndValueMapping, + 'transparent', + ], + 'fill-outline-color': '#ffffff', + 'fill-opacity': 0.5, + }, + } satisfies Omit; + }, [customOptions]); + + const references = useMemo(() => ( + scenario?.tasks?.map(({ reference }) => reference) + ), [scenario?.tasks]); + + const firstTask = scenario?.tasks?.[0]; + const [tileX, tileY, tileZ] = useMemo(() => { + if (isNotDefined(firstTask)) { + return []; + } + + const projectTypeSpecifics = firstTask.projectTypeSpecifics?.locate; + + if (isNotDefined(projectTypeSpecifics)) { + return []; + } + + return [ + projectTypeSpecifics.tileX, + projectTypeSpecifics.tileY, + projectTypeSpecifics.tileZ, + ]; + }, [firstTask]); + + const generatedGeojson = useMemo(() => { + if (isNotDefined(tileX) + || isNotDefined(tileY) + || isNotDefined(tileZ) + || isNotDefined(subgridSize) + ) { + return undefined; + } + + return createSubGridGeoJsonFromTile( + tileX, + tileY, + tileZ, + subgridSizeToValueMap[subgridSize], + references, + ); + }, [tileX, tileY, tileZ, references, subgridSize]); + + const tileServerPropertySafe = useMemo( + () => removeNull(tileServerProperty), + [tileServerProperty], + ); + + return ( + + } + popupTitle={preview?.title || '{title}'} + popupDescription={preview?.description || '{description}'} + popupVariant={preview?.popupVariant} + contentClassName={styles.content} + > + + + + ); +} + +export default LocateFeaturesScenarioPreview; diff --git a/app/components/domain/LocateFeaturesScenarioPreview/styles.module.css b/app/components/domain/LocateFeaturesScenarioPreview/styles.module.css new file mode 100644 index 00000000..2d1bd6c9 --- /dev/null +++ b/app/components/domain/LocateFeaturesScenarioPreview/styles.module.css @@ -0,0 +1,17 @@ +.locate-features-scenario-preview { + isolation: isolate; + + .content { + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + padding: 0 var(--spacing-lg); + + .map-container { + z-index: 0; + width: var(--size-tile-validate); + height: var(--size-tile-validate); + } + } +} diff --git a/app/components/domain/ProjectSpecificDetails/LocateFeatures/index.tsx b/app/components/domain/ProjectSpecificDetails/LocateFeatures/index.tsx new file mode 100644 index 00000000..f3253978 --- /dev/null +++ b/app/components/domain/ProjectSpecificDetails/LocateFeatures/index.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react'; +import { isNotDefined } from '@togglecorp/fujs'; +import { removeNull } from '@togglecorp/toggle-form'; + +import DefaultMapContainer from '#components/DefaultMapContainer'; +import BaseMap from '#components/domain/BaseMap'; +import GeoJsonAssetMapSource from '#components/domain/GeoJsonAssetMapSource'; +import MapZoomViewSelectInput, { MapZoomViewType } from '#components/domain/MapZoomViewSelectInput'; +import RasterTileServerOutput from '#components/domain/RasterTileServerOutput'; +import InlineLayout from '#components/InlineLayout'; +import ListLayout from '#components/ListLayout'; +import TextOutput from '#components/TextOutput'; +import ZoomLevelOutput from '#components/ZoomLevelOutput'; +import { ProjectSpecificDetailsQuery } from '#generated/types/graphql'; + +interface Props { + data: ProjectSpecificDetailsQuery['project']['projectTypeSpecifics']; + defaultBounds: GeoJSON.Polygon | undefined | null; +} + +function LocateFeaturesDetails(props: Props) { + const { + data, + defaultBounds, + } = props; + const [zoomView, setZoomView] = useState('aoiBounds'); + + // eslint-disable-next-line no-underscore-dangle + if (isNotDefined(data) || data.__typename !== 'LocateProjectPropertyType') { + return null; + } + + const tileZ = data.zoomLevel; + + return ( + <> + + + + + + + + + + + + + + + + ); +} + +export default LocateFeaturesDetails; diff --git a/app/components/domain/ProjectSpecificDetails/index.tsx b/app/components/domain/ProjectSpecificDetails/index.tsx index 61aa989d..c602015c 100644 --- a/app/components/domain/ProjectSpecificDetails/index.tsx +++ b/app/components/domain/ProjectSpecificDetails/index.tsx @@ -11,6 +11,7 @@ import ProjectTypeOutput from '../ProjectTypeOutput'; import CompareDetails from './CompareDetails'; import CompletenessDetails from './CompletenessDetails'; import FindDetails from './FindDetails'; +import LocateFeaturesDetails from './LocateFeatures'; import StreetDetails from './StreetDetails'; import ValidateDetails from './ValidateDetails'; import ValidateImageDetails from './ValidateImageDetails'; @@ -134,6 +135,13 @@ function ProjectSpecificDetails(props: Props) { data={projectData.project.projectTypeSpecifics} /> )} + {/* eslint-disable-next-line no-underscore-dangle */} + {projectData?.project.projectTypeSpecifics?.__typename === 'LocateProjectPropertyType' && ( + + )} ); } diff --git a/app/components/domain/ProjectTypeIcon/index.tsx b/app/components/domain/ProjectTypeIcon/index.tsx index ed59a9be..3c0f599b 100644 --- a/app/components/domain/ProjectTypeIcon/index.tsx +++ b/app/components/domain/ProjectTypeIcon/index.tsx @@ -1,3 +1,4 @@ +import { PiGridFour } from 'react-icons/pi'; import { _cs } from '@togglecorp/fujs'; import { ProjectTypeEnum } from '#generated/types/graphql'; @@ -119,6 +120,10 @@ function ProjectTypeIcon(props: Props) { ); } + if (type === ProjectTypeEnum.Locate) { + return ; + } + const remainingTypes = type; remainingTypes satisfies never; diff --git a/app/contexts/EnumsContext.ts b/app/contexts/EnumsContext.ts index b46213dd..8c4b80d8 100644 --- a/app/contexts/EnumsContext.ts +++ b/app/contexts/EnumsContext.ts @@ -12,6 +12,7 @@ export interface EnumsContextProps { overlayLayerTypeOptions: AllEnumsQuery['enums']['OverlayLayerTypeEnum'], tutorialStatusOptions: AllEnumsQuery['enums']['TutorialStatusEnum'], firebasePushStatusOptions: AllEnumsQuery['enums']['FirebasePushStatusEnum'], + subGridSizeOptions: AllEnumsQuery['enums']['SubGridSizeEnum'], validateObjectSourceTypeMapping: Record< AllEnumsQuery['enums']['ValidateObjectSourceTypeEnum'][number]['key'], @@ -61,6 +62,7 @@ export const defaultAllEnumsValue: EnumsContextProps = { overlayLayerTypeOptions: [], tutorialStatusOptions: [], firebasePushStatusOptions: [], + subGridSizeOptions: [], validateObjectSourceTypeMapping: undefined, validateImageSourceTypeMapping: undefined, diff --git a/app/utils/common.ts b/app/utils/common.ts index c6aa4284..5308eae1 100644 --- a/app/utils/common.ts +++ b/app/utils/common.ts @@ -9,6 +9,7 @@ import { import { ProjectTypeEnum, ProjectTypeSpecificInput, + SubGridSizeEnum, } from '#generated/types/graphql'; export const DEFAULT_ALERT_DISMISS_DURATION = 4500; @@ -156,6 +157,7 @@ export const projectTypeToKeyMap: Record = { + [SubGridSizeEnum.Size_2X2]: 1, + [SubGridSizeEnum.Size_4X4]: 2, + [SubGridSizeEnum.Size_8X8]: 3, +}; diff --git a/app/utils/geo.ts b/app/utils/geo.ts index 03113f4a..731e1e25 100644 --- a/app/utils/geo.ts +++ b/app/utils/geo.ts @@ -1,8 +1,10 @@ import { bboxToTile, + getChildren, tileToGeoJSON, } from '@mapbox/tilebelt'; import { + compareNumber, isDefined, isNotDefined, } from '@togglecorp/fujs'; @@ -44,6 +46,54 @@ export function standardizeQuadKey(url: string) { return url.replace('{quad_key}', '{quadkey}'); } +export function createSubGridGeoJsonFromTile( + x: number, + y: number, + z: number, + subgridSize: 1 | 2 | 3, + reference: (number | undefined)[] | undefined, +) { + let childrenTiles = getChildren( + [x, y, z], + ); + + if (subgridSize > 1) { + new Array(subgridSize - 1).keys().forEach(() => { + childrenTiles = childrenTiles.flatMap((tile) => ( + getChildren(tile) + )); + }); + } + + // 1, 2 -> 1.1 1.2 2.1 2.2 + // 3, 4 1.3 1.4 2.3 2.4 + // + // 3.1 3.2 4.1 4.2 + // 3.3 3.4 4.3 4.4 + // For this, list would look like [1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 2,3, ...] + // Ordering it to [1.1, 1.2, 2.1, 2.2, 1.3, 1.4, 2.3, ...] + // Fixing the order of subgrids (since they're created recursively) + childrenTiles.sort( + (a, b) => compareNumber(a[1], b[1]) || compareNumber(a[0], b[0]), + ); + + const geojson: GeoJSON.GeoJSON = { + type: 'FeatureCollection' as const, + features: childrenTiles.map((tile, i) => ({ + type: 'Feature' as const, + geometry: tileToGeoJSON(tile), + properties: { + tile_x: x, + tile_y: y, + tile_z: z, + reference: reference?.[i], + }, + })), + }; + + return geojson; +} + export function createGeoJsonFromTiles( tiles: { tileX: number | undefined, diff --git a/app/utils/query.ts b/app/utils/query.ts index 9a919eeb..53088253 100644 --- a/app/utils/query.ts +++ b/app/utils/query.ts @@ -24,6 +24,8 @@ fragment RasterTileServerPropertyFields on ProjectRasterTileServerConfig { custom { credits url + minZoom + maxZoom } } `; @@ -56,7 +58,7 @@ fragment VectorTileServerPropertyFields on ProjectVectorTileServerConfig { export const PROJECT_TYPE_SPECIFIC_FRAGMENT = gql` ${TILE_SERVER_PROPERTY_FRAGMENT} ${VECTOR_TILE_SERVER_PROPERTY_FRAGMENT} -fragment ProjectTypeSpecificFields on CompareProjectPropertyTypeFindProjectPropertyTypeValidateProjectPropertyTypeValidateImageProjectPropertyTypeCompletenessProjectPropertyTypeStreetProjectPropertyType { +fragment ProjectTypeSpecificFields on CompareProjectPropertyTypeFindProjectPropertyTypeValidateProjectPropertyTypeValidateImageProjectPropertyTypeCompletenessProjectPropertyTypeStreetProjectPropertyTypeLocateProjectPropertyType { ... on CompareProjectPropertyType { __typename aoiGeometry @@ -146,6 +148,20 @@ fragment ProjectTypeSpecificFields on CompareProjectPropertyTypeFindProjectPrope startTime } } + ... on LocateProjectPropertyType { + __typename + aoiGeometry + tileServerProperty { + ...RasterTileServerPropertyFields + } + zoomLevel + subGridSize + exportMetaKey + exportMetaValue + customOptions { + ...ProjectCustomOptionFields + } + } } `; @@ -288,6 +304,7 @@ fragment TutorialDetailFields on TutorialType { clientId reference scenarioId + taskPartitionIndex projectTypeSpecifics { ... on FindTutorialTaskPropertyType { __typename @@ -333,6 +350,12 @@ fragment TutorialDetailFields on TutorialType { mapillaryImageId geometry } + ... on LocateTutorialTaskPropertyType { + __typename + tileX + tileY + tileZ + } } } } diff --git a/app/views/EditProject/UpdateProjectForm/LocateFeaturesProjectSpecifics/index.tsx b/app/views/EditProject/UpdateProjectForm/LocateFeaturesProjectSpecifics/index.tsx new file mode 100644 index 00000000..febe0e68 --- /dev/null +++ b/app/views/EditProject/UpdateProjectForm/LocateFeaturesProjectSpecifics/index.tsx @@ -0,0 +1,128 @@ +import { useContext } from 'react'; +import { isDefined } from '@togglecorp/fujs'; +import { + EntriesAsList, + getErrorObject, + LeafError, + ObjectError, + useFormObject, +} from '@togglecorp/toggle-form'; + +import Container from '#components/Container'; +import AssetInput from '#components/domain/AssetInput'; +import RasterTileServerInput from '#components/domain/RasterTileServerInput'; +import { + defaultRasterTileServerInputValue, + type PartialRasterTileServerInputFields, +} from '#components/domain/RasterTileServerInput/schema'; +import ListLayout from '#components/ListLayout'; +import RadioInput from '#components/RadioInput'; +import TextInput from '#components/TextInput'; +import ZoomLevelSelectInput from '#components/ZoomLevelSelectInput'; +import EnumsContext from '#contexts/EnumsContext'; +import { ProjectAssetInputTypeEnum } from '#generated/types/graphql'; +import { + keySelector, + labelSelector, +} from '#utils/common'; + +import { type PartialLocateFeaturesSpecificFields } from './schema'; + +interface Props { + projectId: string; + value: PartialLocateFeaturesSpecificFields | undefined | null; + error: LeafError | ObjectError; + setFieldValue: (...entries: EntriesAsList) => void; + disabled?: boolean; +} + +function LocateFeaturesProjectSpecifics(props: Props) { + const { + projectId, + value, + error: formError, + setFieldValue, + disabled, + } = props; + + const { subGridSizeOptions } = useContext(EnumsContext); + + const error = getErrorObject(formError); + + const setTileServerInputFieldValue = useFormObject<'tileServerProperty', PartialRasterTileServerInputFields>( + 'tileServerProperty' as const, + setFieldValue, + defaultRasterTileServerInputValue, + ); + + return ( + <> + + + + + + + + + + + + + + ); +} + +export default LocateFeaturesProjectSpecifics; diff --git a/app/views/EditProject/UpdateProjectForm/LocateFeaturesProjectSpecifics/schema.ts b/app/views/EditProject/UpdateProjectForm/LocateFeaturesProjectSpecifics/schema.ts new file mode 100644 index 00000000..a2914343 --- /dev/null +++ b/app/views/EditProject/UpdateProjectForm/LocateFeaturesProjectSpecifics/schema.ts @@ -0,0 +1,57 @@ +import { + greaterThanOrEqualToCondition, + lessThanOrEqualToCondition, + ObjectSchema, + PartialForm, + requiredStringCondition, +} from '@togglecorp/toggle-form'; + +import customOptionSchema from '#components/domain/CustomOptionInput/schema'; +import rasterTileServerFormSchema, { defaultRasterTileServerInputValue } from '#components/domain/RasterTileServerInput/schema'; +import { LocateProjectPropertyInput } from '#generated/types/graphql'; +import { DeepNonNullable } from '#utils/types'; + +import { + type PartialProjectUpdateInput, + type UpdateProjectContext, +} from '../schema'; + +export type PartialLocateFeaturesSpecificFields = PartialForm< + DeepNonNullable, + 'clientId' +>; +type LocateFeaturesSpecificFormSchema = ObjectSchema< + PartialLocateFeaturesSpecificFields, + PartialProjectUpdateInput, + UpdateProjectContext +>; + +export const defaultLocateFeaturesSpecificFormValue: PartialLocateFeaturesSpecificFields = { + tileServerProperty: defaultRasterTileServerInputValue, + zoomLevel: 18, +}; + +const locateFeaturesSpecificFormSchema: LocateFeaturesSpecificFormSchema = { + fields: (): ReturnType => ({ + zoomLevel: { + required: true, + validations: [greaterThanOrEqualToCondition(14), lessThanOrEqualToCondition(22)], + }, + tileServerProperty: rasterTileServerFormSchema, + aoiGeometry: { + required: true, + requiredValidation: requiredStringCondition, + }, + subGridSize: { + required: true, + }, + exportMetaKey: {}, + exportMetaValue: {}, + customOptions: { + keySelector: (value) => value.clientId, + member: () => customOptionSchema, + }, + }), +}; + +export default locateFeaturesSpecificFormSchema; diff --git a/app/views/EditProject/UpdateProjectForm/index.tsx b/app/views/EditProject/UpdateProjectForm/index.tsx index c3274323..776fd36f 100644 --- a/app/views/EditProject/UpdateProjectForm/index.tsx +++ b/app/views/EditProject/UpdateProjectForm/index.tsx @@ -59,6 +59,11 @@ import { defaultFindSpecificFormValue, PartialFindSpecificFields, } from './FindProjectSpecifics/schema'; +import LocateFeaturesProjectSpecifics from './LocateFeaturesProjectSpecifics/index.tsx'; +import { + defaultLocateFeaturesSpecificFormValue, + PartialLocateFeaturesSpecificFields, +} from './LocateFeaturesProjectSpecifics/schema.ts'; import StreetProjectSpecifics from './StreetProjectSpecifics/index.tsx'; import { defaultStreetSpecificFormValue, @@ -165,13 +170,23 @@ function UpdateProjectForm(props: Props) { if (projectData.project.projectType === ProjectTypeEnum.Street) { return { ...defaultStreetSpecificFormValue, - // FIXME: use custom options from street project - customOptions: projectData.defaultValidateCustomOptions.map((customOption) => ({ + customOptions: projectData.defaultStreetCustomOptions.map((customOption) => ({ clientId: ulid(), ...customOption, })), }; } + if (projectData.project.projectType === ProjectTypeEnum.Locate) { + return { + ...defaultLocateFeaturesSpecificFormValue, + customOptions: projectData.defaultLocateFeaturesCustomOptions.map( + (customOption) => ({ + clientId: ulid(), + ...customOption, + }), + ), + }; + } projectData.project.projectType satisfies never; @@ -385,7 +400,13 @@ function UpdateProjectForm(props: Props) { const setStreetProjectSpecificsFieldValue = useFormObject<'street', PartialStreetSpecificFields>( 'street', setProjectSpecificFieldValue, - defaultValidateSpecificFormValue, + defaultStreetSpecificFormValue, + ); + + const setLocateFeaturesProjectSpecificsFieldValue = useFormObject<'locate', PartialLocateFeaturesSpecificFields>( + 'locate', + setProjectSpecificFieldValue, + defaultLocateFeaturesSpecificFormValue, ); const pending = updateProjectPending; @@ -416,6 +437,8 @@ function UpdateProjectForm(props: Props) { ?.validateImage as PartialValidateSpecificFields | undefined; const streetProjectTypeSpecifics = value.projectTypeSpecifics ?.street as PartialStreetSpecificFields | undefined; + const locateFeaturesProjectTypeSpecifics = value.projectTypeSpecifics + ?.locate as PartialLocateFeaturesSpecificFields | undefined; return ( )} + {projectContext.projectType === ProjectTypeEnum.Locate && ( + + )} ); diff --git a/app/views/EditProject/UpdateProjectForm/schema.ts b/app/views/EditProject/UpdateProjectForm/schema.ts index 1004f09c..730559e4 100644 --- a/app/views/EditProject/UpdateProjectForm/schema.ts +++ b/app/views/EditProject/UpdateProjectForm/schema.ts @@ -1,3 +1,4 @@ +import { isNotDefined } from '@togglecorp/fujs'; import { ObjectSchema, PartialForm, @@ -15,6 +16,7 @@ import { DeepNonNullable } from '#utils/types'; import compareSpecificFormSchema from './CompareProjectSpecifics/schema'; import completenessSpecificFormSchema from './CompletenessProjectSpecifics/schema'; import findSpecificFormSchema from './FindProjectSpecifics/schema'; +import locateFeaturesSpecificFormSchema from './LocateFeaturesProjectSpecifics/schema.ts'; import streetSpecificFormSchema from './StreetProjectSpecifics/schema.ts'; import validateImageSpecificFormSchema from './ValidateImageProjectSpecifics/schema.ts'; import validateSpecificFormSchema from './ValidateProjectSpecifics/schema'; @@ -76,82 +78,77 @@ const projectUpdateFormSchema: ProjectUpdateFormSchema = { image: {}, projectTypeSpecifics: { fields: (): ProjectTypeSpecificFormFields => { - if (context?.projectType === ProjectTypeEnum.Find) { + const defaultValue = { + street: { forceValue: undefinedValue }, + find: { forceValue: undefinedValue }, + compare: { forceValue: undefinedValue }, + completeness: { forceValue: undefinedValue }, + validate: { forceValue: undefinedValue }, + validateImage: { forceValue: undefinedValue }, + } satisfies ProjectTypeSpecificFormFields; + + if (isNotDefined(context)) { + return defaultValue; + } + + const { projectType } = context; + + if (isNotDefined(projectType)) { + return defaultValue; + } + + if (projectType === ProjectTypeEnum.Find) { return { - street: { forceValue: undefinedValue }, + ...defaultValue, find: findSpecificFormSchema, - compare: { forceValue: undefinedValue }, - completeness: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, }; } - if (context?.projectType === ProjectTypeEnum.Compare) { + if (projectType === ProjectTypeEnum.Compare) { return { - street: { forceValue: undefinedValue }, + ...defaultValue, compare: compareSpecificFormSchema, - find: { forceValue: undefinedValue }, - completeness: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, }; } - if (context?.projectType === ProjectTypeEnum.Completeness) { + if (projectType === ProjectTypeEnum.Completeness) { return { - street: { forceValue: undefinedValue }, + ...defaultValue, completeness: completenessSpecificFormSchema, - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, }; } - if (context?.projectType === ProjectTypeEnum.Validate) { + if (projectType === ProjectTypeEnum.Validate) { return { - street: { forceValue: undefinedValue }, + ...defaultValue, validate: validateSpecificFormSchema, - completeness: { forceValue: undefinedValue }, - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, }; } - if (context?.projectType === ProjectTypeEnum.ValidateImage) { + if (projectType === ProjectTypeEnum.ValidateImage) { return { - street: { forceValue: undefinedValue }, + ...defaultValue, validateImage: validateImageSpecificFormSchema, - completeness: { forceValue: undefinedValue }, - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, }; } - if (context?.projectType === ProjectTypeEnum.Street) { + if (projectType === ProjectTypeEnum.Street) { return { + ...defaultValue, street: streetSpecificFormSchema, - validateImage: { forceValue: undefinedValue }, - completeness: { forceValue: undefinedValue }, - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, }; } - // context?.projectType satisfies never; + if (projectType === ProjectTypeEnum.Locate) { + return { + ...defaultValue, + locate: locateFeaturesSpecificFormSchema, + }; + } - return { - street: { forceValue: undefinedValue }, - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - completeness: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, - }; + projectType satisfies never; + + return defaultValue; }, }, }), diff --git a/app/views/EditProject/query.ts b/app/views/EditProject/query.ts index 4165fda6..a38f5002 100644 --- a/app/views/EditProject/query.ts +++ b/app/views/EditProject/query.ts @@ -23,5 +23,19 @@ query ProjectDetails($id: ID!) { title value } + defaultStreetCustomOptions: defaultCustomOptions(projectType: STREET) { + description + icon + iconColor + title + value + } + defaultLocateFeaturesCustomOptions: defaultCustomOptions(projectType: LOCATE) { + description + icon + iconColor + title + value + } } `; diff --git a/app/views/EditTutorial/ScenarioPageInput/TaskInput/ComparePropertyInput/index.tsx b/app/views/EditTutorial/ScenarioPageInput/TaskInput/ComparePropertyInput/index.tsx index 1b6546ab..5f200562 100644 --- a/app/views/EditTutorial/ScenarioPageInput/TaskInput/ComparePropertyInput/index.tsx +++ b/app/views/EditTutorial/ScenarioPageInput/TaskInput/ComparePropertyInput/index.tsx @@ -35,6 +35,7 @@ function ComparePropertyInput(props: Props) { layout="grid" numPreferredGridColumns={3} minGridColumnSize="6rem" + spacing="sm" > ) => void; + error: LeafError | ObjectError; + disabled?: boolean; +} + +function LocateFeaturesPropertyInput(props: Props) { + const { + className, + value, + setFieldValue, + error: formError, + disabled, + } = props; + + const error = getErrorObject(formError); + + return ( + + + + + + ); +} + +export default LocateFeaturesPropertyInput; diff --git a/app/views/EditTutorial/ScenarioPageInput/TaskInput/LocateFeaturesPropertyInput/schema.ts b/app/views/EditTutorial/ScenarioPageInput/TaskInput/LocateFeaturesPropertyInput/schema.ts new file mode 100644 index 00000000..a58e7e21 --- /dev/null +++ b/app/views/EditTutorial/ScenarioPageInput/TaskInput/LocateFeaturesPropertyInput/schema.ts @@ -0,0 +1,24 @@ +import { + ObjectSchema, + PartialForm, +} from '@togglecorp/toggle-form'; + +import { LocateTutorialTaskPropertyInput } from '#generated/types/graphql'; +import { DeepNonNullable } from '#utils/types'; + +export type LocateFeaturesPropertyInputFields = DeepNonNullable; +export type PartialLocateFeaturesPropertyInputFields = PartialForm< + LocateFeaturesPropertyInputFields +>; + +type TaskSchema = ObjectSchema; + +const locateFeaturesPropertyInputSchema: TaskSchema = { + fields: (): ReturnType => ({ + tileX: {}, + tileY: {}, + tileZ: {}, + }), +}; + +export default locateFeaturesPropertyInputSchema; diff --git a/app/views/EditTutorial/ScenarioPageInput/TaskInput/ValidateImagePropertyInput/index.tsx b/app/views/EditTutorial/ScenarioPageInput/TaskInput/ValidateImagePropertyInput/index.tsx index e1fe054c..78f421ee 100644 --- a/app/views/EditTutorial/ScenarioPageInput/TaskInput/ValidateImagePropertyInput/index.tsx +++ b/app/views/EditTutorial/ScenarioPageInput/TaskInput/ValidateImagePropertyInput/index.tsx @@ -38,6 +38,7 @@ function ValidateImagePropertyInput(props: Props) { layout="grid" numPreferredGridColumns={2} minGridColumnSize="6rem" + spacing="sm" > - + ( + 'locate' as const, + setProjectSpecificFieldValue, + {}, + ); + + const referenceInput = useMemo( + () => { + const specifics = projectData?.projectTypeSpecifics; + + if (isNotDefined(specifics)) { + return null; + } + + // eslint-disable-next-line no-underscore-dangle + if (specifics.__typename === 'ValidateProjectPropertyType' + // eslint-disable-next-line no-underscore-dangle + || specifics.__typename === 'ValidateImageProjectPropertyType' + // eslint-disable-next-line no-underscore-dangle + || specifics.__typename === 'StreetProjectPropertyType' + // eslint-disable-next-line no-underscore-dangle + || specifics.__typename === 'LocateProjectPropertyType' + ) { + return ( + + ); + } + + // eslint-disable-next-line no-underscore-dangle + if (specifics.__typename === 'FindProjectPropertyType' + // eslint-disable-next-line no-underscore-dangle + || specifics.__typename === 'CompareProjectPropertyType' + // eslint-disable-next-line no-underscore-dangle + || specifics.__typename === 'CompletenessProjectPropertyType' + ) { + return ( + + ); + } + + specifics satisfies never; + + return null; + }, + [ + disabled, + error?.reference, + projectData?.projectTypeSpecifics, + setFieldValue, + value.reference, + ], + ); + return (
-
- {`#${index + 1}`} +
+ {`${index + 1}.`}
- {/* eslint-disable-next-line no-underscore-dangle */} - {(projectData?.projectTypeSpecifics?.__typename === 'ValidateProjectPropertyType' - // eslint-disable-next-line no-underscore-dangle - || projectData?.projectTypeSpecifics?.__typename === 'ValidateImageProjectPropertyType') - ? ( - - ) : ( - - )} + {referenceInput} {projectData?.projectType === ProjectTypeEnum.Find && ( )} + {projectData?.projectType === ProjectTypeEnum.Locate && ( + + )}
); diff --git a/app/views/EditTutorial/ScenarioPageInput/TaskInput/schema.ts b/app/views/EditTutorial/ScenarioPageInput/TaskInput/schema.ts index da7dc5cb..45380459 100644 --- a/app/views/EditTutorial/ScenarioPageInput/TaskInput/schema.ts +++ b/app/views/EditTutorial/ScenarioPageInput/TaskInput/schema.ts @@ -19,6 +19,7 @@ import { import comparePropertyInputSchema from './ComparePropertyInput/schema'; import completenessPropertyInputSchema from './CompletenessPropertyInput/schema'; import findPropertyInputSchema from './FindPropertyInput/schema'; +import locateFeaturesPropertyInputSchema from './LocateFeaturesPropertyInput/schema'; import streetPropertyInputSchema from './StreetPropertyInput/schema'; import validateImagePropertyInputSchema from './ValidateImagePropertyInput/schema'; import validatePropertyInputSchema from './ValidatePropertyInput/schema'; @@ -39,101 +40,83 @@ type ProjectTypeSpecificsSchema = ObjectSchema< TutorialFormContext >; +type ProjectTypeSpecificFields = ReturnType; + const taskSchema: TaskSchema = { fields: (_, __, context): ReturnType => ({ clientId: {}, reference: {}, + taskPartitionIndex: {}, projectTypeSpecifics: { - fields: (): ReturnType => { + fields: (): ProjectTypeSpecificFields => { const projectType = context?.projectType; + const defaultSchema = { + find: { forceValue: undefinedValue }, + compare: { forceValue: undefinedValue }, + completeness: { forceValue: undefinedValue }, + validate: { forceValue: undefinedValue }, + validateImage: { forceValue: undefinedValue }, + street: { forceValue: undefinedValue }, + locate: { forceValue: undefinedValue }, + } satisfies ProjectTypeSpecificFields; + if (isNotDefined(projectType)) { - return { - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - completeness: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, - street: { forceValue: undefinedValue }, - }; + return defaultSchema; } if (projectType === ProjectTypeEnum.Find) { return { + ...defaultSchema, find: findPropertyInputSchema, - completeness: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, - street: { forceValue: undefinedValue }, }; } if (projectType === ProjectTypeEnum.Compare) { return { + ...defaultSchema, compare: comparePropertyInputSchema, - find: { forceValue: undefinedValue }, - completeness: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, - street: { forceValue: undefinedValue }, }; } if (projectType === ProjectTypeEnum.Completeness) { return { + ...defaultSchema, completeness: completenessPropertyInputSchema, - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, - street: { forceValue: undefinedValue }, }; } if (projectType === ProjectTypeEnum.Validate) { return { + ...defaultSchema, validate: validatePropertyInputSchema, - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - completeness: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, - street: { forceValue: undefinedValue }, }; } if (projectType === ProjectTypeEnum.ValidateImage) { return { + ...defaultSchema, validateImage: validateImagePropertyInputSchema, - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - completeness: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - street: { forceValue: undefinedValue }, }; } if (projectType === ProjectTypeEnum.Street) { return { + ...defaultSchema, street: streetPropertyInputSchema, - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - completeness: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, + }; + } + + if (projectType === ProjectTypeEnum.Locate) { + return { + ...defaultSchema, + locate: locateFeaturesPropertyInputSchema, }; } projectType satisfies never; - return { - find: { forceValue: undefinedValue }, - compare: { forceValue: undefinedValue }, - completeness: { forceValue: undefinedValue }, - validate: { forceValue: undefinedValue }, - validateImage: { forceValue: undefinedValue }, - street: { forceValue: undefinedValue }, - }; + return defaultSchema; }, }, }), diff --git a/app/views/EditTutorial/ScenarioPageInput/TaskInput/styles.module.css b/app/views/EditTutorial/ScenarioPageInput/TaskInput/styles.module.css index e789cc89..71eda03d 100644 --- a/app/views/EditTutorial/ScenarioPageInput/TaskInput/styles.module.css +++ b/app/views/EditTutorial/ScenarioPageInput/TaskInput/styles.module.css @@ -3,9 +3,14 @@ .content { display: grid; - grid-template-columns: max-content 9rem auto; - gap: var(--spacing-md); + grid-template-columns: max-content 12rem auto; + gap: var(--spacing-sm); align-items: baseline; + + .sn { + width: 1rem; + text-align: end; + } } @container (max-width: 30rem) { diff --git a/app/views/EditTutorial/ScenarioPageInput/index.tsx b/app/views/EditTutorial/ScenarioPageInput/index.tsx index 052fd1d1..fe1d626a 100644 --- a/app/views/EditTutorial/ScenarioPageInput/index.tsx +++ b/app/views/EditTutorial/ScenarioPageInput/index.tsx @@ -26,6 +26,7 @@ import CompareScenarioPreview from '#components/domain/CompareScenarioPreview'; import CompletenessScenarioPreview from '#components/domain/CompletenessScenarioPreview'; import FindScenarioPreview from '#components/domain/FindScenarioPreview'; import IconSelectInput from '#components/domain/IconSelectInput'; +import LocateFeaturesScenarioPreview from '#components/domain/LocateFeaturesScenarioPreview'; import StreetScenarioPreview from '#components/domain/StreetScenarioPreview'; import TutorialPreviewScreenSelectInput, { PreviewItem, @@ -146,7 +147,10 @@ function ScenarioPageInput(props: Props) { fullWidth /> )} - + - {value.tasks?.map((task, taskIndex) => ( - - ))} + + {value.tasks?.map((task, taskIndex) => ( + + ))} + )} @@ -347,6 +356,17 @@ function ScenarioPageInput(props: Props) { preview={preview} /> )} + {/* eslint-disable-next-line no-underscore-dangle */} + {projectData?.projectTypeSpecifics?.__typename === 'LocateProjectPropertyType' && ( + + )} feature.properties.screen, + ); + + // eslint-disable-next-line no-underscore-dangle + const subgridSize = projectDetailResponse.project.projectTypeSpecifics?.__typename === 'LocateProjectPropertyType' + ? projectDetailResponse.project.projectTypeSpecifics.subGridSize + : undefined; + + const subGridValue = isDefined(subgridSize) + ? subgridSizeToValueMap[subgridSize] + : 0; + + const numSubGrids = (2 ** subGridValue) ** 2; + + const scenarioPages: PartialScenarioPageInputFields[] = unique( + result.features, + (feature) => feature.properties.screen, + ).toSorted( + (a, b) => compareNumber(a.properties.screen, b.properties.screen), + ).map(({ properties }) => ({ + clientId: ulid(), + scenarioPageNumber: properties.screen, + tasks: featuresByScreen[properties.screen].flatMap((feature) => ( + Array.from(new Array(numSubGrids).keys()).map((index) => ({ + clientId: ulid(), + reference: feature.properties.references[index], + taskPartitionIndex: index, + projectTypeSpecifics: { + locate: { + tileX: feature.properties.tile_x, + tileY: feature.properties.tile_y, + tileZ: feature.properties.tile_z, + } satisfies LocateFeaturesPropertyInputFields, + }, + })) + )), + })); + + setFieldValue(scenarioPages, 'scenarios'); + } } }, [projectDetailResponse, setError, setFieldValue]); diff --git a/app/views/NewProject/ProjectGeneralInputs/index.tsx b/app/views/NewProject/ProjectGeneralInputs/index.tsx index 8111eea4..89dfbbf3 100644 --- a/app/views/NewProject/ProjectGeneralInputs/index.tsx +++ b/app/views/NewProject/ProjectGeneralInputs/index.tsx @@ -60,6 +60,7 @@ const hintText: Record< [ProjectTypeEnum.ValidateImage]: 'Enter the description for your project. (markdown syntax is supported)', [ProjectTypeEnum.Completeness]: 'Enter the description for your project. (markdown syntax is supported)', [ProjectTypeEnum.Street]: 'Enter the description for your project. (markdown syntax is supported)', + [ProjectTypeEnum.Locate]: 'Enter the description for your project. (markdown syntax is supported)', }, topic: { [ProjectTypeEnum.Find]: 'Enter the title of your project.', @@ -68,6 +69,7 @@ const hintText: Record< [ProjectTypeEnum.ValidateImage]: 'Enter the title of your project.', [ProjectTypeEnum.Completeness]: 'Enter the title of your project.', [ProjectTypeEnum.Street]: 'Enter the title of your project.', + [ProjectTypeEnum.Locate]: 'Enter the title of your project.', }, projectInstruction: { [ProjectTypeEnum.Find]: 'What should the users look for (e.g. You are looking for: buildings, destroyed buildings, cars, trees, etc.)', @@ -76,6 +78,7 @@ const hintText: Record< [ProjectTypeEnum.ValidateImage]: 'What should the users look to assess? (e.g., You are looking for trees)', [ProjectTypeEnum.Completeness]: undefined, [ProjectTypeEnum.Street]: undefined, + [ProjectTypeEnum.Locate]: undefined, }, lookFor: { [ProjectTypeEnum.Find]: '[This field is used only for legacy app!] What should the users look for? (e.g., buildings, cars, trees)', @@ -84,6 +87,7 @@ const hintText: Record< [ProjectTypeEnum.ValidateImage]: '[This field is used only for legacy app!] What should the users look for? (e.g., buildings, cars, trees)', [ProjectTypeEnum.Completeness]: '[This field is used only for legacy app!] What should the users look for? (e.g., buildings, cars, trees)', [ProjectTypeEnum.Street]: '[This field is used only for legacy app!] What should the users look for? (e.g., buildings, cars, trees)', + [ProjectTypeEnum.Locate]: '[This field is used only for legacy app!] What should the users look for? (e.g., buildings, cars, trees)', }, additionalInfoUrl: { [ProjectTypeEnum.Find]: 'Provide an optional link to a resource with additional information on the project (only visible in the MapSwipe web app)', @@ -92,14 +96,16 @@ const hintText: Record< [ProjectTypeEnum.ValidateImage]: 'Provide an optional link to a resource with additional information on the project (only visible in the MapSwipe web app)', [ProjectTypeEnum.Completeness]: 'Provide an optional link to a resource with additional information on the project (only visible in the MapSwipe web app)', [ProjectTypeEnum.Street]: 'Provide an optional link to a resource with additional information on the project (only visible in the MapSwipe web app)', + [ProjectTypeEnum.Locate]: 'Provide an optional link to a resource with additional information on the project (only visible in the MapSwipe web app)', }, projectNumber: { - [ProjectTypeEnum.Find]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series. ', - [ProjectTypeEnum.Compare]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series. ', - [ProjectTypeEnum.Validate]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series. ', - [ProjectTypeEnum.ValidateImage]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series. ', - [ProjectTypeEnum.Completeness]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series. ', - [ProjectTypeEnum.Street]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series. ', + [ProjectTypeEnum.Find]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series.', + [ProjectTypeEnum.Compare]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series.', + [ProjectTypeEnum.Validate]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series.', + [ProjectTypeEnum.ValidateImage]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series.', + [ProjectTypeEnum.Completeness]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series.', + [ProjectTypeEnum.Street]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series.', + [ProjectTypeEnum.Locate]: 'Is this project part of a bigger campaign with multiple projects? If so, increment this number up by one each time you create a new project in the series.', }, region: { [ProjectTypeEnum.Find]: 'Enter the region/location of your project (eg: City, Country)', @@ -108,6 +114,7 @@ const hintText: Record< [ProjectTypeEnum.ValidateImage]: 'Enter the region/location of your project (eg: City, Country)', [ProjectTypeEnum.Completeness]: 'Enter the region/location of your project (eg: City, Country)', [ProjectTypeEnum.Street]: 'Enter the region/location of your project (eg: City, Country)', + [ProjectTypeEnum.Locate]: 'Enter the region/location of your project (eg: City, Country)', }, requestingOrganization: { [ProjectTypeEnum.Find]: 'Which group, institution or community is requesting this project?', @@ -116,6 +123,7 @@ const hintText: Record< [ProjectTypeEnum.ValidateImage]: 'Which group, institution or community is requesting this project?', [ProjectTypeEnum.Completeness]: 'Which group, institution or community is requesting this project?', [ProjectTypeEnum.Street]: 'Which group, institution or community is requesting this project?', + [ProjectTypeEnum.Locate]: 'Which group, institution or community is requesting this project?', }, team: { [ProjectTypeEnum.Find]: 'Please note that if \'private\', this project will only be visible to the assigned team members. Data results will still be public.', @@ -124,6 +132,7 @@ const hintText: Record< [ProjectTypeEnum.ValidateImage]: 'Please note that if \'private\', this project will only be visible to the assigned team members. Data results will still be public.', [ProjectTypeEnum.Completeness]: 'Please note that if \'private\', this project will only be visible to the assigned team members. Data results will still be public.', [ProjectTypeEnum.Street]: 'Please note that if \'private\', this project will only be visible to the assigned team members. Data results will still be public.', + [ProjectTypeEnum.Locate]: 'Please note that if \'private\', this project will only be visible to the assigned team members. Data results will still be public.', }, }; diff --git a/app/views/NewProject/index.tsx b/app/views/NewProject/index.tsx index d84b397d..391e2099 100644 --- a/app/views/NewProject/index.tsx +++ b/app/views/NewProject/index.tsx @@ -162,6 +162,7 @@ const projectTypeDescriptions: Record = { /> ), + [ProjectTypeEnum.Locate]: 'Locate features', }; function NewProject() { @@ -173,7 +174,19 @@ function NewProject() { createNewProject, ] = useNewProjectMutation(); - const { projectTypeOptions } = useContext(EnumsContext); + const { projectTypeOptions: fullProjectTypeOptions } = useContext(EnumsContext); + + const projectTypeOptions = useMemo(() => { + const { APP_ENVIRONMENT } = import.meta.env; + + if (APP_ENVIRONMENT !== 'PROD') { + return fullProjectTypeOptions; + } + + return fullProjectTypeOptions?.filter( + (option) => option.key !== ProjectTypeEnum.Locate, + ); + }, [fullProjectTypeOptions]); const defaultBaseProjectFormValue = useMemo(() => ({ clientId: ulid(), diff --git a/app/views/RootLayout/index.tsx b/app/views/RootLayout/index.tsx index fcb41b93..5053b6be 100644 --- a/app/views/RootLayout/index.tsx +++ b/app/views/RootLayout/index.tsx @@ -107,6 +107,10 @@ query AllEnums { key label } + SubGridSizeEnum { + key + label + } } } `; @@ -200,6 +204,7 @@ function RootLayout() { overlayLayerTypeOptions: allEnumsResponse?.enums.OverlayLayerTypeEnum ?? [], tutorialStatusOptions: allEnumsResponse?.enums.TutorialStatusEnum ?? [], firebasePushStatusOptions: allEnumsResponse?.enums.FirebasePushStatusEnum ?? [], + subGridSizeOptions: allEnumsResponse?.enums.SubGridSizeEnum ?? [], validateObjectSourceTypeMapping: listToMap( allEnumsResponse?.enums.ValidateObjectSourceTypeEnum, ({ key }) => key, diff --git a/backend b/backend index c0282605..1c83e8f3 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit c028260535afd6a8fe1518d501e7536d1e6ac99e +Subproject commit 1c83e8f39713984cba3a9a19e9f7565d5626608b