From 0f0ed0550c3651fd345a8b52a040f9e67e9f6195 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Wed, 18 May 2022 14:18:29 +0300 Subject: [PATCH] feat: added AutoHeaders component AutoHeaders component is a component that converts object `JSONSchema` to array of string data, where each string is a field name from the object schema. AutoHeaders provides new AutoView props based on object JSONSchema re #57 --- packages/core/src/auto-view/auto-headers.tsx | 79 +++++++ packages/core/src/auto-view/index.ts | 1 + .../core/src/utils/object-schema-as-array.ts | 4 +- .../tests/auto-view/auto-headers.test.tsx | 195 ++++++++++++++++++ 4 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/auto-view/auto-headers.tsx create mode 100644 packages/core/tests/auto-view/auto-headers.test.tsx diff --git a/packages/core/src/auto-view/auto-headers.tsx b/packages/core/src/auto-view/auto-headers.tsx new file mode 100644 index 00000000..c5231fb3 --- /dev/null +++ b/packages/core/src/auto-view/auto-headers.tsx @@ -0,0 +1,79 @@ +import {get} from 'json-pointer'; + +import {objectSchemaAsArray, ObjectSchemaAsArrayRules} from '../utils'; +import {JSONPointer} from '../repository'; + +import {getHints} from './utils'; +import {AutoViewProps} from './auto-view'; + +export interface AutoHeadersProps extends AutoViewProps { + children: (props: AutoViewProps) => JSX.Element; + path?: JSONPointer; +} + +const ensureObjectData = (data: any): Record => { + if (typeof data !== 'object') { + return {}; + } + + if (Array.isArray(data)) { + return data.reduce((acc, item) => { + if (typeof item !== 'object' || Array.isArray(item)) { + return acc; + } + + return {...acc, ...item}; + }, {} as Record); + } + + return data; +}; + +export const AutoHeaders = (props: AutoHeadersProps) => { + const schemaPath = props.path ?? ''; + const sourceObjectSchema = get(props.schema, schemaPath); + + if (sourceObjectSchema?.type !== 'object') { + throw new Error( + `expected schema \`type\` value to be \`object\`, but got ${props.schema?.type}` + ); + } + + const allPossibleFields = ensureObjectData(props.data); + + /** + * TODO: consider support for UIGroups, + * might be useful for per group + */ + + const {order, hidden} = getHints( + props.uiSchema, + (props.schemaPointer ?? '') + schemaPath + ); + + const rules: ObjectSchemaAsArrayRules = { + order, + pick: props.pick, + omit: props.omit ?? hidden + }; + + const data = objectSchemaAsArray( + sourceObjectSchema, + allPossibleFields, + rules, + field => field + ); + + const schema: AutoViewProps['schema'] = { + type: 'array', + items: {type: 'string'} + }; + + return props.children({ + schema, + data, + pointer: '', + schemaPointer: '', + validation: false + }); +}; diff --git a/packages/core/src/auto-view/index.ts b/packages/core/src/auto-view/index.ts index bdf46082..c4485341 100644 --- a/packages/core/src/auto-view/index.ts +++ b/packages/core/src/auto-view/index.ts @@ -1,4 +1,5 @@ export * from './auto-view'; export * from './default-items'; +export * from './auto-headers'; export * from './utils'; export * from './root-schema'; diff --git a/packages/core/src/utils/object-schema-as-array.ts b/packages/core/src/utils/object-schema-as-array.ts index d170f1f5..7c3fe9a1 100644 --- a/packages/core/src/utils/object-schema-as-array.ts +++ b/packages/core/src/utils/object-schema-as-array.ts @@ -3,7 +3,7 @@ import {CoreSchemaMetaSchema} from '../models'; import {allFields} from './all-fields'; import {filterAndOrderFields} from './filter-and-order-fields'; -type Rules = { +export type ObjectSchemaAsArrayRules = { pick?: string[]; omit?: string[]; order?: string[]; @@ -29,7 +29,7 @@ export const objectSchemaAsArrayMapFn = ( export const objectSchemaAsArray = ( schema: CoreSchemaMetaSchema, data: Record, - {pick, omit, order}: Rules = {}, + {pick, omit, order}: ObjectSchemaAsArrayRules = {}, mapFunction: (field: string, schema: CoreSchemaMetaSchema) => T ) => { if (schema.type !== 'object' || !schema.properties) { diff --git a/packages/core/tests/auto-view/auto-headers.test.tsx b/packages/core/tests/auto-view/auto-headers.test.tsx new file mode 100644 index 00000000..7a7c8d95 --- /dev/null +++ b/packages/core/tests/auto-view/auto-headers.test.tsx @@ -0,0 +1,195 @@ +import {render, screen, within} from '@testing-library/react'; +import React from 'react'; + +import { + AutoHeaders, + AutoItems, + AutoView, + ComponentsRepo, + CoreSchemaMetaSchema, + RepositoryProvider +} from '../../src'; +import {getAutomationId, uniqueString} from '../repositories/utils'; + +describe('AutoHeaders', () => { + const testSchema: CoreSchemaMetaSchema = { + type: 'array', + items: { + type: 'object', + properties: { + foo: {type: 'string'}, + bar: {type: 'string'}, + baz: {type: 'number'} + }, + additionalProperties: {type: 'string'} + } + }; + + const arrayRepo = new ComponentsRepo('table') + .register('string', { + name: uniqueString('string'), + component: props => ( + + {props.data} + + ) + }) + .register('array', { + name: uniqueString('array'), + component: props => { + return ( +
+ + {headersProps => ( + ( + + {item} + + )} + /> + )} + +
+ ); + } + }); + + it('should render object fields as data', () => { + render( + + + + ); + + expect(screen.getByTestId('/0#HEADER_CELL')).toHaveTextContent('foo'); + expect(screen.getByTestId('/1#HEADER_CELL')).toHaveTextContent('bar'); + expect(screen.getByTestId('/2#HEADER_CELL')).toHaveTextContent('baz'); + }); + + it('should render only `pick`ed fields', () => { + render( + + + + ); + + const root = screen.getByTestId('#ROOT'); + const firstCell = within(root).queryByTestId('/0#HEADER_CELL'); + const secondCell = within(root).queryByTestId('/1#HEADER_CELL'); + + expect(firstCell).toHaveTextContent('baz'); + expect(secondCell).not.toBeInTheDocument(); + }); + + it('should not render `omit`ed fields', () => { + render( + + + + ); + + const root = screen.getByTestId('#ROOT'); + const firstCell = within(root).queryByTestId('/0#HEADER_CELL'); + const secondCell = within(root).queryByTestId('/1#HEADER_CELL'); + + expect(firstCell).toHaveTextContent('baz'); + expect(secondCell).not.toBeInTheDocument(); + }); + + it('should render extra fields', () => { + render( + + + + ); + + const root = screen.getByTestId('#ROOT'); + const lastCell = within(root).queryByTestId('/3#HEADER_CELL'); + expect(lastCell).toHaveTextContent('extra'); + }); + + describe('UISchema', () => { + it('should order fields', () => { + render( + + + + ); + + const root = screen.getByTestId('#ROOT'); + const firstCell = within(root).queryByTestId('/0#HEADER_CELL'); + const secondCell = within(root).queryByTestId('/1#HEADER_CELL'); + const thirdCell = within(root).queryByTestId('/2#HEADER_CELL'); + + expect(firstCell).toHaveTextContent('baz'); + expect(secondCell).toHaveTextContent('foo'); + expect(thirdCell).toHaveTextContent('bar'); + }); + + it('should hide fields', () => { + render( + + + + ); + + const root = screen.getByTestId('#ROOT'); + const firstCell = within(root).queryByTestId('/0#HEADER_CELL'); + const lastCell = within(root).queryByTestId('/1#HEADER_CELL'); + + expect(firstCell).toHaveTextContent('bar'); + expect(lastCell).not.toBeInTheDocument(); + }); + }); +});