Skip to content

Commit

Permalink
Merge pull request #509
Browse files Browse the repository at this point in the history
feat(24318): Create a compact editable table for attribute/value editor

* refactor(24318): add mapping for custom fields

* feat(24318): add custom editable table for user properties

* feat(24143): add custom editor for compact property/value items

* fix(24143): fix JSX

* feat(24143): add support for add and delete item

* refactor(24143): refactor editable cell and add required checks

* refactor(24143): refactor initial state

* refactor(24143): fix publish

* feat(24143): add ui mapping to adapters

* refactor(24318): update dependencies

* refactor(24318): refactor the form widget to export the UI components

* refactor(24318): refactor the table component

* refactor(24318): add a grid component

* refactor(24318): revert datagrid trials

* feat(24318): add custom templates for the compact grid

* refactor(24318): refactor compact grid field

* feat(24318): add custom field to the configuration of the adapter editor

* test(24318): add mocks and tests
  • Loading branch information
vanch3d authored Aug 6, 2024
1 parent 8f6e10c commit ee9ae1f
Show file tree
Hide file tree
Showing 17 changed files with 471 additions and 2 deletions.
46 changes: 46 additions & 0 deletions hivemq-edge/src/frontend/src/__test-utils__/rjsf/rjsf.mocks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FC, FormEvent } from 'react'
import validator from '@rjsf/validator-ajv8'
import { RJSFSchema, UiSchema, RJSFValidationError, RegistryFieldsType } from '@rjsf/utils'
import Form from '@rjsf/chakra-ui'
import { IChangeEvent } from '@rjsf/core'

import { ObjectFieldTemplate } from '@/components/rjsf/ObjectFieldTemplate.tsx'
import { FieldTemplate } from '@/components/rjsf/FieldTemplate.tsx'
import { BaseInputTemplate } from '@/components/rjsf/BaseInputTemplate.tsx'
import { ArrayFieldTemplate } from '@/components/rjsf/ArrayFieldTemplate.tsx'
import { ArrayFieldItemTemplate } from '@/components/rjsf/ArrayFieldItemTemplate.tsx'

interface RjsfMocksProps {
schema: RJSFSchema
uiSchema?: UiSchema | undefined
onSubmit?: (data: IChangeEvent, event: FormEvent) => void
onError?: (errors: RJSFValidationError[]) => void
formData?: unknown
fields?: RegistryFieldsType
}

const RjsfMocks: FC<RjsfMocksProps> = ({ schema, uiSchema, formData, fields, onSubmit, onError }) => {
return (
<Form
id="mock-jsonschema-form"
schema={schema}
uiSchema={uiSchema}
templates={{
ObjectFieldTemplate,
FieldTemplate,
BaseInputTemplate,
ArrayFieldTemplate,
ArrayFieldItemTemplate,
}}
liveValidate
showErrorList="bottom"
validator={validator}
onSubmit={onSubmit}
onError={onError}
formData={formData}
fields={fields}
/>
)
}

export default RjsfMocks
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/// <reference types="cypress" />
import { RJSFSchema, UiSchema } from '@rjsf/utils'

import RjsfMocks from '@/__test-utils__/rjsf/rjsf.mocks.tsx'
import { adapterJSFFields } from '@/modules/ProtocolAdapters/utils/uiSchema.utils.ts'

const MOCK_SCHEMA: RJSFSchema = {
title: 'User Properties',
description: 'Arbitrary properties to associate with the subscription',
maxItems: 3,
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
title: 'Property Name',
description: 'Name of the associated property',
},
value: {
type: 'string',
title: 'Property Value',
description: 'Value of the associated property',
},
},
required: ['name', 'value'],
},
}

const MOCK_UI_SCHEMA: UiSchema = {
'ui:field': 'compactTable',
}

const MOCK_DATA = [
{
name: 'name1',
value: '1',
},
{
name: 'name2',
value: 'value',
},
]

describe('CompactArrayField', () => {
beforeEach(() => {
cy.viewport(800, 900)
})

it('should render the compact table ', () => {
const onSubmit = cy.stub().as('onSubmit')
cy.mountWithProviders(
<RjsfMocks
schema={MOCK_SCHEMA}
uiSchema={MOCK_UI_SCHEMA}
formData={MOCK_DATA}
onSubmit={onSubmit}
fields={adapterJSFFields}
/>
)

cy.get('[role="group"] h5').should('contain.text', 'User Properties')
cy.get('[role="group"] p').should('contain.text', 'Arbitrary properties to associate with the subscription')
cy.get('table thead tr th').should('have.length', 3)
cy.get('table thead tr th').eq(0).should('contain.text', 'Property Name')
cy.get('table thead tr th').eq(1).should('contain.text', 'Property Value')
cy.get('table thead tr th').eq(2).should('contain.text', 'Actions')

cy.get('table tbody tr').should('have.length', 2)
cy.get('table tbody td input').should('have.length', 4)
cy.get('table tbody td input').eq(0).should('have.value', 'name1')
cy.get('table tbody td input').eq(1).should('have.value', '1')
cy.get('table tbody td input').eq(2).should('have.value', 'name2')
cy.get('table tbody td input').eq(3).should('have.value', 'value')

cy.get("button[type='submit']").click()
cy.get('@onSubmit').should(
'have.been.calledWith',
Cypress.sinon.match({
formData: MOCK_DATA,
})
)
})

it('should add and remove items', () => {
cy.mountWithProviders(
<RjsfMocks schema={MOCK_SCHEMA} uiSchema={MOCK_UI_SCHEMA} formData={MOCK_DATA} fields={adapterJSFFields} />
)

cy.get('table tbody tr').should('have.length', 2)
cy.getByTestId('compact-add-item').click()
cy.get('table tbody tr').should('have.length', 3)
cy.get('table tbody td input')
.eq(4)
.should('have.value', '')
.should('have.attr', 'required', 'required')
.should('have.attr', 'aria-invalid', 'true')
cy.get('[role="alert"] ul li').should('have.length', 2)

cy.getByTestId('compact-delete-item').eq(2).click()
cy.get('table tbody tr').should('have.length', 2)
})

it('should be accessible ', () => {
cy.injectAxe()

cy.mountWithProviders(
<RjsfMocks
schema={MOCK_SCHEMA}
uiSchema={MOCK_UI_SCHEMA}
formData={MOCK_DATA}
onSubmit={(e) => console.log(e)}
fields={adapterJSFFields}
/>
)

cy.checkAccessibility(undefined, {
rules: {
// h5 used for sections is not in order. Not detected on other tests
'heading-order': { enabled: false },
},
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FC } from 'react'
import { FieldProps } from '@rjsf/utils'
import { RJSFSchema } from '@rjsf/utils/src/types.ts'

import { AdapterContext } from '@/modules/ProtocolAdapters/types.ts'
import { CompactArrayFieldTemplate } from '@/components/rjsf/Templates/CompactArrayFieldTemplate.tsx'
import { CompactFieldTemplate } from '@/components/rjsf/Templates/CompactFieldTemplate.tsx'
import { CompactBaseInputTemplate } from '@/components/rjsf/Templates/CompactBaseInputTemplate.tsx'
import { CompactObjectFieldTemplate } from '@/components/rjsf/Templates/CompactObjectFieldTemplate.tsx'
import { CompactArrayFieldItemTemplate } from '@/components/rjsf/Templates/CompactArrayFieldItemTemplate.tsx'

const CompactArrayField: FC<FieldProps<unknown, RJSFSchema, AdapterContext>> = (props) => {
const { registry } = props

registry.templates = {
...registry.templates,
ArrayFieldTemplate: CompactArrayFieldTemplate,
ArrayFieldItemTemplate: CompactArrayFieldItemTemplate,
FieldTemplate: CompactFieldTemplate,
BaseInputTemplate: CompactBaseInputTemplate,
ObjectFieldTemplate: CompactObjectFieldTemplate,
}

return <props.registry.fields.ArrayField {...props} />
}

export default CompactArrayField
18 changes: 18 additions & 0 deletions hivemq-edge/src/frontend/src/components/rjsf/Fields/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { JSONSchema7 } from 'json-schema'
import { Dispatch, SetStateAction } from 'react'

export type CustomPropertyValue = string
export type FormDataItem = Record<string, CustomPropertyValue>
export type CustomPropertyForm = FormDataItem[]

export interface DataGridProps {
data: CustomPropertyForm
columnTypes: [string, JSONSchema7][]
isDisabled?: boolean
required?: string[]
maxItems?: number
onHandleDeleteItem?: (index: number) => void
onHandleAddItem?: () => void
onUpdateData?: (rowIndex: number, columnId: string, value: CustomPropertyValue) => void
onSetData?: Dispatch<SetStateAction<CustomPropertyForm>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { FC, useMemo } from 'react'
import { ArrayFieldTemplateItemType } from '@rjsf/utils'
import { ButtonGroup, Td, Tr } from '@chakra-ui/react'
import { CopyButton, MoveDownButton, MoveUpButton, RemoveButton } from '@/components/rjsf/__internals/IconButton.tsx'

export const CompactArrayFieldItemTemplate: FC<ArrayFieldTemplateItemType> = (props) => {
const {
children,
index,
hasMoveUp,
hasRemove,
hasMoveDown,
hasCopy,
disabled,
readonly,
onCopyIndexClick,
onDropIndexClick,
onReorderClick,
uiSchema,
registry,
} = props

const onCopyClick = useMemo(() => onCopyIndexClick(index), [index, onCopyIndexClick])
const onRemoveClick = useMemo(() => onDropIndexClick(index), [index, onDropIndexClick])
const onArrowUpClick = useMemo(() => onReorderClick(index, index - 1), [index, onReorderClick])
const onArrowDownClick = useMemo(() => onReorderClick(index, index + 1), [index, onReorderClick])

return (
<Tr>
{children}
<Td>
<ButtonGroup isAttached size="sm" orientation="horizontal" ml={2}>
{(hasMoveUp || hasMoveDown) && (
<MoveUpButton
data-testid="compact-up-item"
disabled={disabled || readonly || !hasMoveUp}
onClick={onArrowUpClick}
uiSchema={uiSchema}
registry={registry}
/>
)}
{(hasMoveUp || hasMoveDown) && (
<MoveDownButton
data-testid="compact-down-item"
disabled={disabled || readonly || !hasMoveDown}
onClick={onArrowDownClick}
uiSchema={uiSchema}
registry={registry}
/>
)}
{hasCopy && (
<CopyButton
data-testid="compact-copy-item"
disabled={disabled || readonly}
onClick={onCopyClick}
uiSchema={uiSchema}
registry={registry}
/>
)}
{hasRemove && (
<RemoveButton
data-testid="compact-delete-item"
disabled={disabled || readonly}
onClick={onRemoveClick}
uiSchema={uiSchema}
registry={registry}
/>
)}
</ButtonGroup>
</Td>
</Tr>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { FC } from 'react'
import { JSONSchema7 } from 'json-schema'
import { RJSFSchema, ArrayFieldTemplateProps, getTemplate, getUiOptions, ArrayFieldTemplateItemType } from '@rjsf/utils'
import { Box, HStack, Table, Tbody, Th, Thead, Tr } from '@chakra-ui/react'

import AddButton from '@/components/rjsf/__internals/AddButton.tsx'
import { AdapterContext } from '@/modules/ProtocolAdapters/types.ts'

export const CompactArrayFieldTemplate: FC<ArrayFieldTemplateProps<unknown, RJSFSchema, AdapterContext>> = (props) => {
const { canAdd, disabled, idSchema, uiSchema, items, onAddClick, readonly, registry, required, schema, title } = props
const uiOptions = getUiOptions(uiSchema)
const ArrayFieldDescriptionTemplate = getTemplate<'ArrayFieldDescriptionTemplate'>(
'ArrayFieldDescriptionTemplate',
registry,
uiOptions
)
const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate'>('ArrayFieldItemTemplate', registry, uiOptions)
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate'>('ArrayFieldTitleTemplate', registry, uiOptions)

const { items: SchemaItems } = schema as JSONSchema7
const { properties } = SchemaItems as JSONSchema7

// Better approach to unidentified headers?
if (!properties) return null

return (
<Box>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || title}
schema={schema}
uiSchema={uiSchema}
required={required}
registry={registry}
/>
<ArrayFieldDescriptionTemplate
idSchema={idSchema}
description={uiOptions.description || schema.description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
<>
<Table size="xs">
<Thead>
<Tr>
{Object.entries(properties).map(([key, values]) => {
const { title } = values as JSONSchema7
return <Th key={key}>{title}</Th>
})}
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{items?.length > 0 &&
items?.map(({ key, ...itemProps }: ArrayFieldTemplateItemType) => {
return <ArrayFieldItemTemplate key={key} {...itemProps} />
})}
</Tbody>
</Table>
{canAdd && (
<HStack justifyContent="end" mt={2}>
<AddButton
data-testid="compact-add-item"
className="array-item-add"
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
</HStack>
)}
</>
</Box>
)
}
Loading

0 comments on commit ee9ae1f

Please sign in to comment.