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}
/>
)}