Skip to content

Commit

Permalink
Make it possible to set icons for segment controls through the compon…
Browse files Browse the repository at this point in the history
…ent API (#5845)

# [Try it in the sample
store](https://utopia.fish/p/c144eec3-dog-derby/?branch_name=feature-component-api-segment-control-icons)

<img width="1414" alt="image"
src="https://github.com/concrete-utopia/utopia/assets/16385508/e027a788-79f6-4d56-8b2c-06ae81733c82">

See `layout.utopia.js` for the control descriptor with icons set

## Problem
#5782

## Fix
Interpret the icons coming from the API and use them in the segment
control

### Manual Tests
I hereby swear that:

- [x] I opened a hydrogen project and it loaded
- [x] I could navigate to various routes in Preview mode
  • Loading branch information
bkrmendy authored Jun 7, 2024
1 parent 17596c1 commit 036dd98
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 107 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CSSProperties } from 'react'
import type { ComponentInfo } from './code-file'
import type { PropertyControlIcon } from 'utopia-api/core'
import type { Icons } from '../../uuiui'

interface GenericControlProps<T> {
label?: string
Expand Down Expand Up @@ -50,7 +50,7 @@ export interface BasicControlOption<T> {
export interface BasicControlOptionWithIcon<T> {
value: T
label: string
icon: PropertyControlIcon | null
icon: keyof typeof Icons | null
}

export type BasicControlOptions<T> = AllowedEnumType[] | BasicControlOption<T>[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
import type { Interpolation } from '@emotion/react'
import { jsx } from '@emotion/react'
import React from 'react'
import { FlexRow, colorTheme } from '../../../uuiui'
import { colorTheme } from '../../../uuiui'
import type { IcnProps } from '../../../uuiui'
import { UtopiaTheme } from '../../../uuiui'
import type { DEPRECATEDControlProps, DEPRECATEDGenericControlOptions } from './control'
import { OptionControl } from './option-control'
import Utils from '../../../utils/utils'

export interface OptionChainOption<T> {
value: T
icon?: IcnProps
iconComponent?: React.ReactNode
label?: string
tooltip?: string
forceCallOnSubmitValue?: boolean // Call the onSubmitValue again even when the control is already on that value
Expand Down Expand Up @@ -86,6 +86,7 @@ export const OptionChainControl: React.FunctionComponent<
DEPRECATED_controlOptions={{
tooltip: option.tooltip,
icon: option.icon,
iconComponent: option.iconComponent,
labelInner: option.label,
}}
value={props.value === option.value}
Expand Down
6 changes: 4 additions & 2 deletions editor/src/components/inspector/controls/option-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import type { DEPRECATEDControlProps, DEPRECATEDGenericControlOptions } from './
import { colorTheme } from '../../../uuiui'
import type { IcnProps } from '../../../uuiui'
import { UtopiaTheme, Tooltip, Icn } from '../../../uuiui'
import { useIsMyProject } from '../../editor/store/collaborative-editing'
import { useControlsDisabledInSubtree } from '../../../uuiui/utilities/disable-subtree'

export interface DEPRECATEDOptionControlOptions extends DEPRECATEDGenericControlOptions {
icon?: IcnProps
iconComponent?: React.ReactNode
labelInner?: string
tooltip?: string
roundCorners?:
Expand Down Expand Up @@ -181,7 +181,9 @@ export const OptionControl: React.FunctionComponent<
style={{ marginRight: controlOptions.labelInner == null ? 0 : 4 }}
{...controlOptions.icon}
/>
) : null}
) : (
controlOptions.iconComponent ?? null
)}
{controlOptions.labelInner != null ? controlOptions.labelInner : null}
</div>
</label>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
import fastDeepEquals from 'fast-deep-equal'
import React from 'react'
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import { isRight } from '../../../../core/shared/either'
import { isJSXElement } from '../../../../core/shared/element-template'
import { parseNumber } from '../../../../core/shared/math-utils'
import { forceNotNull } from '../../../../core/shared/optional-utils'
import type { ElementPath, Imports, PropertyPath } from '../../../../core/shared/project-file-types'
import { importDetailsFromImportOption } from '../../../../core/shared/project-file-types'
import * as PP from '../../../../core/shared/property-path'
import { assertNever } from '../../../../core/shared/utils'
import { useKeepReferenceEqualityIfPossible } from '../../../../utils/react-performance'
import type { JSXParsedType, JSXParsedValue } from '../../../../utils/value-parser-utils'
import type { NumberInputProps } from '../../../../uuiui'
import {
ChainedNumberInput,
FlexColumn,
FlexRow,
Icn,
Icons,
PopupList,
SimpleNumberInput,
UtopiaTheme,
useColorTheme,
useWrappedEmptyOrUnknownOnSubmitValue,
wrappedEmptyOrUnknownOnSubmitValue,
} from '../../../../uuiui'
import type { CSSCursor } from '../../../../uuiui-deps'
import { SliderControl, getControlStyles } from '../../../../uuiui-deps'
import {
normalisePathSuccessOrThrowError,
normalisePathToUnderlyingTarget,
} from '../../../custom-code/code-file'
import type {
AllowedEnumType,
BasicControlOption,
Expand All @@ -24,50 +53,22 @@ import type {
Vector3ControlDescription,
Vector4ControlDescription,
} from '../../../custom-code/internal-property-controls'
import type { EditorAction } from '../../../editor/action-types'
import { addImports, forceParseFile } from '../../../editor/actions/action-creators'
import { useDispatch } from '../../../editor/store/dispatch-context'
import { Substores, useEditorState } from '../../../editor/store/store-hook'
import type { CSSNumber } from '../../common/css-utils'
import { cssNumber, defaultCSSColor, printCSSNumber } from '../../common/css-utils'
import type { InspectorInfo, InspectorInfoWithRawValue } from '../../common/property-path-hooks'
import { BooleanControl } from '../../controls/boolean-control'
import type { NumberInputProps } from '../../../../uuiui'
import {
useWrappedEmptyOrUnknownOnSubmitValue,
SimpleNumberInput,
PopupList,
ChainedNumberInput,
wrappedEmptyOrUnknownOnSubmitValue,
FlexColumn,
FlexRow,
UtopiaTheme,
useColorTheme,
Icn,
} from '../../../../uuiui'
import type { CSSNumber } from '../../common/css-utils'
import { printCSSNumber, cssNumber, defaultCSSColor } from '../../common/css-utils'
import * as PP from '../../../../core/shared/property-path'
import { ColorControl } from '../../controls/color-control'
import { StringControl } from '../../controls/string-control'
import type { SelectOption } from '../../controls/select-control'
import type { OptionChainOption } from '../../controls/option-chain-control'
import { OptionChainControl } from '../../controls/option-chain-control'
import { useKeepReferenceEqualityIfPossible } from '../../../../utils/react-performance'
import { UIGridRow } from '../../widgets/ui-grid-row'
import type { ElementPath, Imports, PropertyPath } from '../../../../core/shared/project-file-types'
import { importDetailsFromImportOption } from '../../../../core/shared/project-file-types'
import { Substores, useEditorState } from '../../../editor/store/store-hook'
import { addImports, forceParseFile } from '../../../editor/actions/action-creators'
import type { EditorAction } from '../../../editor/action-types'
import { forceNotNull } from '../../../../core/shared/optional-utils'
import type { SelectOption } from '../../controls/select-control'
import type { DEPRECATEDSliderControlOptions } from '../../controls/slider-control'
import {
normalisePathSuccessOrThrowError,
normalisePathToUnderlyingTarget,
} from '../../../custom-code/code-file'
import { useDispatch } from '../../../editor/store/dispatch-context'
import { StringControl } from '../../controls/string-control'
import { UIGridRow } from '../../widgets/ui-grid-row'
import { HtmlPreview, ImagePreview } from './property-content-preview'
import { isJSXElement } from '../../../../core/shared/element-template'
import type { JSXParsedType, JSXParsedValue } from '../../../../utils/value-parser-utils'
import { assertNever } from '../../../../core/shared/utils'
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import { isRight } from '../../../../core/shared/either'
import { parseNumber } from '../../../../core/shared/math-utils'

export interface ControlForPropProps<T extends RegularControlDescription> {
propPath: PropertyPath
Expand Down Expand Up @@ -435,6 +436,16 @@ function labelFromRadioControlOption(option: RadioControlOption<unknown>): strin
}
}

function iconFromRadioControlOption(
option: RadioControlOption<unknown>,
): OptionChainOption<unknown>['iconComponent'] {
if (option.type === 'allowed-enum-type' || option.option.icon == null) {
return undefined
}

return React.createElement(Icons[option.option.icon])
}

export const RadioPropertyControl = React.memo(
(props: ControlForPropProps<RadioControlDescription>) => {
const { propName, propMetadata, controlDescription } = props
Expand All @@ -450,6 +461,7 @@ export const RadioPropertyControl = React.memo(
return {
value: valueFromRadioControlOption(option),
label: labelFromRadioControlOption(option),
iconComponent: iconFromRadioControlOption(option),
}
}),
)
Expand Down
115 changes: 56 additions & 59 deletions editor/src/core/property-controls/property-controls-parser.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
import type {
CheckboxControlDescription,
ColorControlDescription,
ControlDescription,
NumberInputControlDescription,
RadioControlDescription,
PopUpListControlDescription,
StringInputControlDescription,
NoneControlDescription,
UnionControlDescription,
ArrayControlDescription,
ObjectControlDescription,
StyleControlsControlDescription,
Vector2ControlDescription,
Vector3ControlDescription,
ExpressionPopUpListControlDescription,
ImportType,
PropertyControls,
RegularControlDescription,
ExpressionInputControlDescription,
RegularControlType,
Vector4ControlDescription,
EulerControlDescription,
Matrix3ControlDescription,
Matrix4ControlDescription,
BasicControlOption,
BasicControlOptions,
ExpressionControlOption,
TupleControlDescription,
HtmlInputControlDescription,
JSXControlDescription,
AllowedEnumType,
Matrix3,
Matrix4,
ComponentExample,
PreferredContents,
import {
type CheckboxControlDescription,
type ColorControlDescription,
type ControlDescription,
type NumberInputControlDescription,
type RadioControlDescription,
type PopUpListControlDescription,
type StringInputControlDescription,
type NoneControlDescription,
type UnionControlDescription,
type ArrayControlDescription,
type ObjectControlDescription,
type StyleControlsControlDescription,
type Vector2ControlDescription,
type Vector3ControlDescription,
type ExpressionPopUpListControlDescription,
type ImportType,
type RegularControlDescription,
type ExpressionInputControlDescription,
type RegularControlType,
type Vector4ControlDescription,
type EulerControlDescription,
type Matrix3ControlDescription,
type Matrix4ControlDescription,
type BasicControlOption,
type BasicControlOptions,
type ExpressionControlOption,
type TupleControlDescription,
type HtmlInputControlDescription,
type JSXControlDescription,
type AllowedEnumType,
type Matrix3,
type Matrix4,
type PreferredContents,
type BasicControlOptionWithIcon,
} from 'utopia-api/core'
import { parseColor } from '../../components/inspector/common/css-utils'
import type { Parser, ParseResult } from '../../utils/value-parser-utils'
Expand All @@ -51,45 +50,27 @@ import {
parseArray,
parseBoolean,
parseConstant,
parseFunction,
parseNull,
parseNullable,
parseNumber,
parseObject,
parseString,
parseTuple,
parseUndefined,
parseEnum,
} from '../../utils/value-parser-utils'
import {
applicative2Either,
applicative3Either,
applicative4Either,
applicative5Either,
applicative6Either,
applicative8Either,
applicative9Either,
foldEither,
left,
right,
isRight,
mapEither,
flatMapEither,
isLeft,
applicative10Either,
applicative7Either,
} from '../shared/either'
import {
objectMap,
setOptionalProp,
forEachValue,
objectMapDropNulls,
} from '../shared/object-utils'
import { objectMap, setOptionalProp } from '../shared/object-utils'
import { parseEnumValue } from './property-control-values'
import {
parseComponentExample,
parseComponentInsertOption,
parsePreferredContents,
} from './property-controls-local'
import { parsePreferredContents } from './property-controls-local'
import { UtopiaIcons } from 'utopia-api'

const requiredFieldParser = optionalObjectKeyParser(parseBoolean, 'required')

Expand Down Expand Up @@ -129,6 +110,22 @@ const parseBasicControlOptions: Parser<BasicControlOptions<unknown>> = parseAlte
'Not a valid array of options',
)

const parseBasicControlOptionsWithIcon: Parser<
AllowedEnumType[] | BasicControlOptionWithIcon<unknown>[]
> = parseAlternative<AllowedEnumType[] | BasicControlOptionWithIcon<unknown>[]>(
[
parseArray(parseEnumValue),
parseArray(
objectParser<BasicControlOptionWithIcon<unknown>>({
label: parseString,
value: parseAny,
icon: optionalProp(parseEnum(UtopiaIcons)),
}),
),
],
'Not a valid array of options',
)

const parseEnumValueOrBasicControlOption: Parser<AllowedEnumType | BasicControlOption<unknown>> =
parseAlternative<AllowedEnumType | BasicControlOption<unknown>>(
[parseEnumValue, parseBasicControlOption<unknown>(parseAny)],
Expand Down Expand Up @@ -345,7 +342,7 @@ export function parseRadioControlDescription(value: unknown): ParseResult<RadioC
optionalObjectKeyParser(parseString, 'label')(value),
optionalObjectKeyParser(parseString, 'folder')(value),
objectKeyParser(parseConstant('radio'), 'control')(value),
objectKeyParser(parseBasicControlOptions, 'options')(value),
objectKeyParser(parseBasicControlOptionsWithIcon, 'options')(value),
optionalObjectKeyParser(parseBoolean, 'visibleByDefault')(value),
requiredFieldParser(value),
optionalObjectKeyParser(parseEnumValueOrBasicControlOption, 'defaultValue')(value),
Expand Down
2 changes: 1 addition & 1 deletion editor/src/uuiui/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ export const Icons = {
width: 12,
height: 12,
}),
}
} as const

export const FunctionIcons = {
Add: Icons.Plus,
Expand Down
2 changes: 2 additions & 0 deletions utopia-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export * from './utilities/placeholder'

export * from './helpers/helper-functions'
export * from './property-controls/factories'

export * from './primitives/icons'
Loading

0 comments on commit 036dd98

Please sign in to comment.