diff --git a/app/components/domain/CustomOptionsPreview/index.tsx b/app/components/domain/CustomOptionsPreview/index.tsx index 6ad3084..3a275b3 100644 --- a/app/components/domain/CustomOptionsPreview/index.tsx +++ b/app/components/domain/CustomOptionsPreview/index.tsx @@ -1,4 +1,9 @@ -import { _cs } from '@togglecorp/fujs'; +import { + _cs, + compareNumber, + isDefined, + isFalsyString, +} from '@togglecorp/fujs'; import { type PartialCustomOptionInputFields } from '#components/domain/CustomOptionInput/schema'; import Icon from '#components/domain/Icon'; @@ -9,7 +14,7 @@ import styles from './styles.module.css'; interface Props { className?: string; value: PartialCustomOptionInputFields[] | undefined; - variant?: 'action' | 'info'; + variant?: 'action' | 'info' | 'tile'; } function CustomOptionPreview(props: Props) { @@ -19,6 +24,12 @@ function CustomOptionPreview(props: Props) { variant = 'action', } = props; + // For the tile variant (Locate Objects), options are displayed ordered by + // their value; other variants preserve the order they are given. + const orderedValue = variant === 'tile' && isDefined(value) + ? [...value].sort((a, b) => compareNumber(a.value, b.value)) + : value; + // TODO(frozenhelium): implement sub-option return (
- {value?.map((option) => ( -
+ {orderedValue?.map((option) => { + const isTransparent = isFalsyString(option.iconColor) + || option.iconColor === 'transparent'; + + return (
- -
- -
- {option.title} -
- {variant === 'info' && ( -
- {option.description} + {variant === 'tile' ? ( +
+ ) : ( +
+
)} - -
- ))} + +
+ {option.title} +
+ {variant === 'info' && ( +
+ {option.description} +
+ )} +
+
+ ); + })}
); } diff --git a/app/components/domain/CustomOptionsPreview/styles.module.css b/app/components/domain/CustomOptionsPreview/styles.module.css index 1158bd0..238c340 100644 --- a/app/components/domain/CustomOptionsPreview/styles.module.css +++ b/app/components/domain/CustomOptionsPreview/styles.module.css @@ -50,4 +50,35 @@ } } } + + &.tile-variant { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-lg); + padding: var(--spacing-md); + + .option { + align-items: center; + flex-direction: column; + gap: var(--spacing-xs); + text-align: center; + + .tile { + flex-shrink: 0; + border: var(--width-separator-thin) solid var(--color-separator); + border-radius: 0.4rem; + width: 2.8rem; + height: 2.8rem; + + &.transparent { + border-style: dashed; + background-color: var(--color-background); + } + } + + .label { + font-size: var(--font-size-sm); + } + } + } } diff --git a/app/components/domain/ProjectSpecificDetails/LocateObject/index.tsx b/app/components/domain/ProjectSpecificDetails/LocateObject/index.tsx index 9c90605..aff1b1d 100644 --- a/app/components/domain/ProjectSpecificDetails/LocateObject/index.tsx +++ b/app/components/domain/ProjectSpecificDetails/LocateObject/index.tsx @@ -2,8 +2,10 @@ import { useState } from 'react'; import { isNotDefined } from '@togglecorp/fujs'; import { removeNull } from '@togglecorp/toggle-form'; +import Container from '#components/Container'; import DefaultMapContainer from '#components/DefaultMapContainer'; import BaseMap from '#components/domain/BaseMap'; +import CustomOptionPreview from '#components/domain/CustomOptionsPreview'; import GeoJsonAssetMapSource from '#components/domain/GeoJsonAssetMapSource'; import MapZoomViewSelectInput, { MapZoomViewType } from '#components/domain/MapZoomViewSelectInput'; import RasterTileServerOutput from '#components/domain/RasterTileServerOutput'; @@ -65,6 +67,15 @@ function LocateObjectDetails(props: Props) { label="Sub grid size" value={data.subGridSize} /> + + + ); } diff --git a/app/views/EditProject/UpdateProjectForm/LocateObjectProjectSpecifics/index.tsx b/app/views/EditProject/UpdateProjectForm/LocateObjectProjectSpecifics/index.tsx index 8518692..b2644d1 100644 --- a/app/views/EditProject/UpdateProjectForm/LocateObjectProjectSpecifics/index.tsx +++ b/app/views/EditProject/UpdateProjectForm/LocateObjectProjectSpecifics/index.tsx @@ -1,5 +1,11 @@ -import { useContext } from 'react'; -import { isDefined } from '@togglecorp/fujs'; +import { + useCallback, + useContext, +} from 'react'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; import { EntriesAsList, getErrorObject, @@ -7,9 +13,13 @@ import { ObjectError, useFormObject, } from '@togglecorp/toggle-form'; +import { ulid } from 'ulid'; +import Checkbox from '#components/Checkbox'; import Container from '#components/Container'; import AssetInput from '#components/domain/AssetInput'; +import { type PartialCustomOptionInputFields } from '#components/domain/CustomOptionInput/schema'; +import CustomOptionPreview from '#components/domain/CustomOptionsPreview'; import RasterTileServerInput from '#components/domain/RasterTileServerInput'; import { defaultRasterTileServerInputValue, @@ -20,7 +30,10 @@ 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 { + ProjectAssetInputTypeEnum, + type ProjectDetailsQuery, +} from '#generated/types/graphql'; import { keySelector, labelSelector, @@ -28,11 +41,19 @@ import { import { type PartialLocateObjectSpecificFields } from './schema'; +type DefaultCustomOption = ProjectDetailsQuery['defaultLocateObjectCustomOptions'][number]; + +// NOTE: In the LOCATE custom options, value 2 corresponds to the "Multiple +// Features" option, which managers can optionally exclude to keep only the +// "Single Feature" and "No" options. +const MULTIPLE_FEATURES_VALUE = 2; + interface Props { projectId: string; value: PartialLocateObjectSpecificFields | undefined | null; error: LeafError | ObjectError; setFieldValue: (...entries: EntriesAsList) => void; + defaultMultipleFeaturesOption: DefaultCustomOption | undefined; disabled?: boolean; } @@ -42,6 +63,7 @@ function LocateObjectProjectSpecifics(props: Props) { value, error: formError, setFieldValue, + defaultMultipleFeaturesOption, disabled, } = props; @@ -55,6 +77,40 @@ function LocateObjectProjectSpecifics(props: Props) { defaultRasterTileServerInputValue, ); + const includeMultipleFeatures = value?.customOptions?.some( + (option) => option.value === MULTIPLE_FEATURES_VALUE, + ) ?? false; + + const handleMultipleFeaturesToggle = useCallback((include: boolean) => { + setFieldValue( + (oldOptions: PartialCustomOptionInputFields[] | undefined) => { + const baseOptions = oldOptions ?? []; + + if (!include) { + return baseOptions.filter( + (option) => option.value !== MULTIPLE_FEATURES_VALUE, + ); + } + + const alreadyPresent = baseOptions.some( + (option) => option.value === MULTIPLE_FEATURES_VALUE, + ); + if (alreadyPresent || isNotDefined(defaultMultipleFeaturesOption)) { + return baseOptions; + } + + return [ + ...baseOptions, + { + clientId: ulid(), + ...defaultMultipleFeaturesOption, + }, + ]; + }, + 'customOptions' as const, + ); + }, [setFieldValue, defaultMultipleFeaturesOption]); + return ( <> + + + + option.value === 2, + )} disabled={projectTypeSpecificInputsDisabled || readOnly} /> )}