diff --git a/docs/spec.md b/docs/spec.md index 6458736a..9b50ad8b 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -184,6 +184,7 @@ You can provide all props of [original component](https://preview.gravity-ui.com | iconColor | `'primary'` `'complementary'` `'secondary'` `'hint'` `'info'` `'info-heavy'` `'positive'` `'positive-heavy'` `'warning'` `'warning-heavy'` `'danger'` `'danger-heavy'` `'utility'` `'utility-heavy'` `'misc'` `'misc-heavy'` `'brand'` `'dark-primary'` `'dark-complementary'` `'dark-secondary'` | | The color of the icon, if it does not have the themeLabel parameter | | themeIcon | `'normal'` `'info'` `'success'` `'warning'` `'danger'` `'utility'` | | Alert color | | titleAlert | `string` | | Alert title | +| viewAlert | `'filled'` `'outlined'` | | Alert view | #### SelectParams diff --git a/src/lib/core/components/View/types/layout.ts b/src/lib/core/components/View/types/layout.ts index 8bbdcf2b..5196f66b 100644 --- a/src/lib/core/components/View/types/layout.ts +++ b/src/lib/core/components/View/types/layout.ts @@ -4,7 +4,10 @@ import {FormValue, Spec} from '../../../'; import {ViewProps} from './'; -export type ViewLayoutProps = { +export type ViewLayoutProps< + Value extends FormValue, + SpecType extends Spec | undefined> = Spec, +> = { children: React.ReactElement; } & ViewProps; diff --git a/src/lib/core/components/View/types/views.ts b/src/lib/core/components/View/types/views.ts index 564f4a9c..ddf85b07 100644 --- a/src/lib/core/components/View/types/views.ts +++ b/src/lib/core/components/View/types/views.ts @@ -4,7 +4,10 @@ import {FormValue, Spec} from '../../../'; import {ViewLayoutType} from './'; -export type ViewProps = { +export type ViewProps< + Value extends FormValue, + SpecType extends Spec | undefined> = Spec, +> = { spec: SpecType; name: string; value?: Value; diff --git a/src/lib/core/types/specs.ts b/src/lib/core/types/specs.ts index fb7b8a0f..6e3beef1 100644 --- a/src/lib/core/types/specs.ts +++ b/src/lib/core/types/specs.ts @@ -1,4 +1,4 @@ -import {AlertProps, LabelProps} from '@gravity-ui/uikit'; +import type {AlertProps, LabelProps} from '@gravity-ui/uikit'; import {ColorTextBaseProps} from '@gravity-ui/uikit/build/esm/components/Text/colorText/colorText'; import {ReadAsMethod, SpecTypes} from '../constants'; @@ -175,6 +175,7 @@ export interface StringSpec< iconColor?: ColorTextBaseProps['color']; titleAlert?: string; themeAlert?: AlertProps['theme']; + viewAlert?: AlertProps['view']; }; fileInput?: { accept?: string; diff --git a/src/lib/kit/components/Inputs/TextContent/TextContent.tsx b/src/lib/kit/components/Inputs/TextContent/TextContent.tsx index af8063c9..4cda1a7d 100644 --- a/src/lib/kit/components/Inputs/TextContent/TextContent.tsx +++ b/src/lib/kit/components/Inputs/TextContent/TextContent.tsx @@ -56,6 +56,7 @@ export const TextContentComponent: React.FC = ({ // If the title is an empty line, then you need to explicitly write undefined, otherwise there will be an additional indent title={titleAlert} theme={textContentParams?.themeAlert} + view={textContentParams?.viewAlert} /> ); } else if (textContentParams?.themeLabel) { diff --git a/src/lib/kit/components/Layouts/Accordeon/Accordeon.tsx b/src/lib/kit/components/Layouts/Accordeon/Accordeon.tsx index 7b261e88..1a619fea 100644 --- a/src/lib/kit/components/Layouts/Accordeon/Accordeon.tsx +++ b/src/lib/kit/components/Layouts/Accordeon/Accordeon.tsx @@ -1,18 +1,30 @@ import React from 'react'; +import {TextProps} from '@gravity-ui/uikit'; + import {ArrayLayoutProps, ObjectLayoutProps, isArrayItem} from '../../../../core'; import {ErrorWrapper} from '../../../components'; import {useErrorChecker} from '../../../hooks'; import {RemoveButton} from '../../RemoveButton'; import {SimpleVerticalAccordeon} from '../../SimpleVerticalAccordeon'; -export const Accordeon = ({ +interface AccordeonLayoutProps { + variantTitle?: TextProps['variant']; +} + +export const Accordeon = < + T extends + | ArrayLayoutProps | undefined, AccordeonLayoutProps | undefined> + | ObjectLayoutProps | undefined, AccordeonLayoutProps | undefined>, +>({ name, spec, input, meta, children, }: T): JSX.Element => { + const {variantTitle} = spec.viewSpec.layoutProps || {}; + const [open, setOpen] = React.useState(Boolean(spec.viewSpec?.layoutOpen)); const onDrop = React.useCallback(() => { @@ -40,6 +52,7 @@ export const Accordeon = ({ headerActionsTemplate={removeButton} hideInsteadOfDestroy withBranchView + variantTitle={variantTitle} > {children} diff --git a/src/lib/kit/components/Layouts/Section/Section.tsx b/src/lib/kit/components/Layouts/Section/Section.tsx index 4e1024a8..662a3d5e 100644 --- a/src/lib/kit/components/Layouts/Section/Section.tsx +++ b/src/lib/kit/components/Layouts/Section/Section.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {HelpPopover} from '@gravity-ui/components'; -import {Popover, Text} from '@gravity-ui/uikit'; +import {Popover, Text, TextProps} from '@gravity-ui/uikit'; import {GroupIndent} from '../../'; import {RemoveButton} from '../../RemoveButton'; @@ -23,6 +23,10 @@ import './Section.scss'; const b = block('section'); +interface SectionLayoutProps { + variantTitle?: TextProps['variant']; +} + interface SectionProps { titleSize: 's' | 'm'; withIndent?: boolean; @@ -30,7 +34,11 @@ interface SectionProps { descriptionAsSubtitle?: boolean; } -const SectionBase = ({ +const SectionBase = < + D extends FieldValue, + T extends FormValue, + S extends Spec, +>({ name, spec, titleSize, @@ -39,7 +47,7 @@ const SectionBase = ( descriptionAsSubtitle, children, ...restProps -}: (LayoutProps | ViewLayoutProps) & SectionProps) => { +}: (LayoutProps | ViewLayoutProps) & SectionProps) => { const input = (restProps as FieldRenderProps).input as | FieldRenderProps['input'] | undefined; @@ -48,6 +56,29 @@ const SectionBase = ( const titleRef = React.useRef(null); let content = children; + const {variantTitle: variantTitleProp} = spec.viewSpec.layoutProps || {}; + + const {sizeTitle, variantTitle} = React.useMemo(() => { + if (variantTitleProp) { + return { + sizeTitle: undefined, + variantTitle: variantTitleProp, + }; + } + + if (titleSize === 'm') { + return { + sizeTitle: titleSize, + variantTitle: 'body-2', + }; + } + + return { + sizeTitle: titleSize, + variantTitle: 'body-1', + }; + }, [variantTitleProp, titleSize]); + const removeButton = React.useMemo(() => { if (input?.value && input?.onDrop && isArrayItem(name)) { return ( @@ -107,7 +138,7 @@ const SectionBase = (
( > diff --git a/src/lib/kit/components/SimpleVerticalAccordeon/SimpleVerticalAccordeon.tsx b/src/lib/kit/components/SimpleVerticalAccordeon/SimpleVerticalAccordeon.tsx index 60e0dade..24e9c9f2 100644 --- a/src/lib/kit/components/SimpleVerticalAccordeon/SimpleVerticalAccordeon.tsx +++ b/src/lib/kit/components/SimpleVerticalAccordeon/SimpleVerticalAccordeon.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {HelpPopover} from '@gravity-ui/components'; import {ChevronDown} from '@gravity-ui/icons'; -import {Button, Icon, Popover, Text} from '@gravity-ui/uikit'; +import {Button, Icon, Popover, Text, TextProps} from '@gravity-ui/uikit'; import {COMMON_POPOVER_PLACEMENT} from '../../constants/common'; import {block} from '../../utils'; @@ -27,6 +27,7 @@ interface SimpleVerticalAccordeonProps { hideInsteadOfDestroy?: boolean; withBranchView?: boolean; viewLayout?: boolean; + variantTitle?: TextProps['variant']; } interface SimpleVerticalAccordeonState { @@ -81,6 +82,7 @@ export class SimpleVerticalAccordeon extends React.Component< withBranchView, viewLayout, name, + variantTitle, } = this.props; const {open, hidden, isFirstRender} = this.state; @@ -104,7 +106,7 @@ export class SimpleVerticalAccordeon extends React.Component< const titlePopoverDisabled = (this.titleRef.current?.offsetWidth || 0) <= TITLE_TEXT_MAX_WIDTH; - const currentTitleVariant = this.getCurrentTitleVariant(); + const currentTitleVariant = variantTitle || this.getCurrentTitleVariant(); return ( Boolean(React.Children.count(children)) && ( diff --git a/src/lib/kit/components/ViewLayouts/ViewAccordeon/ViewAccordeon.tsx b/src/lib/kit/components/ViewLayouts/ViewAccordeon/ViewAccordeon.tsx index 7e866445..f8463e8c 100644 --- a/src/lib/kit/components/ViewLayouts/ViewAccordeon/ViewAccordeon.tsx +++ b/src/lib/kit/components/ViewLayouts/ViewAccordeon/ViewAccordeon.tsx @@ -1,12 +1,19 @@ import React from 'react'; import isBoolean from 'lodash/isBoolean'; +import {TextProps} from '@gravity-ui/uikit'; -import {ArrayViewLayoutProps, ObjectViewLayoutProps, useDynamicFormsCtx} from '../../../../core'; +import {ArrayValue, ObjectValue, Spec, ViewLayoutProps, useDynamicFormsCtx} from '../../../../core'; import {isNotEmptyValue} from '../../../utils'; import {SimpleVerticalAccordeon} from '../../SimpleVerticalAccordeon'; -export const ViewAccordeon = ({ +interface ViewAccordeonLayoutProps { + variantTitle?: TextProps['variant']; +} + +export const ViewAccordeon = < + T extends ViewLayoutProps>, +>({ name, value, spec, @@ -17,7 +24,9 @@ export const ViewAccordeon = {children} diff --git a/src/lib/kit/utils/common.ts b/src/lib/kit/utils/common.ts index c9781dcb..cfed7206 100644 --- a/src/lib/kit/utils/common.ts +++ b/src/lib/kit/utils/common.ts @@ -120,6 +120,11 @@ export const prepareSpec = ( result.viewSpec.textContentParams.themeAlert.toLowerCase(); } + if (isString(result.viewSpec?.textContentParams?.viewAlert)) { + result.viewSpec.textContentParams.viewAlert = + result.viewSpec.textContentParams.viewAlert.toLowerCase(); + } + if (isString(result.validator)) { result.validator = result.validator.toLowerCase(); } diff --git a/src/stories/ArrayCheckboxGroup.stories.tsx b/src/stories/ArrayCheckboxGroup.stories.tsx index ff33ddad..090f9822 100644 --- a/src/stories/ArrayCheckboxGroup.stories.tsx +++ b/src/stories/ArrayCheckboxGroup.stories.tsx @@ -46,6 +46,7 @@ const excludeOptions = [ 'viewSpec.inputProps', 'viewSpec.placeholder', 'viewSpec.layoutOpen', + 'viewSpec.layoutProps', ]; const template = () => { diff --git a/src/stories/ArraySelect.stories.tsx b/src/stories/ArraySelect.stories.tsx index 20761534..94eec848 100644 --- a/src/stories/ArraySelect.stories.tsx +++ b/src/stories/ArraySelect.stories.tsx @@ -57,6 +57,7 @@ const excludeOptions = [ 'viewSpec.itemPrefix', 'viewSpec.addButtonPosition', 'viewSpec.checkboxGroupParams', + 'viewSpec.layoutProps', ]; const template = () => { diff --git a/src/stories/ArrayTable.stories.tsx b/src/stories/ArrayTable.stories.tsx index a6b2135f..6f335b8d 100644 --- a/src/stories/ArrayTable.stories.tsx +++ b/src/stories/ArrayTable.stories.tsx @@ -64,6 +64,7 @@ const excludeOptions = [ 'viewSpec.selectParams', 'viewSpec.checkboxGroupParams', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const value = [ diff --git a/src/stories/ObjectCardOneOf.stories.tsx b/src/stories/ObjectCardOneOf.stories.tsx index 8a427ea3..98d109a4 100644 --- a/src/stories/ObjectCardOneOf.stories.tsx +++ b/src/stories/ObjectCardOneOf.stories.tsx @@ -64,6 +64,7 @@ const excludeOptions = [ 'viewSpec.placeholder', 'viewSpec.delimiter', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const template = (spec: ObjectSpec = baseSpec) => { diff --git a/src/stories/ObjectInline.stories.tsx b/src/stories/ObjectInline.stories.tsx index b06d9037..7c81ed90 100644 --- a/src/stories/ObjectInline.stories.tsx +++ b/src/stories/ObjectInline.stories.tsx @@ -60,6 +60,7 @@ const excludeOptions = [ 'viewSpec.oneOfParams', 'viewSpec.placeholder', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const template = (spec: ObjectSpec = baseSpec) => { diff --git a/src/stories/ObjectMultiOneOf.stories.tsx b/src/stories/ObjectMultiOneOf.stories.tsx index ca6d7086..8b831c15 100644 --- a/src/stories/ObjectMultiOneOf.stories.tsx +++ b/src/stories/ObjectMultiOneOf.stories.tsx @@ -63,6 +63,7 @@ const excludeOptions = [ 'viewSpec.oneOfParams', 'viewSpec.delimiter', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const value = { diff --git a/src/stories/ObjectMultiOneOfFlat.stories.tsx b/src/stories/ObjectMultiOneOfFlat.stories.tsx index d45ccc68..4de0aed6 100644 --- a/src/stories/ObjectMultiOneOfFlat.stories.tsx +++ b/src/stories/ObjectMultiOneOfFlat.stories.tsx @@ -63,6 +63,7 @@ const excludeOptions = [ 'viewSpec.oneOfParams', 'viewSpec.delimiter', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const value = { diff --git a/src/stories/ObjectOneOf.stories.tsx b/src/stories/ObjectOneOf.stories.tsx index b6de963c..160b1ef2 100644 --- a/src/stories/ObjectOneOf.stories.tsx +++ b/src/stories/ObjectOneOf.stories.tsx @@ -65,6 +65,7 @@ const excludeOptions = [ 'viewSpec.placeholder', 'viewSpec.delimiter', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const template = (spec: ObjectSpec = baseSpec) => { diff --git a/src/stories/ObjectOneOfFlat.stories.tsx b/src/stories/ObjectOneOfFlat.stories.tsx index f50c9baf..287adb79 100644 --- a/src/stories/ObjectOneOfFlat.stories.tsx +++ b/src/stories/ObjectOneOfFlat.stories.tsx @@ -65,6 +65,7 @@ const excludeOptions = [ 'viewSpec.placeholder', 'viewSpec.delimiter', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const template = (spec: ObjectSpec = baseSpec) => { diff --git a/src/stories/ObjectSecret.stories.tsx b/src/stories/ObjectSecret.stories.tsx index d0a53304..31edf13c 100644 --- a/src/stories/ObjectSecret.stories.tsx +++ b/src/stories/ObjectSecret.stories.tsx @@ -37,6 +37,7 @@ const excludeOptions = [ 'viewSpec.placeholder', 'viewSpec.delimiter', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const template = (spec: ObjectSpec = baseSpec) => { diff --git a/src/stories/ObjectTextLink.stories.tsx b/src/stories/ObjectTextLink.stories.tsx index 0ac2fc39..90172d2f 100644 --- a/src/stories/ObjectTextLink.stories.tsx +++ b/src/stories/ObjectTextLink.stories.tsx @@ -42,6 +42,7 @@ const excludeOptions = [ 'viewSpec.placeholder', 'viewSpec.delimiter', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const template = (spec: ObjectSpec = baseSpec) => { diff --git a/src/stories/ObjectTimeRangeSelector.stories.tsx b/src/stories/ObjectTimeRangeSelector.stories.tsx index 34b7600f..94652fee 100644 --- a/src/stories/ObjectTimeRangeSelector.stories.tsx +++ b/src/stories/ObjectTimeRangeSelector.stories.tsx @@ -106,6 +106,7 @@ const excludeOptions = [ 'viewSpec.placeholder', 'viewSpec.delimiter', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const template = (spec: ObjectSpec = baseSpec) => { diff --git a/src/stories/ObjectValue.stories.tsx b/src/stories/ObjectValue.stories.tsx index 43ab8c0e..ff40af3d 100644 --- a/src/stories/ObjectValue.stories.tsx +++ b/src/stories/ObjectValue.stories.tsx @@ -39,6 +39,7 @@ const excludeOptions = [ 'viewSpec.placeholder', 'viewSpec.delimiter', 'viewSpec.inputProps', + 'viewSpec.layoutProps', ]; const template = (spec: ObjectSpec = baseSpec) => { diff --git a/src/stories/components/InputPreview/constants.ts b/src/stories/components/InputPreview/constants.ts index 4a59f51e..7760f483 100644 --- a/src/stories/components/InputPreview/constants.ts +++ b/src/stories/components/InputPreview/constants.ts @@ -359,6 +359,11 @@ const textContentParams: ObjectSpec = { enum: ['―', 'normal', 'info', 'danger', 'warning', 'success', 'utility'], viewSpec: {type: 'select', layout: 'row', layoutTitle: 'Theme alert'}, }, + viewAlert: { + type: SpecTypes.String, + enum: ['―', 'filled', 'outlined'], + viewSpec: {type: 'select', layout: 'row', layoutTitle: 'View alert'}, + }, }, viewSpec: { type: 'base', @@ -575,6 +580,48 @@ const copy: BooleanSpec = { viewSpec: {type: 'base', layout: 'row', layoutTitle: 'Copy'}, }; +const layoutProps: ArraySpec = { + type: SpecTypes.Array, + items: { + type: SpecTypes.Object, + required: true, + properties: { + prop: { + type: SpecTypes.Object, + required: true, + properties: { + key: { + type: SpecTypes.String, + viewSpec: {type: 'base', layout: 'transparent', placeholder: 'property'}, + }, + value: { + type: SpecTypes.String, + viewSpec: {type: 'base', layout: 'transparent', placeholder: 'value'}, + }, + }, + viewSpec: { + type: 'inline', + layout: 'transparent', + delimiter: {key: ':'}, + }, + }, + parse: { + type: SpecTypes.Boolean, + viewSpec: { + type: 'base', + inputProps: {content: 'parse like JSON value'} as unknown as undefined, + }, + }, + }, + viewSpec: {type: 'base', layout: 'transparent'}, + }, + viewSpec: { + type: 'base', + layout: 'row', + layoutTitle: 'Layout props', + }, +}; + const inputProps: ArraySpec = { type: SpecTypes.Array, items: { @@ -645,6 +692,7 @@ export const getArrayOptions = (): ObjectSpec => ({ selectParams, checkboxGroupParams, inputProps, + layoutProps, }, [ 'disabled', @@ -662,6 +710,7 @@ export const getArrayOptions = (): ObjectSpec => ({ 'selectParams', 'checkboxGroupParams', 'inputProps', + 'layoutProps', ], ), }, @@ -782,6 +831,7 @@ export const getObjectOptions = (): ObjectSpec => ({ hidden, delimiter, timeRangeSelectorParams, + layoutProps, }, [ 'disabled', @@ -796,6 +846,7 @@ export const getObjectOptions = (): ObjectSpec => ({ 'hidden', 'delimiter', 'timeRangeSelectorParams', + 'layoutProps', ], ), }, diff --git a/src/stories/components/InputPreview/utils.ts b/src/stories/components/InputPreview/utils.ts index b70ba695..88dd4f24 100644 --- a/src/stories/components/InputPreview/utils.ts +++ b/src/stories/components/InputPreview/utils.ts @@ -231,6 +231,33 @@ export const transformIncorrect = (spec: Spec) => { ); } + if (_spec.viewSpec.layoutProps) { + const incorrectLayoutProps = _spec.viewSpec.layoutProps as unknown as { + prop?: {key?: string; value?: string}; + parse: string; + }[]; + + // @ts-ignore + _spec.viewSpec.layoutProps = incorrectLayoutProps.reduce( + (acc: Record, {prop, parse}) => { + if (prop?.key && prop?.value) { + if (parse) { + try { + const _value = JSON.parse(prop.value); + + acc[prop.key] = _value; + } catch {} + } else { + acc[prop.key] = prop.value; + } + } + + return acc; + }, + {}, + ); + } + return _spec; };