Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/lib/unstable/core/Entity/Entity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ const EntityComponent: React.FC<EntityProps> = ({name, schema: schemaProps = {}}
}
}

return content ? <div>{content}</div> : null;
return content;
};

export const Entity = React.memo(EntityComponent);
4 changes: 2 additions & 2 deletions src/lib/unstable/core/Entity/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type GetRenderKitReturn<Schema extends JsonSchema> = {
};
};

export interface EntityState {
export interface EntityState<Schema extends JsonSchema = JsonSchema> {
headName?: string;
schema?: JsonSchema;
schema?: Schema;
}
2 changes: 1 addition & 1 deletion src/lib/unstable/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export {Entity, type EntityProps} from './Entity';
export {Entity, type EntityProps, type EntityState} from './Entity';
export {SchemaRenderer, type SchemaRendererProps} from './SchemaRenderer';
export {EntityType, JsonSchemaType, SchemaRendererMode} from './constants';
export type * from './types';
Expand Down
1 change: 1 addition & 0 deletions src/lib/unstable/core/types/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface WrapperProps<
copy?: boolean;
required?: boolean;
} & Partial<WrapperComponentProps>;
children: React.ReactNode;
}

export type Wrapper<
Expand Down
170 changes: 104 additions & 66 deletions src/lib/unstable/core/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,105 @@ import type {
import type {ErrorMessages} from './validation';
import type {ArrayValue, FieldValue, ObjectValue} from './values';

type ControlEntityParameters<
TypeConfig extends SchemaRendererConfig[EntityType],
Control extends ObjectKeys<TypeConfig['controls']> = ObjectKeys<TypeConfig['controls']>,
> = [Control] extends [never]
? {}
: {
[C in Control]: {
/**
* Identifier of a control registered in `config[entityType].controls`. Selects
* which React component renders the field in form mode. When omitted, the
* renderer falls back to the default control for this entity type.
*/
controlType?: C;
/**
* Extra props forwarded to the selected control. Typed to the `controlProps`
* shape declared by the chosen control component.
*/
controlProps?: ExtractControlProps<TypeConfig['controls'][C]>;
};
}[Control];

type ControlWrapperEntityParameters<
TypeConfig extends SchemaRendererConfig[EntityType],
Wrapper extends ObjectKeys<TypeConfig['wrappers']> = ObjectKeys<TypeConfig['wrappers']>,
> = [Wrapper] extends [never]
? {}
: {
[W in Wrapper]: {
/**
* Identifier of a wrapper registered in `config[entityType].wrappers`. The
* wrapper component is rendered around the control in form mode (e.g. to add
* a label, layout, or visibility logic).
*/
controlWrapperType?: W;
/**
* Extra props forwarded to the selected control wrapper. Typed to the
* `wrapperProps` shape declared by the chosen wrapper component.
*/
controlWrapperProps?: ExtractWrapperProps<TypeConfig['wrappers'][W]>;
};
}[Wrapper];

type ViewEntityParameters<
TypeConfig extends SchemaRendererConfig[EntityType],
View extends ObjectKeys<TypeConfig['views']> = ObjectKeys<TypeConfig['views']>,
> = [View] extends [never]
? {}
: {
[V in View]: {
/**
* Identifier of a view registered in `config[entityType].views`. Selects which
* React component renders the field in overview (read-only) mode. When omitted,
* the renderer falls back to the default view for this entity type.
*/
viewType?: V;
/**
* Extra props forwarded to the selected view. Typed to the `viewProps` shape
* declared by the chosen view component.
*/
viewProps?: ExtractViewProps<TypeConfig['views'][V]>;
};
}[View];

type ViewWrapperEntityParameters<
TypeConfig extends SchemaRendererConfig[EntityType],
Wrapper extends ObjectKeys<TypeConfig['wrappers']> = ObjectKeys<TypeConfig['wrappers']>,
> = [Wrapper] extends [never]
? {}
: {
[W in Wrapper]: {
/**
* Identifier of a wrapper registered in `config[entityType].wrappers`. The
* wrapper component is rendered around the view in overview mode.
*/
viewWrapperType?: W;
/**
* Extra props forwarded to the selected view wrapper. Typed to the
* `wrapperProps` shape declared by the chosen wrapper component.
*/
viewWrapperProps?: ExtractWrapperProps<TypeConfig['wrappers'][W]>;
};
}[Wrapper];

type ValidatorEntityParameters<Validator extends string> = [Validator] extends [never]
? {}
: {
[V in Validator]: {
/**
* Identifier of a custom validator registered in `config[entityType].validators`.
* Runs in addition to (or instead of, depending on the renderer policy) the
* JSON Schema validation derived from the schema keywords.
*
* TODO(verify): confirm whether the custom validator replaces or complements
* JSON Schema validation and adjust this comment.
*/
validatorType?: V;
};
}[Validator];

/**
* Renderer-specific configuration carried on each schema node. Not part of the
* JSON Schema specification — these keywords are ignored by JSON Schema
Expand Down Expand Up @@ -41,71 +140,6 @@ interface EntityParameters<
* via different `EntityType` registrations).
*/
type?: Type;
/**
* Identifier of a control registered in `config[entityType].controls`. Selects
* which React component renders the field in form mode. When omitted, the
* renderer falls back to the default control for this entity type.
*/
controlType?: Control;
/**
* Extra props forwarded to the selected control. Typed to the `controlProps`
* shape declared by the chosen control component.
*/
controlProps?: ExtractControlProps<TypeConfig['controls'][Control]>;
/**
* Identifier of a wrapper registered in `config[entityType].wrappers`. The
* wrapper component is rendered around the control in form mode (e.g. to add
* a label, layout, or visibility logic).
*/
controlWrapperType?: Wrapper;
/**
* Extra props forwarded to the selected control wrapper. Typed to the
* `wrapperProps` shape declared by the chosen wrapper component.
*/
controlWrapperProps?: ExtractWrapperProps<TypeConfig['wrappers'][Wrapper]>;
/**
* Identifier of a view registered in `config[entityType].views`. Selects which
* React component renders the field in overview (read-only) mode. When omitted,
* the renderer falls back to the default view for this entity type.
*/
viewType?: View;
/**
* Extra props forwarded to the selected view. Typed to the `viewProps` shape
* declared by the chosen view component.
*/
viewProps?: ExtractViewProps<TypeConfig['views'][View]>;
/**
* Identifier of a wrapper registered in `config[entityType].wrappers`. The
* wrapper component is rendered around the view in overview mode.
*/
viewWrapperType?: Wrapper;
/**
* Extra props forwarded to the selected view wrapper. Typed to the
* `wrapperProps` shape declared by the chosen wrapper component.
*/
viewWrapperProps?: ExtractWrapperProps<TypeConfig['wrappers'][Wrapper]>;
/**
* Identifier of a custom validator registered in `config[entityType].validators`.
* Runs in addition to (or instead of, depending on the renderer policy) the
* JSON Schema validation derived from the schema keywords.
*
* TODO(verify): confirm whether the custom validator replaces or complements
* JSON Schema validation and adjust this comment.
*/
validatorType?: Validator;
/**
* Map of enum value (as a string key) to a human-readable label. The renderer
* uses these labels in selects / radio groups instead of showing raw enum values.
*
* @example
* {
* enum: ['draft', 'published'],
* entityParameters: {
* enumDescription: {draft: 'Draft', published: 'Published'},
* },
* }
*/
enumDescription?: {[key: string]: string};
/**
* Custom error messages shown by the renderer when validation fails. Keys
* correspond to JSON Schema validation keywords (`minLength`, `pattern`, `enum`,
Expand Down Expand Up @@ -143,7 +177,11 @@ interface EntityParameters<
dependencies?: string | Record<string, string>;
required?: string | Record<string, string>;
};
};
} & ControlEntityParameters<TypeConfig, Control> &
ControlWrapperEntityParameters<TypeConfig, Wrapper> &
ViewEntityParameters<TypeConfig, View> &
ViewWrapperEntityParameters<TypeConfig, Wrapper> &
ValidatorEntityParameters<Validator>;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/lib/unstable/kit/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export const MultiSelect: Control<JsonSchemaArray> = ({input, schema}) => {
schema.items?.enum?.map((id) => ({
id,
value: id,
text: schema.entityParameters?.enumDescription?.[id] || id,
content: schema.entityParameters?.enumDescription?.[id] || id,
text: id,
content: id,
key: id,
})),
[
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';

import {TrashBin} from '@gravity-ui/icons';
import {Button, Icon} from '@gravity-ui/uikit';
import {useForm} from 'react-final-form';

import {getArrayItemIndex, getArrayItemParentName, isArrayItem, isTupleItem} from '../../utils';

export interface ArrayRemoveButtonProps {
name: string;
}

export const ArrayRemoveButton: React.FC<ArrayRemoveButtonProps> = ({name}) => {
const form = useForm();

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

const removeItem = React.useCallback(() => {
const parentName = getArrayItemParentName(name);
const parentValue = form.getFieldState(parentName)?.value;
const index = Number(getArrayItemIndex(name));

if (Array.isArray(parentValue)) {
form.change(
parentName,
parentValue.filter((_, i) => i !== index),
);
}
}, [form, name]);

if (arrayItem && !tupleItem) {
return (
<Button view="flat-secondary" onClick={removeItem} qa={`${name}-remove-item`}>
<Icon data={TrashBin} size={16} />
</Button>
);
}

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {ArrayRemoveButton, type ArrayRemoveButtonProps} from './ArrayRemoveButton';
71 changes: 71 additions & 0 deletions src/lib/unstable/kit/components/ControlError/ControlError.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
@import '../../styles/variables.scss';

.#{$ns}control-error {
&__children {
width: 100%;

&_errors {
.g-control-label:not(.g-control-label_disabled) {
.g-checkbox__indicator {
&:hover {
&::before {
border: 1px solid var(--g-color-text-danger);
}
}

&::before {
border: 1px solid var(--g-color-text-danger);
}
}
}

.g-text-input:not(.g-text-input_disabled) {
.g-text-input__content {
border-color: var(--g-color-text-danger);

&:hover {
border-color: var(--g-color-text-danger);
}
}
}

.g-text-area__content {
border-color: var(--g-color-text-danger);

&:hover {
border-color: var(--g-color-text-danger);
}
}

.g-select-control:not(.g-select-control_disabled) {
.g-select-control__button {
&:hover {
&::before {
// stylelint-disable-next-line declaration-no-important
border: 1px solid var(--g-color-text-danger) !important;
}
}

&::before {
// stylelint-disable-next-line declaration-no-important
border: 1px solid var(--g-color-text-danger) !important;
}
}
}

.g-segmented-radio-group:not([aria-disabled='true']) {
.g-segmented-radio-group__option_checked {
&:hover {
&::after {
border: 1px solid var(--g-color-text-danger);
}
}

&::after {
border: 1px solid var(--g-color-text-danger);
}
}
}
}
}
}
41 changes: 41 additions & 0 deletions src/lib/unstable/kit/components/ControlError/ControlError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';

import {Flex, Text} from '@gravity-ui/uikit';
import _ from 'lodash';

import {block} from '../../utils';

import './ControlError.scss';

const b = block('control-error');

export interface ControlErrorProps {
className?: string;
errorMessage?: string;
validationState?: 'invalid';
children: React.ReactNode;
}

export const ControlError: React.FC<ControlErrorProps> = ({
className,
errorMessage,
validationState,
children,
}) => {
const error =
validationState === 'invalid' && errorMessage ? (
<Text color="danger">{errorMessage}</Text>
) : null;

return (
<Flex
className={b(null, className)}
width="100%"
direction="column"
alignItems="flex-start"
>
<div className={b('children', {errors: Boolean(error)})}>{children}</div>
{error}
</Flex>
);
};
1 change: 1 addition & 0 deletions src/lib/unstable/kit/components/ControlError/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {ControlError, type ControlErrorProps} from './ControlError';
Loading
Loading