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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface ArrayRemoveButtonProps {
export const ArrayRemoveButton: React.FC<ArrayRemoveButtonProps> = ({name}) => {
const form = useForm();

const [ready, setReady] = React.useState(false);

const arrayItem = isArrayItem(name);
const tupleItem = isTupleItem(name, form);

Expand All @@ -29,7 +31,11 @@ export const ArrayRemoveButton: React.FC<ArrayRemoveButtonProps> = ({name}) => {
}
}, [form, name]);

if (arrayItem && !tupleItem) {
React.useLayoutEffect(() => {
setReady(true);
}, []);

if (ready && arrayItem && !tupleItem) {
return (
<Button view="flat-secondary" onClick={removeItem} qa={`${name}-remove-item`}>
<Icon data={TrashBin} size={16} />
Expand Down
12 changes: 12 additions & 0 deletions src/lib/unstable/kit/constants/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import {EntityType, type SchemaRendererConfig} from '../../core';
import {
Alert,
ArrayBase,
ArrayTable,
Checkbox,
CheckboxGroup,
ColorPicker,
DateInput,
DotValue,
FileInput,
Label,
MultiSelect,
NumberBase,
ObjectBase,
Expand All @@ -16,6 +21,7 @@ import {
StringBase,
Switch,
TextArea,
TextContent,
} from '../controls';
import {Row, Transparent} from '../wrappers';

Expand All @@ -31,6 +37,7 @@ export const untypedConfig = {
base: {Component: ArrayBase},
checkbox_group: {Component: CheckboxGroup},
select: {Component: MultiSelect},
array_table: {Component: ArrayTable},
},
views: {},
wrappers: {row: Row, transparent: Transparent},
Expand All @@ -54,6 +61,7 @@ export const untypedConfig = {
[EntityType.Object]: {
controls: {
base: {Component: ObjectBase},
dot_value: {Component: DotValue},
range_slider: {Component: RangeSlider},
},
views: {},
Expand All @@ -64,10 +72,14 @@ export const untypedConfig = {
controls: {
base: {Component: StringBase},
color_picker: {Component: ColorPicker},
file: {Component: FileInput},
password: {Component: Password},
radio_group: {Component: RadioGroup},
select: {Component: Select},
textarea: {Component: TextArea},
text_content: {Component: TextContent},
label: {Component: Label},
alert: {Component: Alert},
},
views: {},
wrappers: {row: Row, transparent: Transparent},
Expand Down
48 changes: 48 additions & 0 deletions src/lib/unstable/kit/controls/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';

import * as icons from '@gravity-ui/icons';
import {
Alert as GravityAlert,
type AlertProps as GravityAlertProps,
Icon,
type IconProps,
} from '@gravity-ui/uikit';

import type {Control, JsonSchemaString} from '../../../core';

export interface AlertProps extends GravityAlertProps {
iconName?: keyof typeof icons;
iconProps?: Partial<IconProps>;
}

const Component: Control<JsonSchemaString, AlertProps> = ({controlProps, schema}) => {
const {iconName, iconProps, message: messageProp, ...controlRestProps} = controlProps;

const icon = React.useMemo(
() =>
iconName && icons[iconName] ? (
<Icon data={icons[iconName]} {...iconProps} />
) : undefined,
[iconName, iconProps],
);

const message = React.useMemo(() => {
if (messageProp) {
if (typeof messageProp === 'string') {
return <span dangerouslySetInnerHTML={{__html: messageProp}} />;
}

return messageProp;
}

if (schema.description) {
return <span dangerouslySetInnerHTML={{__html: schema.description}} />;
}

return undefined;
}, [messageProp, schema.description]);

return <GravityAlert icon={icon} message={message} {...controlRestProps} />;
};

export const Alert = React.memo(Component);
1 change: 1 addition & 0 deletions src/lib/unstable/kit/controls/Alert/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {Alert, type AlertProps} from './Alert';
51 changes: 51 additions & 0 deletions src/lib/unstable/kit/controls/ArrayTable/ArrayTable.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@import '../../styles/functions.scss';
@import '../../styles/variables.scss';

.#{$ns}array-table {
&__row {
width: 100%;
display: grid;
align-items: start;
grid-template-columns: 18px repeat(var(--columns-count), 1fr);
gap: spacing(3);
padding: spacing(2) 0;
border-bottom: 1px solid var(--g-color-line-generic);

&_head {
align-items: center;
}

&_with-remove-button {
grid-template-columns: 18px repeat(var(--columns-count), 1fr) 24px;
}

&:last-child {
border-bottom: none;
}
}

&__cell {
width: 100%;
min-height: 28px;
display: flex;
align-items: center;
}

&__index {
margin: 0 auto;
}

&__column-title {
display: flex;
align-items: center;
gap: 0 spacing(0.5);
flex-wrap: wrap;
}

&__column-title-word {
display: flex;
align-items: center;
gap: 0 spacing(1);
flex-wrap: wrap;
}
}
177 changes: 177 additions & 0 deletions src/lib/unstable/kit/controls/ArrayTable/ArrayTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import React from 'react';

import {Plus, TrashBin} from '@gravity-ui/icons';
import {Button, Flex, HelpMark, Icon, Text} from '@gravity-ui/uikit';

import {type Control, Entity, type JsonSchema, type JsonSchemaArray} from '../../../core';
import {ControlError} from '../../components';
import {block, getValidationState} from '../../utils';

import './ArrayTable.scss';

const b = block('array-table');

export interface ArrayTableProps {
order?: string[];
addButtonText?: string;
disabled?: boolean;
}

const Component: Control<JsonSchemaArray, ArrayTableProps> = ({
controlProps,
input,
meta,
schema,
}) => {
const addButton = React.useMemo(() => {
const itemsSchema = schema.items;

if (Array.isArray(itemsSchema)) {
return null;
}

const onClick = () => input.onChange([...(input.value || []), itemsSchema?.default]);

return (
<Button
onClick={onClick}
disabled={controlProps.disabled || schema.readOnly}
qa={`${input.name}-add-button`}
>
<Icon data={Plus} size={14} />
{controlProps.addButtonText || null}
</Button>
);
}, [
controlProps.addButtonText,
controlProps.disabled,
input.onChange,
input.value,
schema.default,
schema.items,
schema.readOnly,
]);

const columns = React.useMemo(() => {
const columns: {name: string | undefined; schema: JsonSchema<any>}[] = [];
const itemsSchema = schema.items;

if (itemsSchema) {
const itemSchema = Array.isArray(itemsSchema) ? itemsSchema[0] : itemsSchema;

if (
'properties' in itemSchema &&
itemSchema.properties &&
Object.keys(itemSchema.properties).length > 0
) {
const properties = itemSchema.properties;

(controlProps.order || Object.keys(properties)).forEach((columnKey) => {
columns.push({name: columnKey, schema: properties[columnKey] || {}});
});
} else {
columns.push({name: undefined, schema: itemSchema});
}
}

return columns;
}, [controlProps.order, schema.items]);

const {head, rows} = React.useMemo(() => {
let rowsCount = input.value?.length;
let withRemoveButton = true;

if (Array.isArray(schema.items)) {
rowsCount = schema.items.length;
withRemoveButton = false;
}

const head = (
<div
className={b('row', {'with-remove-button': withRemoveButton})}
style={{'--columns-count': columns.length} as React.CSSProperties}
>
<div className={b('cell')}>
<Text className={b('index')} variant="subheader-1">
#
</Text>
</div>
{columns.map((column, cIndex) => (
<div className={b('cell')} key={cIndex}>
<div className={b('column-title')}>
{column.schema.title?.split(' ').map((word, wIndex, array) => (
<div className={b('column-title-word')} key={word}>
<Text variant="subheader-1">{word}</Text>
{wIndex + 1 === array.length && column.schema.description ? (
<HelpMark>{column.schema.description}</HelpMark>
) : null}
</div>
))}
</div>
</div>
))}
</div>
);

const rows = new Array(rowsCount).fill(null).map((_, rIndex) => (
<div
className={b('row', {'with-remove-button': withRemoveButton})}
style={{'--columns-count': columns.length} as React.CSSProperties}
key={rIndex}
>
<div className={b('cell')}>
<Text className={b('index')} variant="subheader-1">
{rIndex + 1}
</Text>
</div>
{columns.map((column, cIndex) => (
<div className={b('cell')} key={cIndex}>
<Entity
name={`${input.name}[${rIndex}]${
column.name === undefined ? '' : `.${column.name}`
}`}
schema={column.schema}
/>
</div>
))}
{withRemoveButton ? (
<div className={b('cell')}>
<Button
className={b('delete-button')}
view="flat-secondary"
size="s"
onClick={() =>
input.onChange(input.value?.filter((__, i) => i !== rIndex))
}
>
<Icon data={TrashBin} size={16} />
</Button>
</div>
) : null}
</div>
));

return {head, rows};
}, [
columns,
controlProps.order,
input.name,
input.onChange,
input.value?.length,
schema.items,
]);

return (
<ControlError errorMessage={meta.error} validationState={getValidationState(meta)}>
<Flex direction="column" gap={2}>
<Flex direction="column" gap={2}>
{head}
{rows}
</Flex>
{addButton}
</Flex>
</ControlError>
);
};

export const ArrayTable = React.memo(Component);
1 change: 1 addition & 0 deletions src/lib/unstable/kit/controls/ArrayTable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {ArrayTable, type ArrayTableProps} from './ArrayTable';
36 changes: 36 additions & 0 deletions src/lib/unstable/kit/controls/DotValue/DotValue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';

import get from 'lodash/get';
import isString from 'lodash/isString';

import {type Control, Entity, type JsonSchemaObject} from '../../../core';

export interface DotValueProps {}

const Component: Control<JsonSchemaObject, DotValueProps> = ({input, schema}) => {
const childKey = 'value';

React.useLayoutEffect(() => {
if (input.value) {
const childValue = get(input.value, childKey);

if (
childValue === null ||
childValue === undefined ||
childValue === '' ||
(isString(childValue) && childValue.endsWith('_UNSPECIFIED'))
) {
input.onChange(undefined);
}
}
}, [input.value]);

return (
<Entity
name={`${input.name ? input.name + '.' : ''}${childKey}`}
schema={schema.properties?.[childKey]}
/>
);
};

export const DotValue = React.memo(Component);
1 change: 1 addition & 0 deletions src/lib/unstable/kit/controls/DotValue/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {DotValue, type DotValueProps} from './DotValue';
Loading
Loading