Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 55 additions & 26 deletions app/components/domain/CustomOptionsPreview/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand All @@ -19,44 +24,68 @@ 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 (
<div
className={_cs(
styles.customOptionsPreview,
variant === 'action' && styles.actionVariant,
variant === 'info' && styles.infoVariant,
variant === 'tile' && styles.tileVariant,
className,
)}
>
{value?.map((option) => (
<div
key={option.clientId}
className={styles.option}
>
{orderedValue?.map((option) => {
const isTransparent = isFalsyString(option.iconColor)
|| option.iconColor === 'transparent';

return (
<div
className={styles.icon}
style={{
backgroundColor: option.iconColor,
}}
>
<Icon value={option.icon} />
</div>
<ListLayout
layout="block"
spacing="none"
key={option.clientId}
className={styles.option}
>
<div className={styles.label}>
{option.title}
</div>
{variant === 'info' && (
<div className={styles.description}>
{option.description}
{variant === 'tile' ? (
<div
className={_cs(
styles.tile,
isTransparent && styles.transparent,
)}
style={isTransparent
? undefined
: { backgroundColor: option.iconColor }}
/>
) : (
<div
className={styles.icon}
style={{
backgroundColor: option.iconColor,
}}
>
<Icon value={option.icon} />
</div>
)}
</ListLayout>
</div>
))}
<ListLayout
layout="block"
spacing="none"
>
<div className={styles.label}>
{option.title}
</div>
{variant === 'info' && (
<div className={styles.description}>
{option.description}
</div>
)}
</ListLayout>
</div>
);
})}
</div>
);
}
Expand Down
31 changes: 31 additions & 0 deletions app/components/domain/CustomOptionsPreview/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,6 +67,15 @@ function LocateObjectDetails(props: Props) {
label="Sub grid size"
value={data.subGridSize}
/>
<Container
heading="Answer options"
headingLevel={5}
>
<CustomOptionPreview
variant="tile"
value={removeNull(data.customOptions)}
/>
</Container>
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { useContext } from 'react';
import { isDefined } from '@togglecorp/fujs';
import {
useCallback,
useContext,
} from 'react';
import {
isDefined,
isNotDefined,
} from '@togglecorp/fujs';
import {
EntriesAsList,
getErrorObject,
LeafError,
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,
Expand All @@ -20,19 +30,30 @@ 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,
} from '#utils/common';

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<PartialLocateObjectSpecificFields>;
setFieldValue: (...entries: EntriesAsList<PartialLocateObjectSpecificFields>) => void;
defaultMultipleFeaturesOption: DefaultCustomOption | undefined;
disabled?: boolean;
}

Expand All @@ -42,6 +63,7 @@ function LocateObjectProjectSpecifics(props: Props) {
value,
error: formError,
setFieldValue,
defaultMultipleFeaturesOption,
disabled,
} = props;

Expand All @@ -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 (
<>
<AssetInput
Expand Down Expand Up @@ -100,6 +156,26 @@ function LocateObjectProjectSpecifics(props: Props) {
error={error?.subGridSize}
/>
</Container>
<Container
heading="Answer options"
headingLevel={5}
headerDescription="Choose which answer options appear on the tiles. Disable 'Multiple Features' to keep only the first two options."
>
<Checkbox
name="includeMultipleFeatures"
label="Include 'Multiple Features' option"
value={includeMultipleFeatures}
onChange={handleMultipleFeaturesToggle}
// NOTE: re-adding the option needs the canonical default from
// the backend; without it the toggle could not function, so
// we disable it rather than let a click silently no-op.
disabled={disabled || isNotDefined(defaultMultipleFeaturesOption)}
/>
<CustomOptionPreview
variant="tile"
value={value?.customOptions}
/>
</Container>
<Container
heading="Export Meta"
headingLevel={5}
Expand Down
4 changes: 4 additions & 0 deletions app/views/EditProject/UpdateProjectForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,10 @@ function UpdateProjectForm(props: Props) {
value={locateObjectProjectTypeSpecifics}
setFieldValue={setLocateObjectProjectSpecificsFieldValue}
error={getErrorObject(error?.projectTypeSpecifics)?.locate}
defaultMultipleFeaturesOption={projectData
.defaultLocateObjectCustomOptions.find(
(option) => option.value === 2,
)}
disabled={projectTypeSpecificInputsDisabled || readOnly}
/>
)}
Expand Down
Loading