Skip to content

Commit

Permalink
feat: added AutoHeaders component
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tyv committed May 18, 2022
1 parent 6158314 commit 0f0ed05
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 2 deletions.
79 changes: 79 additions & 0 deletions packages/core/src/auto-view/auto-headers.tsx
Original file line number Diff line number Diff line change
@@ -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<string, any> => {
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<string, any>);
}

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 <th colspan=""> 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
});
};
1 change: 1 addition & 0 deletions packages/core/src/auto-view/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './auto-view';
export * from './default-items';
export * from './auto-headers';
export * from './utils';
export * from './root-schema';
4 changes: 2 additions & 2 deletions packages/core/src/utils/object-schema-as-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -29,7 +29,7 @@ export const objectSchemaAsArrayMapFn = (
export const objectSchemaAsArray = <T>(
schema: CoreSchemaMetaSchema,
data: Record<string, any>,
{pick, omit, order}: Rules = {},
{pick, omit, order}: ObjectSchemaAsArrayRules = {},
mapFunction: (field: string, schema: CoreSchemaMetaSchema) => T
) => {
if (schema.type !== 'object' || !schema.properties) {
Expand Down
195 changes: 195 additions & 0 deletions packages/core/tests/auto-view/auto-headers.test.tsx
Original file line number Diff line number Diff line change
@@ -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 => (
<span
data-automation-id={getAutomationId(
props.pointer,
'SPAN_STRING'
)}
>
{props.data}
</span>
)
})
.register('array', {
name: uniqueString('array'),
component: props => {
return (
<div
data-automation-id={getAutomationId(
props.pointer,
'ROOT'
)}
>
<AutoHeaders
{...props}
path="/items"
>
{headersProps => (
<AutoItems
{...headersProps}
render={(item, {pointer}) => (
<span
key={pointer}
data-automation-id={getAutomationId(
pointer,
'HEADER_CELL'
)}
>
{item}
</span>
)}
/>
)}
</AutoHeaders>
</div>
);
}
});

it('should render object fields as data', () => {
render(
<RepositoryProvider components={arrayRepo}>
<AutoView schema={testSchema} />
</RepositoryProvider>
);

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(
<RepositoryProvider components={arrayRepo}>
<AutoView
pick={['baz']}
schema={testSchema}
/>
</RepositoryProvider>
);

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(
<RepositoryProvider components={arrayRepo}>
<AutoView
omit={['foo', 'bar']}
schema={testSchema}
/>
</RepositoryProvider>
);

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(
<RepositoryProvider components={arrayRepo}>
<AutoView
schema={testSchema}
data={[{extra: 'data'}]}
/>
</RepositoryProvider>
);

const root = screen.getByTestId('#ROOT');
const lastCell = within(root).queryByTestId('/3#HEADER_CELL');
expect(lastCell).toHaveTextContent('extra');
});

describe('UISchema', () => {
it('should order fields', () => {
render(
<RepositoryProvider components={arrayRepo}>
<AutoView
schema={testSchema}
uiSchema={{
hints: {
'/items': {
order: ['baz', 'foo', 'bar']
}
},
components: {}
}}
/>
</RepositoryProvider>
);

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(
<RepositoryProvider components={arrayRepo}>
<AutoView
schema={testSchema}
uiSchema={{
hints: {
'/items': {
order: ['baz', 'foo', 'bar'],
hidden: ['baz', 'foo']
}
},
components: {}
}}
/>
</RepositoryProvider>
);

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();
});
});
});

0 comments on commit 0f0ed05

Please sign in to comment.