From 50a5490543ad454f9318877fd7c5fd6bb5af9349 Mon Sep 17 00:00:00 2001 From: Nikita Zolotykh Date: Tue, 25 Aug 2026 11:39:23 +0200 Subject: [PATCH] feat(unstable): add overview entities --- .../SchemaRendererNode/SchemaRendererNode.tsx | 7 +- .../utils/__tests__/get-dispatch.test.ts | 240 +++++++++++++ .../utils/__tests__/get-subscribe.test.ts | 218 +++++++++++ .../core/utils/__tests__/common.test.ts | 143 ++++++++ .../kit/components/CopyButton/CopyButton.scss | 12 + .../kit/components/CopyButton/CopyButton.tsx | 30 ++ .../kit/components/CopyButton/index.ts | 1 + .../EmptyEntityValue/EmptyEntityValue.tsx | 14 + .../kit/components/EmptyEntityValue/index.ts | 1 + .../EntityContainer/EntityContainer.scss | 4 +- .../EntityContainer/EntityContainer.tsx | 10 +- .../LayoutContainer/LayoutContainer.scss | 6 + .../LayoutContainer/LayoutContainer.tsx | 4 +- .../kit/components/LongValue/LongValue.scss | 25 ++ .../kit/components/LongValue/LongValue.tsx | 81 +++++ .../kit/components/LongValue/index.ts | 1 + src/lib/unstable/kit/components/index.ts | 3 + src/lib/unstable/kit/constants/common.ts | 1 + src/lib/unstable/kit/constants/config.ts | 53 ++- src/lib/unstable/kit/constants/index.ts | 1 + .../Alert/Alert.tsx | 2 +- .../Alert/index.ts | 0 .../ArrayBase/ArrayBase.tsx | 21 +- .../ArrayBase/index.ts | 0 .../ArrayTable/ArrayTable.scss | 0 .../ArrayTable/ArrayTable.tsx | 21 +- .../ArrayTable/index.ts | 0 .../unstable/kit/entities/Monaco/Monaco.tsx | 18 +- .../kit/entities/ObjectBase/ObjectBase.tsx | 18 +- src/lib/unstable/kit/entities/index.ts | 3 + .../form-entities/FewOfNested/FewOfNested.tsx | 2 +- .../form-entities/OneOfNested/OneOfNested.tsx | 2 +- .../form-entities/RangeInput/RangeInput.tsx | 4 +- src/lib/unstable/kit/form-entities/index.ts | 3 - src/lib/unstable/kit/index.ts | 2 + .../OverviewBooleanBase.tsx | 49 +++ .../OverviewBooleanBase/index.ts | 1 + .../OverviewStringBase/OverviewStringBase.tsx | 23 ++ .../OverviewStringBase/index.ts | 1 + .../unstable/kit/overview-entities/index.ts | 2 + .../OverviewRow/OverviewRow.scss | 50 +++ .../OverviewRow/OverviewRow.tsx | 49 +++ .../kit/overview-layouts/OverviewRow/index.ts | 1 + .../unstable/kit/overview-layouts/index.ts | 1 + .../kit/utils/__tests__/common.test.ts | 182 ++++++++++ src/stories/Unstable.stories.tsx | 337 +++++++++++------- 46 files changed, 1476 insertions(+), 171 deletions(-) create mode 100644 src/lib/unstable/core/useSchemaRenderer/utils/__tests__/get-dispatch.test.ts create mode 100644 src/lib/unstable/core/useSchemaRenderer/utils/__tests__/get-subscribe.test.ts create mode 100644 src/lib/unstable/core/utils/__tests__/common.test.ts create mode 100644 src/lib/unstable/kit/components/CopyButton/CopyButton.scss create mode 100644 src/lib/unstable/kit/components/CopyButton/CopyButton.tsx create mode 100644 src/lib/unstable/kit/components/CopyButton/index.ts create mode 100644 src/lib/unstable/kit/components/EmptyEntityValue/EmptyEntityValue.tsx create mode 100644 src/lib/unstable/kit/components/EmptyEntityValue/index.ts create mode 100644 src/lib/unstable/kit/components/LongValue/LongValue.scss create mode 100644 src/lib/unstable/kit/components/LongValue/LongValue.tsx create mode 100644 src/lib/unstable/kit/components/LongValue/index.ts create mode 100644 src/lib/unstable/kit/constants/common.ts rename src/lib/unstable/kit/{form-entities => entities}/Alert/Alert.tsx (96%) rename src/lib/unstable/kit/{form-entities => entities}/Alert/index.ts (100%) rename src/lib/unstable/kit/{form-entities => entities}/ArrayBase/ArrayBase.tsx (81%) rename src/lib/unstable/kit/{form-entities => entities}/ArrayBase/index.ts (100%) rename src/lib/unstable/kit/{form-entities => entities}/ArrayTable/ArrayTable.scss (100%) rename src/lib/unstable/kit/{form-entities => entities}/ArrayTable/ArrayTable.tsx (90%) rename src/lib/unstable/kit/{form-entities => entities}/ArrayTable/index.ts (100%) create mode 100644 src/lib/unstable/kit/overview-entities/OverviewBooleanBase/OverviewBooleanBase.tsx create mode 100644 src/lib/unstable/kit/overview-entities/OverviewBooleanBase/index.ts create mode 100644 src/lib/unstable/kit/overview-entities/OverviewStringBase/OverviewStringBase.tsx create mode 100644 src/lib/unstable/kit/overview-entities/OverviewStringBase/index.ts create mode 100644 src/lib/unstable/kit/overview-entities/index.ts create mode 100644 src/lib/unstable/kit/overview-layouts/OverviewRow/OverviewRow.scss create mode 100644 src/lib/unstable/kit/overview-layouts/OverviewRow/OverviewRow.tsx create mode 100644 src/lib/unstable/kit/overview-layouts/OverviewRow/index.ts create mode 100644 src/lib/unstable/kit/overview-layouts/index.ts create mode 100644 src/lib/unstable/kit/utils/__tests__/common.test.ts diff --git a/src/lib/unstable/core/SchemaRendererNode/SchemaRendererNode.tsx b/src/lib/unstable/core/SchemaRendererNode/SchemaRendererNode.tsx index 88b1b772..6c9dd1b0 100644 --- a/src/lib/unstable/core/SchemaRendererNode/SchemaRendererNode.tsx +++ b/src/lib/unstable/core/SchemaRendererNode/SchemaRendererNode.tsx @@ -4,7 +4,7 @@ import type {FieldState} from 'final-form'; import noop from 'lodash/noop'; import {type FieldInputProps, type FieldMetaState, useForm} from 'react-final-form'; -import {SchemaRendererEventType, type SchemaRendererMode} from '../constants'; +import {SchemaRendererEventType, SchemaRendererMode} from '../constants'; import type {JsonSchema} from '../types'; import {SCHEMA_RENDERER_SERVICE_FIELD} from '../useSchemaRenderer'; import {useSchemaRendererState} from '../useSchemaRendererState'; @@ -136,7 +136,10 @@ const SchemaRendererNodeComponent: React.FC = ({ let content = null; - if (mode) { + if ( + (mode === SchemaRendererMode.Overview && !schema?.writeOnly) || + (mode === SchemaRendererMode.Form && !schema?.readOnly) + ) { const {Entity, Layout, entityProps, independent, layoutProps} = kit[mode]; if (Entity) { diff --git a/src/lib/unstable/core/useSchemaRenderer/utils/__tests__/get-dispatch.test.ts b/src/lib/unstable/core/useSchemaRenderer/utils/__tests__/get-dispatch.test.ts new file mode 100644 index 00000000..ecca5fa0 --- /dev/null +++ b/src/lib/unstable/core/useSchemaRenderer/utils/__tests__/get-dispatch.test.ts @@ -0,0 +1,240 @@ +import {createForm} from 'final-form'; + +import {SchemaRendererEventType} from '../../../constants'; +import {getServiceFieldName} from '../../../utils'; +import {SCHEMA_RENDERER_SERVICE_FIELD} from '../../constants'; +import type {SchemaRendererState} from '../../types'; +import {getDispatch} from '../get-dispatch'; + +describe('getDispatch', () => { + test('does nothing if events are empty', () => { + const form = createForm({onSubmit: () => {}}); + const callback = jest.fn(); + const state = { + subscribers: { + byId: { + 'sub-1': { + callback, + subscription: {[SchemaRendererEventType.Patch]: true}, + }, + }, + byName: new Map(), + byPath: new Map(), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + getDispatch(form, 'form')([]); + + expect(callback).not.toHaveBeenCalled(); + }); + + test('notifies subscribers with a matching type when the event has all: true', () => { + const form = createForm({onSubmit: () => {}}); + const matching = jest.fn(); + const otherType = jest.fn(); + const state = { + subscribers: { + byId: { + 'sub-1': { + callback: matching, + subscription: {[SchemaRendererEventType.Patch]: true}, + }, + 'sub-2': { + callback: otherType, + subscription: {[SchemaRendererEventType.Error]: true}, + }, + }, + byName: new Map(), + byPath: new Map(), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const events = [{type: SchemaRendererEventType.Patch, all: true}]; + + getDispatch(form, 'form')(events); + + expect(matching).toHaveBeenCalledTimes(1); + expect(matching).toHaveBeenCalledWith(events); + expect(otherType).not.toHaveBeenCalled(); + }); + + test('notifies subscribers registered on a path prefix', () => { + const form = createForm({onSubmit: () => {}}); + const nested = jest.fn(); + const sibling = jest.fn(); + const state = { + subscribers: { + byId: { + 'sub-nested': { + callback: nested, + schemaPaths: ['#/properties/a'], + subscription: {[SchemaRendererEventType.Patch]: true}, + }, + 'sub-sibling': { + callback: sibling, + schemaPaths: ['#/properties/b'], + subscription: {[SchemaRendererEventType.Patch]: true}, + }, + }, + byName: new Map(), + byPath: new Map([ + ['#/properties/a', new Set(['sub-nested'])], + ['#/properties/b', new Set(['sub-sibling'])], + ]), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const events = [ + { + type: SchemaRendererEventType.Patch, + paths: [['#', 'properties', 'a', 'minLength']], + }, + ]; + + getDispatch(form, 'form')(events); + + expect(nested).toHaveBeenCalledTimes(1); + expect(nested).toHaveBeenCalledWith(events); + expect(sibling).not.toHaveBeenCalled(); + }); + + test('notifies subscribers registered by field name', () => { + const form = createForm({onSubmit: () => {}}); + const matching = jest.fn(); + const other = jest.fn(); + const state = { + subscribers: { + byId: { + 'sub-a': { + callback: matching, + name: 'form.a', + subscription: {[SchemaRendererEventType.Error]: true}, + }, + 'sub-b': { + callback: other, + name: 'form.b', + subscription: {[SchemaRendererEventType.Error]: true}, + }, + }, + byName: new Map([ + ['form.a', new Set(['sub-a'])], + ['form.b', new Set(['sub-b'])], + ]), + byPath: new Map(), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const events = [{type: SchemaRendererEventType.Error, names: ['form.a']}]; + + getDispatch(form, 'form')(events); + + expect(matching).toHaveBeenCalledTimes(1); + expect(matching).toHaveBeenCalledWith(events); + expect(other).not.toHaveBeenCalled(); + }); + + test('notifies a subscriber only once if it matches by path and by name', () => { + const form = createForm({onSubmit: () => {}}); + const callback = jest.fn(); + const state = { + subscribers: { + byId: { + 'sub-1': { + callback, + name: 'form.a', + schemaPaths: ['#/properties/a'], + subscription: {[SchemaRendererEventType.Patch]: true}, + }, + }, + byName: new Map([['form.a', new Set(['sub-1'])]]), + byPath: new Map([['#/properties/a', new Set(['sub-1'])]]), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const events = [ + { + type: SchemaRendererEventType.Patch, + names: ['form.a'], + paths: [['#', 'properties', 'a']], + }, + ]; + + getDispatch(form, 'form')(events); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(events); + }); + + test('notifies a subscriber only once if several events match', () => { + const form = createForm({onSubmit: () => {}}); + const callback = jest.fn(); + const state = { + subscribers: { + byId: { + 'sub-1': { + callback, + name: 'form.a', + subscription: { + [SchemaRendererEventType.Error]: true, + [SchemaRendererEventType.Patch]: true, + }, + }, + }, + byName: new Map([['form.a', new Set(['sub-1'])]]), + byPath: new Map(), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const events = [ + {type: SchemaRendererEventType.Patch, names: ['form.a']}, + {type: SchemaRendererEventType.Error, names: ['form.a']}, + ]; + + getDispatch(form, 'form')(events); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(events); + }); +}); diff --git a/src/lib/unstable/core/useSchemaRenderer/utils/__tests__/get-subscribe.test.ts b/src/lib/unstable/core/useSchemaRenderer/utils/__tests__/get-subscribe.test.ts new file mode 100644 index 00000000..194143b3 --- /dev/null +++ b/src/lib/unstable/core/useSchemaRenderer/utils/__tests__/get-subscribe.test.ts @@ -0,0 +1,218 @@ +import {createForm} from 'final-form'; + +import {SchemaRendererEventType} from '../../../constants'; +import {getServiceFieldName} from '../../../utils'; +import {SCHEMA_RENDERER_SERVICE_FIELD} from '../../constants'; +import type {SchemaRendererState} from '../../types'; +import {getSubscribe} from '../get-subscribe'; + +describe('subscribe', () => { + test('returns an id without registering if there is no renderer state', () => { + const form = createForm({onSubmit: () => {}}); + const {subscribe} = getSubscribe(form, 'form'); + + const id = subscribe({ + callback: jest.fn(), + subscription: {[SchemaRendererEventType.Patch]: true}, + }); + + expect(typeof id).toBe('string'); + expect(id.length).toBeGreaterThan(0); + }); + + test('registers a subscriber in byId', () => { + const form = createForm({onSubmit: () => {}}); + const subscriber = { + callback: jest.fn(), + subscription: {[SchemaRendererEventType.Patch]: true}, + }; + const state = { + subscribers: { + byId: {}, + byName: new Map(), + byPath: new Map(), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const {subscribe} = getSubscribe(form, 'form'); + const id = subscribe(subscriber); + + expect(state.subscribers.byId[id]).toBe(subscriber); + expect(state.subscribers.byName.size).toBe(0); + expect(state.subscribers.byPath.size).toBe(0); + }); + + test('indexes a subscriber by schemaPaths and name', () => { + const form = createForm({onSubmit: () => {}}); + const state = { + subscribers: { + byId: {}, + byName: new Map(), + byPath: new Map(), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const {subscribe} = getSubscribe(form, 'form'); + const id = subscribe({ + callback: jest.fn(), + name: 'form.a', + schemaPaths: ['#/properties/a', '#/properties/b'], + subscription: {[SchemaRendererEventType.Patch]: true}, + }); + + expect(state.subscribers.byName.get('form.a')).toEqual(new Set([id])); + expect(state.subscribers.byPath.get('#/properties/a')).toEqual(new Set([id])); + expect(state.subscribers.byPath.get('#/properties/b')).toEqual(new Set([id])); + }); + + test('adds a subscriber to an existing path and name index', () => { + const form = createForm({onSubmit: () => {}}); + const state = { + subscribers: { + byId: {}, + byName: new Map(), + byPath: new Map(), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const {subscribe} = getSubscribe(form, 'form'); + const firstId = subscribe({ + callback: jest.fn(), + name: 'form.a', + schemaPaths: ['#/properties/a'], + subscription: {[SchemaRendererEventType.Patch]: true}, + }); + const secondId = subscribe({ + callback: jest.fn(), + name: 'form.a', + schemaPaths: ['#/properties/a'], + subscription: {[SchemaRendererEventType.Error]: true}, + }); + + expect(state.subscribers.byName.get('form.a')).toEqual(new Set([firstId, secondId])); + expect(state.subscribers.byPath.get('#/properties/a')).toEqual( + new Set([firstId, secondId]), + ); + }); +}); + +describe('unsubscribe', () => { + test('does nothing if the subscriber id is unknown', () => { + const form = createForm({onSubmit: () => {}}); + const state = { + subscribers: { + byId: {}, + byName: new Map([['form.a', new Set(['other'])]]), + byPath: new Map([['#/properties/a', new Set(['other'])]]), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const {unsubscribe} = getSubscribe(form, 'form'); + + unsubscribe('missing'); + + expect(state.subscribers.byId).toEqual({}); + expect(state.subscribers.byName.get('form.a')).toEqual(new Set(['other'])); + expect(state.subscribers.byPath.get('#/properties/a')).toEqual(new Set(['other'])); + }); + + test('removes a subscriber from byId, byPath, and byName', () => { + const form = createForm({onSubmit: () => {}}); + const state = { + subscribers: { + byId: {}, + byName: new Map(), + byPath: new Map(), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const {subscribe, unsubscribe} = getSubscribe(form, 'form'); + const id = subscribe({ + callback: jest.fn(), + name: 'form.a', + schemaPaths: ['#/properties/a', '#/properties/b'], + subscription: {[SchemaRendererEventType.Patch]: true}, + }); + + unsubscribe(id); + + expect(state.subscribers.byId[id]).toBeUndefined(); + expect(state.subscribers.byName.has('form.a')).toBe(false); + expect(state.subscribers.byPath.has('#/properties/a')).toBe(false); + expect(state.subscribers.byPath.has('#/properties/b')).toBe(false); + }); + + test('keeps a shared path and name index when another subscriber remains', () => { + const form = createForm({onSubmit: () => {}}); + const state = { + subscribers: { + byId: {}, + byName: new Map(), + byPath: new Map(), + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + + const {subscribe, unsubscribe} = getSubscribe(form, 'form'); + const firstId = subscribe({ + callback: jest.fn(), + name: 'form.a', + schemaPaths: ['#/properties/a'], + subscription: {[SchemaRendererEventType.Patch]: true}, + }); + const secondId = subscribe({ + callback: jest.fn(), + name: 'form.a', + schemaPaths: ['#/properties/a'], + subscription: {[SchemaRendererEventType.Error]: true}, + }); + + unsubscribe(firstId); + + expect(state.subscribers.byId[firstId]).toBeUndefined(); + expect(state.subscribers.byId[secondId]).toBeDefined(); + expect(state.subscribers.byName.get('form.a')).toEqual(new Set([secondId])); + expect(state.subscribers.byPath.get('#/properties/a')).toEqual(new Set([secondId])); + }); +}); diff --git a/src/lib/unstable/core/utils/__tests__/common.test.ts b/src/lib/unstable/core/utils/__tests__/common.test.ts new file mode 100644 index 00000000..9dfe2167 --- /dev/null +++ b/src/lib/unstable/core/utils/__tests__/common.test.ts @@ -0,0 +1,143 @@ +import {JsonSchemaType} from '../../constants'; +import type {JsonSchemaObject} from '../../types'; +import { + arrayPathToDotBracket, + dotBracketToArrayPath, + getSchemaByPointer, + getServiceFieldName, + getValuePaths, + pointerToArrayPath, +} from '../common'; + +describe('pointerToArrayPath', () => { + test('parses a fragment pointer into path segments', () => { + expect(pointerToArrayPath('#/properties/name')).toEqual(['properties', 'name']); + }); + + test('parses a document pointer into path segments', () => { + expect(pointerToArrayPath('/properties/name')).toEqual(['properties', 'name']); + }); + + test('returns an empty array for an empty pointer', () => { + expect(pointerToArrayPath('#')).toEqual([]); + expect(pointerToArrayPath('')).toEqual([]); + }); + + test('unescapes JSON Pointer tokens', () => { + expect(pointerToArrayPath('#/foo~1bar')).toEqual(['foo/bar']); + expect(pointerToArrayPath('#/foo~0bar')).toEqual(['foo~bar']); + }); + + test('decodes URI-encoded characters', () => { + expect(pointerToArrayPath('#/foo%20bar')).toEqual(['foo bar']); + }); +}); + +describe('dotBracketToArrayPath', () => { + test('parses a dotted path with bracket indices', () => { + expect(dotBracketToArrayPath('items[0].name')).toEqual(['items', '0', 'name']); + }); + + test('parses nested object keys', () => { + expect(dotBracketToArrayPath('form.a.b')).toEqual(['form', 'a', 'b']); + }); + + test('parses consecutive bracket indices', () => { + expect(dotBracketToArrayPath('items[0][1]')).toEqual(['items', '0', '1']); + }); + + test('returns an empty array for an empty path', () => { + expect(dotBracketToArrayPath('')).toEqual([]); + }); +}); + +describe('arrayPathToDotBracket', () => { + test('joins object keys with dots and numeric segments with brackets', () => { + expect(arrayPathToDotBracket(['items', '0', 'name'])).toBe('items[0].name'); + }); + + test('returns an empty string for an empty path', () => { + expect(arrayPathToDotBracket([])).toBe(''); + }); + + test('wraps a leading numeric segment in brackets', () => { + expect(arrayPathToDotBracket(['0', 'name'])).toBe('[0].name'); + }); +}); + +describe('getSchemaByPointer', () => { + test('returns a nested schema by a string pointer', () => { + const nameSchema = {type: JsonSchemaType.String, minLength: 5}; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: {name: nameSchema}, + }; + + expect(getSchemaByPointer(schema, '#/properties/name')).toBe(nameSchema); + }); + + test('returns a nested schema by path segments', () => { + const nameSchema = {type: JsonSchemaType.String, minLength: 5}; + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: {name: nameSchema}, + }; + + expect(getSchemaByPointer(schema, ['properties', 'name'])).toBe(nameSchema); + }); + + test('returns the root schema when the pointer is empty', () => { + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: {name: {type: JsonSchemaType.String}}, + }; + + expect(getSchemaByPointer(schema, '#')).toBe(schema); + expect(getSchemaByPointer(schema, [])).toBe(schema); + }); + + test('returns undefined when the pointer does not resolve', () => { + const schema: JsonSchemaObject = { + type: JsonSchemaType.Object, + properties: {name: {type: JsonSchemaType.String}}, + }; + + expect(getSchemaByPointer(schema, '#/properties/missing')).toBeUndefined(); + }); +}); + +describe('getValuePaths', () => { + test('returns leaf paths of a nested object', () => { + expect(getValuePaths({properties: {a: {type: JsonSchemaType.String}}})).toEqual([ + ['properties', 'a', 'type'], + ]); + }); + + test('returns index paths of an array', () => { + expect(getValuePaths([1, {type: JsonSchemaType.String}])).toEqual([['0'], ['1', 'type']]); + }); + + test('returns an empty array for a root primitive, empty object, or empty array', () => { + expect(getValuePaths('x')).toEqual([]); + expect(getValuePaths({})).toEqual([]); + expect(getValuePaths([])).toEqual([]); + }); + + test('treats null as a leaf', () => { + expect(getValuePaths({a: null})).toEqual([['a']]); + }); +}); + +describe('getServiceFieldName', () => { + test('joins the service field with a head name', () => { + expect(getServiceFieldName('SCHEMA_RENDERER_SERVICE_FIELD', 'form')).toBe( + 'SCHEMA_RENDERER_SERVICE_FIELD.form', + ); + }); + + test('returns the service field when the head name is empty', () => { + expect(getServiceFieldName('SCHEMA_RENDERER_SERVICE_FIELD', '')).toBe( + 'SCHEMA_RENDERER_SERVICE_FIELD', + ); + }); +}); diff --git a/src/lib/unstable/kit/components/CopyButton/CopyButton.scss b/src/lib/unstable/kit/components/CopyButton/CopyButton.scss new file mode 100644 index 00000000..b940bebf --- /dev/null +++ b/src/lib/unstable/kit/components/CopyButton/CopyButton.scss @@ -0,0 +1,12 @@ +@import '../../styles/variables.scss'; + +.#{$ns}copy-button { + width: 20px; + height: 20px; + margin-top: -1px; + + .g-button { + margin-top: -2px; + margin-left: -2px; + } +} diff --git a/src/lib/unstable/kit/components/CopyButton/CopyButton.tsx b/src/lib/unstable/kit/components/CopyButton/CopyButton.tsx new file mode 100644 index 00000000..7c28d2ed --- /dev/null +++ b/src/lib/unstable/kit/components/CopyButton/CopyButton.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import {ClipboardButton} from '@gravity-ui/uikit'; +import {isNumber, isString} from 'lodash'; + +import {block} from '../../utils'; + +import './CopyButton.scss'; + +const b = block('copy-button'); + +export interface CopyButtonProps { + className?: string; + copy?: boolean; + value: unknown; +} + +const CopyButtonComponent: React.FC = ({className, copy, value}) => { + if (copy && (isString(value) || isNumber(value))) { + return ( +
+ +
+ ); + } + + return null; +}; + +export const CopyButton = React.memo(CopyButtonComponent); diff --git a/src/lib/unstable/kit/components/CopyButton/index.ts b/src/lib/unstable/kit/components/CopyButton/index.ts new file mode 100644 index 00000000..3808133d --- /dev/null +++ b/src/lib/unstable/kit/components/CopyButton/index.ts @@ -0,0 +1 @@ +export {CopyButton, type CopyButtonProps} from './CopyButton'; diff --git a/src/lib/unstable/kit/components/EmptyEntityValue/EmptyEntityValue.tsx b/src/lib/unstable/kit/components/EmptyEntityValue/EmptyEntityValue.tsx new file mode 100644 index 00000000..6c3ea40e --- /dev/null +++ b/src/lib/unstable/kit/components/EmptyEntityValue/EmptyEntityValue.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import {DASH} from '../../constants'; +import {EntityContainer} from '../EntityContainer'; + +const EmptyEntityValueComponent: React.FC = () => { + return ( + + {DASH} + + ); +}; + +export const EmptyEntityValue = React.memo(EmptyEntityValueComponent); diff --git a/src/lib/unstable/kit/components/EmptyEntityValue/index.ts b/src/lib/unstable/kit/components/EmptyEntityValue/index.ts new file mode 100644 index 00000000..f3bdd870 --- /dev/null +++ b/src/lib/unstable/kit/components/EmptyEntityValue/index.ts @@ -0,0 +1 @@ +export {EmptyEntityValue} from './EmptyEntityValue'; diff --git a/src/lib/unstable/kit/components/EntityContainer/EntityContainer.scss b/src/lib/unstable/kit/components/EntityContainer/EntityContainer.scss index 65a3f6bb..8624dd8d 100644 --- a/src/lib/unstable/kit/components/EntityContainer/EntityContainer.scss +++ b/src/lib/unstable/kit/components/EntityContainer/EntityContainer.scss @@ -12,7 +12,9 @@ &_stretch_fit { width: fit-content; - flex: 0 0 auto; + max-width: 100%; + min-width: 0; + flex: 0 1 auto; } &_stretch_by-child { diff --git a/src/lib/unstable/kit/components/EntityContainer/EntityContainer.tsx b/src/lib/unstable/kit/components/EntityContainer/EntityContainer.tsx index ac32d658..825967c9 100644 --- a/src/lib/unstable/kit/components/EntityContainer/EntityContainer.tsx +++ b/src/lib/unstable/kit/components/EntityContainer/EntityContainer.tsx @@ -9,18 +9,20 @@ import './EntityContainer.scss'; const b = block('entity-container'); export interface EntityContainerProps extends FlexProps { - stretch: 'max' | 'fit' | 'by-child'; children: React.ReactNode; + fill?: 'populated' | 'empty' | 'by-child'; + stretch: 'max' | 'fit' | 'by-child'; } const EntityContainerComponent: React.FC = ({ - stretch, children, + fill, + stretch, ...restFlexProps }) => { return ( -
- +
+ {children}
diff --git a/src/lib/unstable/kit/components/LayoutContainer/LayoutContainer.scss b/src/lib/unstable/kit/components/LayoutContainer/LayoutContainer.scss index b84c20cc..2db5f5ee 100644 --- a/src/lib/unstable/kit/components/LayoutContainer/LayoutContainer.scss +++ b/src/lib/unstable/kit/components/LayoutContainer/LayoutContainer.scss @@ -9,4 +9,10 @@ width: 100%; flex: 1 0 0; } + + &_hide-empty { + &:not(:has([data-fill='populated'])) { + display: none; + } + } } diff --git a/src/lib/unstable/kit/components/LayoutContainer/LayoutContainer.tsx b/src/lib/unstable/kit/components/LayoutContainer/LayoutContainer.tsx index 6a7161ff..ed6fd587 100644 --- a/src/lib/unstable/kit/components/LayoutContainer/LayoutContainer.tsx +++ b/src/lib/unstable/kit/components/LayoutContainer/LayoutContainer.tsx @@ -10,15 +10,17 @@ const b = block('layout-container'); export interface LayoutContainerProps extends FlexProps { children: React.ReactNode; + hideEmpty?: boolean; } const LayoutContainerComponent: React.FC = ({ className, children, + hideEmpty = false, ...restProps }) => { return ( - + {children} ); diff --git a/src/lib/unstable/kit/components/LongValue/LongValue.scss b/src/lib/unstable/kit/components/LongValue/LongValue.scss new file mode 100644 index 00000000..8088f7b8 --- /dev/null +++ b/src/lib/unstable/kit/components/LongValue/LongValue.scss @@ -0,0 +1,25 @@ +@import '../../styles/variables.scss'; + +.#{$ns}long-value { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &_open { + white-space: pre-wrap; + overflow-wrap: break-word; + } + + &_long { + cursor: pointer; + + &:hover { + color: var(--g-color-text-secondary); + } + + &.#{$ns}long-value_secondary:hover { + color: var(--g-color-text-primary); + } + } +} diff --git a/src/lib/unstable/kit/components/LongValue/LongValue.tsx b/src/lib/unstable/kit/components/LongValue/LongValue.tsx new file mode 100644 index 00000000..baec8ddd --- /dev/null +++ b/src/lib/unstable/kit/components/LongValue/LongValue.tsx @@ -0,0 +1,81 @@ +import React from 'react'; + +import {Text, type TextProps} from '@gravity-ui/uikit'; + +import {block} from '../../utils'; + +import './LongValue.scss'; + +const b = block('long-value'); + +export interface LongValueProps extends Omit { + onClick?: (event: React.MouseEvent) => void; + value?: string | number | boolean; +} + +const LongValueComponent: React.FC = ({ + className, + color, + onClick, + value, + ...restProps +}) => { + const ref = React.useRef(null); + const openRef = React.useRef(false); + const [open, setOpen] = React.useState(false); + const [long, setLong] = React.useState(false); + + const handleClick = React.useCallback( + (event: React.MouseEvent) => { + onClick?.(event); + setOpen((current) => { + openRef.current = !current; + + return !current; + }); + }, + [onClick], + ); + + React.useLayoutEffect(() => { + const node = ref.current; + + if (!node) { + return undefined; + } + + openRef.current = false; + setOpen(false); + + const measure = () => { + if (openRef.current || (node.offsetWidth === 0 && node.scrollWidth === 0)) { + return; + } + + setLong(node.scrollWidth > node.offsetWidth); + }; + + measure(); + + const observer = new ResizeObserver(measure); + + observer.observe(node); + + return () => observer.disconnect(); + }, [value]); + + return ( + + {value} + + ); +}; + +export const LongValue = React.memo(LongValueComponent); diff --git a/src/lib/unstable/kit/components/LongValue/index.ts b/src/lib/unstable/kit/components/LongValue/index.ts new file mode 100644 index 00000000..86926973 --- /dev/null +++ b/src/lib/unstable/kit/components/LongValue/index.ts @@ -0,0 +1 @@ +export {LongValue, type LongValueProps} from './LongValue'; diff --git a/src/lib/unstable/kit/components/index.ts b/src/lib/unstable/kit/components/index.ts index 49b2e635..9aa60e5b 100644 --- a/src/lib/unstable/kit/components/index.ts +++ b/src/lib/unstable/kit/components/index.ts @@ -1,5 +1,8 @@ export {ArrayRemoveButton, type ArrayRemoveButtonProps} from './ArrayRemoveButton'; +export {CopyButton, type CopyButtonProps} from './CopyButton'; +export {EmptyEntityValue} from './EmptyEntityValue'; export {EntityContainer, type EntityContainerProps} from './EntityContainer'; export {EntityError, type EntityErrorProps} from './EntityError'; export {HTMLContent, type HTMLContentProps} from './HTMLContent'; export {LayoutContainer, type LayoutContainerProps} from './LayoutContainer'; +export {LongValue, type LongValueProps} from './LongValue'; diff --git a/src/lib/unstable/kit/constants/common.ts b/src/lib/unstable/kit/constants/common.ts new file mode 100644 index 00000000..9759570b --- /dev/null +++ b/src/lib/unstable/kit/constants/common.ts @@ -0,0 +1 @@ +export const DASH = '—'; diff --git a/src/lib/unstable/kit/constants/config.ts b/src/lib/unstable/kit/constants/config.ts index 5106d6ec..0642a9a5 100644 --- a/src/lib/unstable/kit/constants/config.ts +++ b/src/lib/unstable/kit/constants/config.ts @@ -1,9 +1,6 @@ import {NodeType, type NodesConfig} from '../../core'; -import {Monaco, ObjectBase} from '../entities'; +import {Alert, ArrayBase, ArrayTable, Monaco, ObjectBase} from '../entities'; import { - Alert, - ArrayBase, - ArrayTable, Checkbox, CheckboxGroup, ColorPicker, @@ -31,6 +28,8 @@ import { TextContent, } from '../form-entities'; import {Accordeon, Card, ColumnRow, Row, Section, Transparent} from '../form-layouts'; +import {OverviewBooleanBase, OverviewStringBase} from '../overview-entities'; +import {OverviewRow} from '../overview-layouts'; export const untypedConfig = { [NodeType.Any]: { @@ -46,7 +45,9 @@ export const untypedConfig = { card: {Component: Card}, }, overviewEntities: {}, - overviewLayouts: {}, + overviewLayouts: { + row: {Component: OverviewRow}, + }, validators: {}, }, [NodeType.Array]: { @@ -64,8 +65,13 @@ export const untypedConfig = { accordeon: {Component: Accordeon}, card: {Component: Card}, }, - overviewEntities: {}, - overviewLayouts: {}, + overviewEntities: { + base: {Component: ArrayBase}, + table: {Component: ArrayTable}, + }, + overviewLayouts: { + row: {Component: OverviewRow}, + }, validators: {}, }, [NodeType.Boolean]: { @@ -77,8 +83,13 @@ export const untypedConfig = { transparent: {Component: Transparent}, card: {Component: Card}, }, - overviewEntities: {}, - overviewLayouts: {}, + overviewEntities: { + base: {Component: OverviewBooleanBase}, + switch: {Component: OverviewBooleanBase}, + }, + overviewLayouts: { + row: {Component: OverviewRow}, + }, validators: {}, }, [NodeType.Number]: { @@ -96,7 +107,9 @@ export const untypedConfig = { card: {Component: Card}, }, overviewEntities: {}, - overviewLayouts: {}, + overviewLayouts: { + row: {Component: OverviewRow}, + }, validators: {}, }, [NodeType.Object]: { @@ -120,7 +133,9 @@ export const untypedConfig = { overviewEntities: { base: {Component: ObjectBase}, }, - overviewLayouts: {}, + overviewLayouts: { + row: {Component: OverviewRow}, + }, validators: {}, }, [NodeType.String]: { @@ -148,9 +163,23 @@ export const untypedConfig = { card: {Component: Card}, }, overviewEntities: { + base: {Component: OverviewStringBase}, + color_picker: {Component: OverviewStringBase}, + // file: {Component: OverviewStringBase}, + password: {Component: OverviewStringBase}, + radio_group: {Component: OverviewStringBase}, + segmented_radio_group: {Component: OverviewStringBase}, + select: {Component: OverviewStringBase}, + textarea: {Component: OverviewStringBase}, + // text_content: {Component: OverviewStringBase}, + // label: {Component: OverviewStringBase}, + alert: {Component: Alert}, + // string_number_with_scale: {Component: OverviewStringBase}, monaco: {Component: Monaco}, }, - overviewLayouts: {}, + overviewLayouts: { + row: {Component: OverviewRow}, + }, validators: {}, }, } as const; diff --git a/src/lib/unstable/kit/constants/index.ts b/src/lib/unstable/kit/constants/index.ts index 93d0b36d..e4644b6a 100644 --- a/src/lib/unstable/kit/constants/index.ts +++ b/src/lib/unstable/kit/constants/index.ts @@ -1 +1,2 @@ +export {DASH} from './common'; export {config, untypedConfig} from './config'; diff --git a/src/lib/unstable/kit/form-entities/Alert/Alert.tsx b/src/lib/unstable/kit/entities/Alert/Alert.tsx similarity index 96% rename from src/lib/unstable/kit/form-entities/Alert/Alert.tsx rename to src/lib/unstable/kit/entities/Alert/Alert.tsx index 0bf11b71..f65f5b0e 100644 --- a/src/lib/unstable/kit/form-entities/Alert/Alert.tsx +++ b/src/lib/unstable/kit/entities/Alert/Alert.tsx @@ -44,7 +44,7 @@ const AlertComponent: NodeEntity = ({props, schema }, [message, schema.description]); return ( - + ); diff --git a/src/lib/unstable/kit/form-entities/Alert/index.ts b/src/lib/unstable/kit/entities/Alert/index.ts similarity index 100% rename from src/lib/unstable/kit/form-entities/Alert/index.ts rename to src/lib/unstable/kit/entities/Alert/index.ts diff --git a/src/lib/unstable/kit/form-entities/ArrayBase/ArrayBase.tsx b/src/lib/unstable/kit/entities/ArrayBase/ArrayBase.tsx similarity index 81% rename from src/lib/unstable/kit/form-entities/ArrayBase/ArrayBase.tsx rename to src/lib/unstable/kit/entities/ArrayBase/ArrayBase.tsx index e827742a..723a5d27 100644 --- a/src/lib/unstable/kit/form-entities/ArrayBase/ArrayBase.tsx +++ b/src/lib/unstable/kit/entities/ArrayBase/ArrayBase.tsx @@ -3,8 +3,13 @@ import React from 'react'; import {Plus} from '@gravity-ui/icons'; import {Button, Flex, Icon} from '@gravity-ui/uikit'; -import {type JsonSchemaArray, type NodeEntity, SchemaRendererNode} from '../../../core'; -import {EntityContainer} from '../../components'; +import { + type JsonSchemaArray, + type NodeEntity, + SchemaRendererMode, + SchemaRendererNode, +} from '../../../core'; +import {EmptyEntityValue, EntityContainer} from '../../components'; export interface ArrayBaseProps { addButtonText?: string; @@ -15,16 +20,19 @@ export interface ArrayBaseProps { const ArrayBaseComponent: NodeEntity = ({ headName, input, + mode, props, schema, schemaPath, }) => { const {name, onBlur, onChange, onFocus, value} = input; + const overviewFlag = mode === SchemaRendererMode.Overview; + const addButton = React.useMemo(() => { const itemsSchema = schema.items; - if (Array.isArray(itemsSchema)) { + if (Array.isArray(itemsSchema) || overviewFlag) { return null; } @@ -51,6 +59,7 @@ const ArrayBaseComponent: NodeEntity = ({ onBlur, onChange, onFocus, + overviewFlag, schema.items, schema.readOnly, value, @@ -82,8 +91,12 @@ const ArrayBaseComponent: NodeEntity = ({ )); }, [headName, name, schema.items, schemaPath, value?.length]); + if (overviewFlag && !value?.length) { + return ; + } + return ( - + {items} {addButton} diff --git a/src/lib/unstable/kit/form-entities/ArrayBase/index.ts b/src/lib/unstable/kit/entities/ArrayBase/index.ts similarity index 100% rename from src/lib/unstable/kit/form-entities/ArrayBase/index.ts rename to src/lib/unstable/kit/entities/ArrayBase/index.ts diff --git a/src/lib/unstable/kit/form-entities/ArrayTable/ArrayTable.scss b/src/lib/unstable/kit/entities/ArrayTable/ArrayTable.scss similarity index 100% rename from src/lib/unstable/kit/form-entities/ArrayTable/ArrayTable.scss rename to src/lib/unstable/kit/entities/ArrayTable/ArrayTable.scss diff --git a/src/lib/unstable/kit/form-entities/ArrayTable/ArrayTable.tsx b/src/lib/unstable/kit/entities/ArrayTable/ArrayTable.tsx similarity index 90% rename from src/lib/unstable/kit/form-entities/ArrayTable/ArrayTable.tsx rename to src/lib/unstable/kit/entities/ArrayTable/ArrayTable.tsx index 61b35b47..81342483 100644 --- a/src/lib/unstable/kit/form-entities/ArrayTable/ArrayTable.tsx +++ b/src/lib/unstable/kit/entities/ArrayTable/ArrayTable.tsx @@ -7,9 +7,10 @@ import { type JsonSchema, type JsonSchemaArray, type NodeEntity, + SchemaRendererMode, SchemaRendererNode, } from '../../../core'; -import {ArrayRemoveButton, EntityContainer} from '../../components'; +import {ArrayRemoveButton, EmptyEntityValue, EntityContainer} from '../../components'; import {block} from '../../utils'; import './ArrayTable.scss'; @@ -25,16 +26,19 @@ export interface ArrayTableProps { const ArrayTableComponent: NodeEntity = ({ headName, input, + mode, props, schema, schemaPath, }) => { const {name, onBlur, onChange, onFocus, value} = input; + const overviewFlag = mode === SchemaRendererMode.Overview; + const addButton = React.useMemo(() => { const itemsSchema = schema.items; - if (Array.isArray(itemsSchema)) { + if (Array.isArray(itemsSchema) || overviewFlag) { return null; } @@ -61,6 +65,7 @@ const ArrayTableComponent: NodeEntity = ({ onBlur, onChange, onFocus, + overviewFlag, schema.items, schema.readOnly, value, @@ -109,6 +114,10 @@ const ArrayTableComponent: NodeEntity = ({ withRemoveButton = false; } + if (overviewFlag) { + withRemoveButton = false; + } + const head = (
= ({ )); return {head, rows}; - }, [columns, headName, name, schema.items, schemaPath, value?.length]); + }, [columns, headName, name, overviewFlag, schema.items, schemaPath, value?.length]); + + if (overviewFlag && !value?.length) { + return ; + } return ( - +
{head} {rows} diff --git a/src/lib/unstable/kit/form-entities/ArrayTable/index.ts b/src/lib/unstable/kit/entities/ArrayTable/index.ts similarity index 100% rename from src/lib/unstable/kit/form-entities/ArrayTable/index.ts rename to src/lib/unstable/kit/entities/ArrayTable/index.ts diff --git a/src/lib/unstable/kit/entities/Monaco/Monaco.tsx b/src/lib/unstable/kit/entities/Monaco/Monaco.tsx index ea58a30d..a141e8da 100644 --- a/src/lib/unstable/kit/entities/Monaco/Monaco.tsx +++ b/src/lib/unstable/kit/entities/Monaco/Monaco.tsx @@ -12,7 +12,7 @@ import { useSchemaRendererState, useSchemaRendererTools, } from '../../../core'; -import {EntityContainer} from '../../components'; +import {EmptyEntityValue, EntityContainer} from '../../components'; import {block, getBooleanValidationState} from '../../utils'; import './Monaco.scss'; @@ -35,6 +35,8 @@ const MonacoComponent: NodeEntity = ({ }) => { const {name, onBlur, onChange, onFocus, value} = input; + const overviewFlag = mode === SchemaRendererMode.Overview; + const { language = 'plaintext', options: optionsProps, @@ -61,7 +63,7 @@ const MonacoComponent: NodeEntity = ({ const options: MonacoEditorProps['options'] = React.useMemo( () => ({ fontSize: 12, - readOnly: mode === SchemaRendererMode.Overview || schema.readOnly, + readOnly: overviewFlag || schema.readOnly, formatOnPaste: true, formatOnType: true, contextmenu: false, @@ -70,7 +72,7 @@ const MonacoComponent: NodeEntity = ({ automaticLayout: true, ...optionsProps, }), - [mode, optionsProps, schema.readOnly], + [optionsProps, overviewFlag, schema.readOnly], ); const onUpdate = React.useCallback( @@ -171,6 +173,10 @@ const MonacoComponent: NodeEntity = ({ return null; } + if (overviewFlag && !value) { + return ; + } + const control = ( = ({ ); return ( - + {control} diff --git a/src/lib/unstable/kit/entities/ObjectBase/ObjectBase.tsx b/src/lib/unstable/kit/entities/ObjectBase/ObjectBase.tsx index 00410ad8..dad81f03 100644 --- a/src/lib/unstable/kit/entities/ObjectBase/ObjectBase.tsx +++ b/src/lib/unstable/kit/entities/ObjectBase/ObjectBase.tsx @@ -1,7 +1,12 @@ import React from 'react'; -import {type JsonSchemaObject, type NodeEntity, SchemaRendererNode} from '../../../core'; -import {EntityContainer} from '../../components'; +import { + type JsonSchemaObject, + type NodeEntity, + SchemaRendererMode, + SchemaRendererNode, +} from '../../../core'; +import {EmptyEntityValue, EntityContainer} from '../../components'; export interface ObjectBaseProps { disabled?: boolean; @@ -10,6 +15,7 @@ export interface ObjectBaseProps { const ObjectBaseComponent: NodeEntity = ({ headName, + mode, input, props, schema, @@ -17,8 +23,14 @@ const ObjectBaseComponent: NodeEntity = ({ }) => { const {name} = input; + const overviewFlag = mode === SchemaRendererMode.Overview; + + if (overviewFlag && !Object.keys(schema.properties || {}).length) { + return ; + } + return ( - + {(props.order || Object.keys(schema.properties || {})).map((property: string) => ( = ({ ))} diff --git a/src/lib/unstable/kit/form-entities/OneOfNested/OneOfNested.tsx b/src/lib/unstable/kit/form-entities/OneOfNested/OneOfNested.tsx index 7dc4fb5a..8c4b901d 100644 --- a/src/lib/unstable/kit/form-entities/OneOfNested/OneOfNested.tsx +++ b/src/lib/unstable/kit/form-entities/OneOfNested/OneOfNested.tsx @@ -140,7 +140,7 @@ const OneOfNestedComponent: NodeEntity = ({
diff --git a/src/lib/unstable/kit/form-entities/RangeInput/RangeInput.tsx b/src/lib/unstable/kit/form-entities/RangeInput/RangeInput.tsx index 039189a5..9555d7c3 100644 --- a/src/lib/unstable/kit/form-entities/RangeInput/RangeInput.tsx +++ b/src/lib/unstable/kit/form-entities/RangeInput/RangeInput.tsx @@ -36,8 +36,8 @@ const RangeInputComponent: NodeEntity = ({ const {addSchemaPatches, removeSchemaPatches} = useSchemaRendererTools(); const [fromKey, toKey] = propertyKeys || ['from', 'to']; - const fromName = `${name}.${fromKey}`; - const toName = `${name}.${toKey}`; + const fromName = `${name ? name + '.' : ''}${fromKey}`; + const toName = `${name ? name + '.' : ''}${toKey}`; React.useEffect(() => { const patches: SchemaPatch[] = []; diff --git a/src/lib/unstable/kit/form-entities/index.ts b/src/lib/unstable/kit/form-entities/index.ts index c4539993..7cde6f2c 100644 --- a/src/lib/unstable/kit/form-entities/index.ts +++ b/src/lib/unstable/kit/form-entities/index.ts @@ -1,6 +1,3 @@ -export {Alert, type AlertProps} from './Alert'; -export {ArrayBase, type ArrayBaseProps} from './ArrayBase'; -export {ArrayTable, type ArrayTableProps} from './ArrayTable'; export {Checkbox, type CheckboxProps} from './Checkbox'; export {CheckboxGroup, type CheckboxGroupProps} from './CheckboxGroup'; export {ColorPicker, type ColorPickerProps} from './ColorPicker'; diff --git a/src/lib/unstable/kit/index.ts b/src/lib/unstable/kit/index.ts index 8fc5358a..df073aaf 100644 --- a/src/lib/unstable/kit/index.ts +++ b/src/lib/unstable/kit/index.ts @@ -3,6 +3,8 @@ export * from './constants'; export * from './entities'; export * from './form-entities'; export * from './form-layouts'; +export * from './overview-entities'; +export * from './overview-layouts'; export { getArrayItemIndex, getArrayItemParentName, diff --git a/src/lib/unstable/kit/overview-entities/OverviewBooleanBase/OverviewBooleanBase.tsx b/src/lib/unstable/kit/overview-entities/OverviewBooleanBase/OverviewBooleanBase.tsx new file mode 100644 index 00000000..17b1e7e9 --- /dev/null +++ b/src/lib/unstable/kit/overview-entities/OverviewBooleanBase/OverviewBooleanBase.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import type {TextProps} from '@gravity-ui/uikit'; +import {isBoolean} from 'lodash'; + +import type {JsonSchemaBoolean, NodeEntity} from '../../../core'; +import {EmptyEntityValue, EntityContainer, LongValue, type LongValueProps} from '../../components'; + +export interface OverviewBooleanBaseProps extends Omit { + valueDescriptions?: { + true?: string; + false?: string; + }; + viewColor?: { + true?: TextProps['color']; + false?: TextProps['color']; + }; +} + +const OverviewBooleanBaseComponent: NodeEntity = ({ + input, + props, +}) => { + const {valueDescriptions, viewColor, ...entityRestProps} = props; + + if (!isBoolean(input.value)) { + return ; + } + + const stringValue = String(input.value) as 'true' | 'false'; + let value: string = stringValue; + let color = entityRestProps.color; + + if (valueDescriptions?.[stringValue]) { + value = valueDescriptions[stringValue]; + } + + if (viewColor?.[stringValue]) { + color = viewColor[stringValue]; + } + + return ( + + + + ); +}; + +export const OverviewBooleanBase = React.memo(OverviewBooleanBaseComponent); diff --git a/src/lib/unstable/kit/overview-entities/OverviewBooleanBase/index.ts b/src/lib/unstable/kit/overview-entities/OverviewBooleanBase/index.ts new file mode 100644 index 00000000..9d486b80 --- /dev/null +++ b/src/lib/unstable/kit/overview-entities/OverviewBooleanBase/index.ts @@ -0,0 +1 @@ +export {OverviewBooleanBase, type OverviewBooleanBaseProps} from './OverviewBooleanBase'; diff --git a/src/lib/unstable/kit/overview-entities/OverviewStringBase/OverviewStringBase.tsx b/src/lib/unstable/kit/overview-entities/OverviewStringBase/OverviewStringBase.tsx new file mode 100644 index 00000000..10ca4eab --- /dev/null +++ b/src/lib/unstable/kit/overview-entities/OverviewStringBase/OverviewStringBase.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import type {JsonSchemaString, NodeEntity} from '../../../core'; +import {EmptyEntityValue, EntityContainer, LongValue, type LongValueProps} from '../../components'; + +export interface OverviewStringBaseProps extends Omit {} + +const OverviewStringBaseComponent: NodeEntity = ({ + input, + props, +}) => { + if (!input.value) { + return ; + } + + return ( + + + + ); +}; + +export const OverviewStringBase = React.memo(OverviewStringBaseComponent); diff --git a/src/lib/unstable/kit/overview-entities/OverviewStringBase/index.ts b/src/lib/unstable/kit/overview-entities/OverviewStringBase/index.ts new file mode 100644 index 00000000..80ff76b8 --- /dev/null +++ b/src/lib/unstable/kit/overview-entities/OverviewStringBase/index.ts @@ -0,0 +1 @@ +export {OverviewStringBase, type OverviewStringBaseProps} from './OverviewStringBase'; diff --git a/src/lib/unstable/kit/overview-entities/index.ts b/src/lib/unstable/kit/overview-entities/index.ts new file mode 100644 index 00000000..7e8cf618 --- /dev/null +++ b/src/lib/unstable/kit/overview-entities/index.ts @@ -0,0 +1,2 @@ +export {OverviewBooleanBase, type OverviewBooleanBaseProps} from './OverviewBooleanBase'; +export {OverviewStringBase, type OverviewStringBaseProps} from './OverviewStringBase'; diff --git a/src/lib/unstable/kit/overview-layouts/OverviewRow/OverviewRow.scss b/src/lib/unstable/kit/overview-layouts/OverviewRow/OverviewRow.scss new file mode 100644 index 00000000..8acb763f --- /dev/null +++ b/src/lib/unstable/kit/overview-layouts/OverviewRow/OverviewRow.scss @@ -0,0 +1,50 @@ +@import '../../styles/functions.scss'; +@import '../../styles/variables.scss'; + +.#{$ns}overview-row { + max-width: 100%; + margin-bottom: spacing(4); + + &:last-child { + margin-bottom: 0; + } + + &:hover { + .#{$ns}overview-row__copy-button { + display: block; + } + } + + &__left { + width: 300px; + flex-shrink: 0; + min-height: 20px; + display: flex; + align-items: baseline; + } + + &__title { + min-height: 18px; + } + + &__help-mark { + min-width: 16px; + margin-left: spacing(1); + margin-top: 1.5px; + } + + &__dots { + min-width: 25px; + flex-grow: 1; + margin: 0 spacing(0.5); + border-bottom: 1px dotted var(--g-color-text-secondary); + } + + &__right { + min-width: 0; + } + + &__copy-button { + display: none; + } +} diff --git a/src/lib/unstable/kit/overview-layouts/OverviewRow/OverviewRow.tsx b/src/lib/unstable/kit/overview-layouts/OverviewRow/OverviewRow.tsx new file mode 100644 index 00000000..bba873eb --- /dev/null +++ b/src/lib/unstable/kit/overview-layouts/OverviewRow/OverviewRow.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import {Flex, HelpMark, Text} from '@gravity-ui/uikit'; + +import type {JsonSchema, NodeLayout} from '../../../core'; +import {CopyButton, HTMLContent, LayoutContainer} from '../../components'; +import {block} from '../../utils'; + +import './OverviewRow.scss'; + +const b = block('overview-row'); + +const Component: NodeLayout = ({children, input, schema, props}) => { + const tooltip = React.useMemo(() => { + if (!schema.description) { + return null; + } + + return ( + + + + ); + }, [schema.description]); + + return ( + +
+ + {schema.title} + + {tooltip} +
+
+ + + {children} + + + + + ); +}; + +export const OverviewRow = React.memo(Component); diff --git a/src/lib/unstable/kit/overview-layouts/OverviewRow/index.ts b/src/lib/unstable/kit/overview-layouts/OverviewRow/index.ts new file mode 100644 index 00000000..082e6871 --- /dev/null +++ b/src/lib/unstable/kit/overview-layouts/OverviewRow/index.ts @@ -0,0 +1 @@ +export {OverviewRow} from './OverviewRow'; diff --git a/src/lib/unstable/kit/overview-layouts/index.ts b/src/lib/unstable/kit/overview-layouts/index.ts new file mode 100644 index 00000000..082e6871 --- /dev/null +++ b/src/lib/unstable/kit/overview-layouts/index.ts @@ -0,0 +1 @@ +export {OverviewRow} from './OverviewRow'; diff --git a/src/lib/unstable/kit/utils/__tests__/common.test.ts b/src/lib/unstable/kit/utils/__tests__/common.test.ts new file mode 100644 index 00000000..f47a9dfa --- /dev/null +++ b/src/lib/unstable/kit/utils/__tests__/common.test.ts @@ -0,0 +1,182 @@ +import {createForm} from 'final-form'; + +import { + JsonSchemaType, + SCHEMA_RENDERER_SERVICE_FIELD, + type SchemaRendererState, + getServiceFieldName, +} from '../../../core'; +import { + getArrayItemIndex, + getArrayItemParentName, + getBooleanValidationState, + getValidationState, + isArrayItem, + isStringFloat, + isStringInt, + isStringNumber, + isTupleItem, +} from '../common'; + +describe('getValidationState', () => { + test('returns invalid when the field is touched and has an error', () => { + expect(getValidationState({touched: true, error: 'error'} as any)).toBe('invalid'); + }); + + test('returns invalid when submit failed and the field has an error', () => { + expect(getValidationState({submitFailed: true, error: 'error'} as any)).toBe('invalid'); + }); + + test('returns undefined when the field is not touched and submit did not fail', () => { + expect(getValidationState({error: 'error'} as any)).toBeUndefined(); + }); + + test('returns undefined when there is no error', () => { + expect(getValidationState({touched: true} as any)).toBeUndefined(); + }); +}); + +describe('getBooleanValidationState', () => { + test('returns true when the field is invalid', () => { + expect(getBooleanValidationState({touched: true, error: 'error'} as any)).toBe(true); + }); + + test('returns false when the field is valid', () => { + expect(getBooleanValidationState({touched: true} as any)).toBe(false); + }); +}); + +describe('getArrayItemParentName', () => { + test('returns the parent name of an array item', () => { + expect(getArrayItemParentName('form.items[0]')).toBe('form.items'); + }); + + test('returns the inner array name for a nested index', () => { + expect(getArrayItemParentName('form.items[0][1]')).toBe('form.items[0]'); + }); +}); + +describe('getArrayItemIndex', () => { + test('returns the last bracket index', () => { + expect(getArrayItemIndex('form.items[0]')).toBe('0'); + expect(getArrayItemIndex('form.items[0][1]')).toBe('1'); + }); +}); + +describe('isArrayItem', () => { + test('returns true when the name ends with a bracket index', () => { + expect(isArrayItem('form.items[0]')).toBe(true); + }); + + test('returns false when the name is not an array item', () => { + expect(isArrayItem('form.items')).toBe(false); + }); +}); + +describe('isTupleItem', () => { + test('returns false when the name is not an array item', () => { + const form = createForm({onSubmit: () => {}}); + + expect(isTupleItem('form.items', 'form', form)).toBe(false); + }); + + test('returns true when the parent schema items is a tuple', () => { + const form = createForm({onSubmit: () => {}}); + const state = { + schema: { + type: JsonSchemaType.Object, + properties: { + items: { + type: JsonSchemaType.Array, + items: [{type: JsonSchemaType.String}, {type: JsonSchemaType.Number}], + }, + }, + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + form.registerField( + 'form.items', + () => {}, + {}, + {data: {state: {schemaPath: '#/properties/items'}}}, + ); + + expect(isTupleItem('form.items[0]', 'form', form)).toBe(true); + }); + + test('returns false when the parent schema items is not a tuple', () => { + const form = createForm({onSubmit: () => {}}); + const state = { + schema: { + type: JsonSchemaType.Object, + properties: { + items: { + type: JsonSchemaType.Array, + items: {type: JsonSchemaType.String}, + }, + }, + }, + } as unknown as SchemaRendererState; + + form.registerField( + getServiceFieldName(SCHEMA_RENDERER_SERVICE_FIELD, 'form'), + () => {}, + {}, + {data: {state}}, + ); + form.registerField( + 'form.items', + () => {}, + {}, + {data: {state: {schemaPath: '#/properties/items'}}}, + ); + + expect(isTupleItem('form.items[0]', 'form', form)).toBe(false); + }); +}); + +describe('isStringInt', () => { + test('accepts integer strings', () => { + expect(isStringInt('0')).toBe(true); + expect(isStringInt('-12')).toBe(true); + expect(isStringInt(42)).toBe(true); + }); + + test('rejects non-integers and leading zeros', () => { + expect(isStringInt('01')).toBe(false); + expect(isStringInt('1.5')).toBe(false); + expect(isStringInt('')).toBe(false); + }); +}); + +describe('isStringFloat', () => { + test('accepts integer and decimal strings', () => { + expect(isStringFloat('0')).toBe(true); + expect(isStringFloat('8')).toBe(true); + expect(isStringFloat('1.5')).toBe(true); + expect(isStringFloat('-0.25')).toBe(true); + }); + + test('rejects invalid numeric strings', () => { + expect(isStringFloat('.5')).toBe(false); + expect(isStringFloat('1.')).toBe(false); + expect(isStringFloat('01.2')).toBe(false); + }); +}); + +describe('isStringNumber', () => { + test('accepts integer and decimal strings', () => { + expect(isStringNumber('12')).toBe(true); + expect(isStringNumber('1.5')).toBe(true); + }); + + test('rejects non-numeric strings', () => { + expect(isStringNumber('abc')).toBe(false); + }); +}); diff --git a/src/stories/Unstable.stories.tsx b/src/stories/Unstable.stories.tsx index 5caa57ff..16ec775f 100644 --- a/src/stories/Unstable.stories.tsx +++ b/src/stories/Unstable.stories.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {Button} from '@gravity-ui/uikit'; +import {Button, SegmentedRadioGroup} from '@gravity-ui/uikit'; import type {StoryFn} from '@storybook/react'; import {noop} from 'lodash'; import {Form} from 'react-final-form'; @@ -30,13 +30,13 @@ const schema: JsonSchemaObject = { nodeParameters: defineNodeParameters({ entity: 'textarea', type: NodeType.String, - formEntityProps: { + entityProps: { rows: 10, }, - formEntity: 'textarea', - formLayout: 'row', - formLayoutProps: { + layout: 'row', + layoutProps: { descriptionType: 'tooltip', + copy: true, }, }), }, @@ -70,9 +70,9 @@ const schema: JsonSchemaObject = { title: 'monaco', nodeParameters: { type: NodeType.String, - formEntity: 'monaco', - formLayout: 'row', - formEntityProps: {language: 'json'}, + entity: 'monaco', + layout: 'row', + entityProps: {language: 'json'}, overviewEntity: 'monaco', overviewEntityProps: {language: 'json'}, }, @@ -89,9 +89,9 @@ const schema: JsonSchemaObject = { }, nodeParameters: { type: NodeType.String, - formEntity: 'string_number_with_scale', - formLayout: 'row', - formEntityProps: { + entity: 'string_number_with_scale', + layout: 'row', + entityProps: { scale: { km: {title: 'km', factor: '1000'}, m: {title: 'm', factor: '1'}, @@ -111,9 +111,9 @@ const schema: JsonSchemaObject = { title: 'number_with_scale', nodeParameters: defineNodeParameters({ type: NodeType.Number, - formEntity: 'number_with_scale', - formLayout: 'row', - formEntityProps: { + entity: 'number_with_scale', + layout: 'row', + entityProps: { scale: { km: {title: 'km', factor: 1000}, m: {title: 'm', factor: 1}, @@ -134,7 +134,7 @@ const schema: JsonSchemaObject = { title: 'from', nodeParameters: { type: NodeType.Number, - formEntity: 'base', + entity: 'base', formLayout: 'transparent', }, }, @@ -143,15 +143,15 @@ const schema: JsonSchemaObject = { title: 'to', nodeParameters: defineNodeParameters({ type: NodeType.Number, - formEntity: 'base', + entity: 'base', formLayout: 'transparent', }), }, }, nodeParameters: { type: NodeType.Object, - formEntity: 'range_input', - formLayout: 'row', + entity: 'range_input', + layout: 'row', }, }, object_inline: { @@ -164,7 +164,7 @@ const schema: JsonSchemaObject = { title: 'foo', nodeParameters: { type: NodeType.String, - formEntity: 'select', + entity: 'select', formLayout: 'transparent', }, }, @@ -173,7 +173,7 @@ const schema: JsonSchemaObject = { title: 'bar', nodeParameters: { type: NodeType.Number, - formEntity: 'base', + entity: 'base', formLayout: 'transparent', }, }, @@ -182,16 +182,16 @@ const schema: JsonSchemaObject = { title: 'rab', nodeParameters: { type: NodeType.Boolean, - formEntity: 'base', + entity: 'base', formLayout: 'transparent', }, }, }, nodeParameters: { type: NodeType.Object, - formEntity: 'inline', - formLayout: 'row', - formEntityProps: { + entity: 'inline', + layout: 'row', + entityProps: { delimiter: ':', }, }, @@ -212,8 +212,8 @@ const schema: JsonSchemaObject = { description: 'one_of_nested', nodeParameters: { type: NodeType.String, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, allOf: [ { @@ -227,8 +227,8 @@ const schema: JsonSchemaObject = { multipleOf: 5, nodeParameters: { type: NodeType.Number, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, rab: { @@ -236,14 +236,14 @@ const schema: JsonSchemaObject = { title: 'rab', nodeParameters: { type: NodeType.Boolean, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, }, nodeParameters: { type: NodeType.Object, - formEntity: 'base', + entity: 'base', formLayout: 'section', }, }, @@ -252,8 +252,8 @@ const schema: JsonSchemaObject = { title: 'bar', nodeParameters: { type: NodeType.Number, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, rab: { @@ -261,16 +261,16 @@ const schema: JsonSchemaObject = { title: 'rab', nodeParameters: { type: NodeType.Boolean, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, }, nodeParameters: { type: NodeType.Object, - formEntity: 'one_of_nested', + entity: 'one_of_nested', formLayout: 'transparent', - formEntityProps: { + entityProps: { booleanToKey: { true: 'foo', false: 'bar', @@ -279,8 +279,8 @@ const schema: JsonSchemaObject = { title: 'one of toggler', nodeParameters: { type: NodeType.Boolean, - formEntity: 'switch', - formLayout: 'row', + entity: 'switch', + layout: 'row', }, }, withIndent: true, @@ -302,8 +302,8 @@ const schema: JsonSchemaObject = { title: 'foo', nodeParameters: { type: NodeType.String, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, bar: { @@ -311,8 +311,8 @@ const schema: JsonSchemaObject = { title: 'bar', nodeParameters: { type: NodeType.Number, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, rab: { @@ -320,14 +320,14 @@ const schema: JsonSchemaObject = { title: 'rab', nodeParameters: { type: NodeType.Boolean, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, }, nodeParameters: { type: NodeType.Object, - formEntity: 'base', + entity: 'base', formLayout: 'section', }, }, @@ -336,8 +336,8 @@ const schema: JsonSchemaObject = { title: 'bar', nodeParameters: { type: NodeType.Number, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, rab: { @@ -345,16 +345,16 @@ const schema: JsonSchemaObject = { title: 'rab', nodeParameters: { type: NodeType.Boolean, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, }, nodeParameters: { type: NodeType.Object, - formEntity: 'few_of_nested', + entity: 'few_of_nested', formLayout: 'transparent', - formEntityProps: { + entityProps: { toggler: { title: 'few of toggler', items: { @@ -362,8 +362,8 @@ const schema: JsonSchemaObject = { }, nodeParameters: { type: NodeType.Array, - formEntity: 'select', - formLayout: 'row', + entity: 'select', + layout: 'row', }, }, }, @@ -379,13 +379,13 @@ const schema: JsonSchemaObject = { description: 'item description', nodeParameters: { type: NodeType.String, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, nodeParameters: { type: NodeType.Array, - formEntity: 'base', + entity: 'base', formLayout: 'card', }, }, @@ -400,8 +400,8 @@ const schema: JsonSchemaObject = { description: 'string item description', nodeParameters: { type: NodeType.String, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, { @@ -410,15 +410,15 @@ const schema: JsonSchemaObject = { description: 'number item description', nodeParameters: { type: NodeType.Number, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, ], nodeParameters: { type: NodeType.Array, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, array_table: { @@ -436,7 +436,7 @@ const schema: JsonSchemaObject = { description: 'name description', nodeParameters: { type: NodeType.String, - formEntity: 'base', + entity: 'base', formLayout: 'transparent', }, }, @@ -444,7 +444,7 @@ const schema: JsonSchemaObject = { title: 'surname', nodeParameters: { type: NodeType.String, - formEntity: 'base', + entity: 'base', formLayout: 'transparent', }, }, @@ -452,7 +452,7 @@ const schema: JsonSchemaObject = { title: 'age', nodeParameters: { type: NodeType.Number, - formEntity: 'base', + entity: 'base', formLayout: 'transparent', }, }, @@ -460,7 +460,7 @@ const schema: JsonSchemaObject = { title: 'item title', nodeParameters: { type: NodeType.Object, - formEntity: 'base', + entity: 'base', formLayout: 'accordeon', formLayoutProps: { withIndent: true, @@ -469,7 +469,7 @@ const schema: JsonSchemaObject = { }, nodeParameters: { type: NodeType.Array, - formEntity: 'array_table', + entity: 'array_table', formLayout: 'accordeon', formLayoutProps: { withIndent: true, @@ -482,8 +482,8 @@ const schema: JsonSchemaObject = { description: 'number description', nodeParameters: { type: NodeType.Number, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, string: { @@ -492,8 +492,8 @@ const schema: JsonSchemaObject = { description: 'string description', nodeParameters: { type: NodeType.String, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', formLayoutProps: { descriptionType: 'bottom', required: true, @@ -506,8 +506,8 @@ const schema: JsonSchemaObject = { description: 'boolean description', nodeParameters: { type: NodeType.Boolean, - formEntity: 'base', - formLayout: 'row', + entity: 'base', + layout: 'row', }, }, switch: { @@ -516,8 +516,8 @@ const schema: JsonSchemaObject = { description: 'switch description', nodeParameters: { type: NodeType.Boolean, - formEntity: 'switch', - formLayout: 'row', + entity: 'switch', + layout: 'row', }, }, alert: { @@ -526,9 +526,9 @@ const schema: JsonSchemaObject = { description: 'alert description', nodeParameters: { type: NodeType.String, - formEntity: 'alert', - formLayout: 'row', - formEntityProps: { + entity: 'alert', + layout: 'row', + entityProps: { iconName: 'CircleExclamationFill', iconProps: { size: 18, @@ -547,9 +547,9 @@ const schema: JsonSchemaObject = { description: 'label description', nodeParameters: { type: NodeType.String, - formEntity: 'label', - formLayout: 'row', - formEntityProps: { + entity: 'label', + layout: 'row', + entityProps: { iconName: 'TriangleExclamation', title: 'Label title', theme: 'clear', @@ -562,9 +562,9 @@ const schema: JsonSchemaObject = { description: 'text content description', nodeParameters: { type: NodeType.String, - formEntity: 'text_content', - formLayout: 'row', - formEntityProps: { + entity: 'text_content', + layout: 'row', + entityProps: { iconName: 'TriangleExclamation', }, }, @@ -575,8 +575,8 @@ const schema: JsonSchemaObject = { description: 'password description', nodeParameters: { type: NodeType.String, - formEntity: 'password', - formLayout: 'row', + entity: 'password', + layout: 'row', }, }, color_picker: { @@ -586,9 +586,9 @@ const schema: JsonSchemaObject = { description: 'color picker description', nodeParameters: { type: NodeType.String, - formEntity: 'color_picker', - formLayout: 'row', - formEntityProps: { + entity: 'color_picker', + layout: 'row', + entityProps: { withAlpha: true, }, }, @@ -599,8 +599,8 @@ const schema: JsonSchemaObject = { description: 'date input description', nodeParameters: { type: NodeType.Any, - formEntity: 'date_input', - formLayout: 'row', + entity: 'date_input', + layout: 'row', }, }, radio_group: { @@ -611,9 +611,9 @@ const schema: JsonSchemaObject = { description: 'radio group description', nodeParameters: { type: NodeType.String, - formEntity: 'radio_group', - formLayout: 'row', - formEntityProps: { + entity: 'radio_group', + layout: 'row', + entityProps: { direction: 'horizontal', enumDescriptions: { foo: 'Option 1', @@ -631,9 +631,9 @@ const schema: JsonSchemaObject = { description: 'segmented radio group description', nodeParameters: { type: NodeType.String, - formEntity: 'segmented_radio_group', - formLayout: 'row', - formEntityProps: { + entity: 'segmented_radio_group', + layout: 'row', + entityProps: { enumDescriptions: { foo: 'Option 1', bar: 'Option 2', @@ -651,9 +651,9 @@ const schema: JsonSchemaObject = { examples: ['Choose status'], nodeParameters: { type: NodeType.String, - formEntity: 'select', + entity: 'select', formLayout: 'accordeon', - formEntityProps: { + entityProps: { enumDescriptions: { draft: 'Draft', published: 'Published', @@ -678,9 +678,9 @@ const schema: JsonSchemaObject = { examples: ['Choose status'], nodeParameters: { type: NodeType.String, - formEntity: 'select', + entity: 'select', formLayout: 'accordeon', - formEntityProps: { + entityProps: { enumDescriptions: { draft: 'Draft', published: 'Published', @@ -697,7 +697,7 @@ const schema: JsonSchemaObject = { }, nodeParameters: { type: NodeType.Object, - formEntity: 'base', + entity: 'base', formLayout: 'transparent', }, }, @@ -711,9 +711,9 @@ const schema: JsonSchemaObject = { title: 'checkbox_group', nodeParameters: { type: NodeType.Array, - formEntity: 'checkbox_group', + entity: 'checkbox_group', formLayout: 'accordeon', - formEntityProps: { + entityProps: { direction: 'column', enumDescriptions: { monday: 'Mon', @@ -738,9 +738,9 @@ const schema: JsonSchemaObject = { title: 'checkbox_group', nodeParameters: { type: NodeType.Array, - formEntity: 'checkbox_group', + entity: 'checkbox_group', formLayout: 'accordeon', - formEntityProps: { + entityProps: { direction: 'column', enumDescriptions: { monday: 'Mon', @@ -756,7 +756,7 @@ const schema: JsonSchemaObject = { }, nodeParameters: { type: NodeType.Object, - formEntity: 'base', + entity: 'base', formLayout: 'transparent', }, }, @@ -768,9 +768,9 @@ const schema: JsonSchemaObject = { title: 'multi_select', nodeParameters: { type: NodeType.Array, - formEntity: 'select', - formLayout: 'row', - formEntityProps: { + entity: 'select', + layout: 'row', + entityProps: { enumDescriptions: { draft: 'Draft', published: 'Published', @@ -789,8 +789,8 @@ const schema: JsonSchemaObject = { description: 'slider description', nodeParameters: { type: NodeType.Number, - formEntity: 'slider', - formLayout: 'row', + entity: 'slider', + layout: 'row', }, }, range_slider: { @@ -803,8 +803,8 @@ const schema: JsonSchemaObject = { }, nodeParameters: { type: NodeType.Object, - formEntity: 'range_slider', - formLayout: 'row', + entity: 'range_slider', + layout: 'row', }, }, object_value: { @@ -817,15 +817,15 @@ const schema: JsonSchemaObject = { title: 'Value', nodeParameters: { type: NodeType.String, - formEntity: 'base', + entity: 'base', formLayout: 'transparent', }, }, }, nodeParameters: { type: NodeType.Object, - formEntity: 'dot_value', - formLayout: 'row', + entity: 'dot_value', + layout: 'row', }, }, file: { @@ -833,9 +833,9 @@ const schema: JsonSchemaObject = { title: 'File Input', nodeParameters: { type: NodeType.String, - formEntity: 'file', - formLayout: 'row', - formEntityProps: { + entity: 'file', + layout: 'row', + entityProps: { accept: ['.json', '.txt'], readAsMethod: 'readAsText', }, @@ -846,7 +846,7 @@ const schema: JsonSchemaObject = { description: 'Main object description', nodeParameters: { type: NodeType.Object, - formEntity: 'base', + entity: 'base', formLayout: 'section', formLayoutProps: { descriptionType: 'bottom', @@ -859,13 +859,82 @@ const schema: JsonSchemaObject = { const value = { test: { + textarea: + 'textarea value min-height: 20px; min-height: 20px; min-height: 20px;min-height: 20px; min-height: 20px; min-height: 20px; min-height: 20px; min-height: 20px; min-height: 20px;', + monaco: JSON.stringify( + { + extends: '@gravity-ui/tsconfig/tsconfig.json', + include: ['src/**/*.ts', 'src/**/*.tsx'], + exclude: ['src/stories'], + compilerOptions: { + outDir: 'build/esm', + module: 'esnext', + jsx: 'react', + resolveJsonModule: true, + baseUrl: '.', + declaration: true, + allowJs: true, + importHelpers: true, + paths: { + '@gravity-ui/dynamic-forms/unstable': ['./src/unstable.ts'], + '@gravity-ui/uikit/unstable': [ + 'node_modules/@gravity-ui/uikit/build/esm/unstable', + ], + }, + }, + }, + null, + 2, + ), + string_number_with_scale: '1000', + number_with_scale: 1000, + range_input: {from: 10, to: 20}, + object_inline: {foo: 'foo', bar: 1, rab: true}, + one_of_nested: { + foo: { + foo: 'textarea value', + bar: 10, + rab: true, + }, + bar: 5, + rab: false, + }, + few_of_nested: { + foo: { + foo: 'textarea value', + bar: 10, + rab: true, + }, + bar: 5, + rab: true, + }, array: ['test', 'test2'], tuple: ['test', 123], + array_table: [ + {name: 'John', surname: 'Doe', age: 30}, + {name: 'Jane', surname: 'Smith', age: 25}, + ], number: 123, string: 'test', boolean: true, - object_value: {value: 'test'}, + switch: true, + alert: 'alert value', + label: 'label value', text_content: 'value', + password: 'password', + color_picker: '#5282ff', + date_input: '2020-01-01', + radio_group: 'foo', + segmented_radio_group: 'foo', + select: 'draft', + sselect: {select: 'draft'}, + checkbox_group: ['monday'], + ccheckbox_group: {checkbox_group: ['monday']}, + multi_select: ['draft'], + slider: 50, + range_slider: {from: 0, to: 100}, + object_value: {value: 'test'}, + file: 'file content', }, }; @@ -1086,6 +1155,7 @@ const template = () => { const Template: StoryFn = (__) => { const [cfg, _setCfg] = React.useState(config); const [sch, _setSch] = React.useState(schema); + const [mode, setMode] = React.useState(SchemaRendererMode.Overview); React.useEffect(() => { setTimeout(() => { @@ -1096,8 +1166,8 @@ const template = () => { // description: 'textarea description', // nodeParameters: { // type: NodeType.String, - // formEntity: 'textarea', - // formLayout: 'row', + // entity: 'textarea', + // layout: 'row', // }, // }); }, 5000); @@ -1107,6 +1177,17 @@ const template = () => {
{(form) => ( + setMode(value as SchemaRendererMode)} + > + + Form + + + Overview + + {/* {fields.map((field) => ( { name="test" schema={sch} config={cfg} - mode={SchemaRendererMode.Form} + mode={mode} userContext={userContext} /> { name="test2" schema={sch} config={cfg} - mode={SchemaRendererMode.Form} + mode={mode} userContext={userContext} /> {/* */}