fix(ui): fine-tune plugins config form (#4916)
* fix(ui): use stock array renderer for plugins config form Signed-off-by: Deluan <deluan@navidrome.org> * fix(plugins): enforce minimum user tokens and require users field Signed-off-by: Deluan <deluan@navidrome.org> * fix(ui): simplify error handling in control state hook Signed-off-by: Deluan <deluan@navidrome.org> * fix(ui): remove "None" MenuItem from OutlinedEnumControl Signed-off-by: Deluan <deluan@navidrome.org> * fix(ui): enhance error handling by returning field info and path in validation errors Signed-off-by: Deluan <deluan@navidrome.org> * fix(ui): update OutlinedEnumControl to handle empty values and remove "None" option when required Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -42,7 +42,7 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"title": "User Tokens",
|
"title": "User Tokens",
|
||||||
"description": "Discord tokens for each Navidrome user. WARNING: Store tokens securely!",
|
"description": "Discord tokens for each Navidrome user. WARNING: Store tokens securely!",
|
||||||
"default": [{}],
|
"minItems": 1,
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["clientid"]
|
"required": ["clientid", "users"]
|
||||||
},
|
},
|
||||||
"uiSchema": {
|
"uiSchema": {
|
||||||
"type": "VerticalLayout",
|
"type": "VerticalLayout",
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"title": "User Tokens",
|
"title": "User Tokens",
|
||||||
"description": "Discord tokens for each Navidrome user. WARNING: Store tokens securely!",
|
"description": "Discord tokens for each Navidrome user. WARNING: Store tokens securely!",
|
||||||
"default": [{}],
|
"minItems": 1,
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["clientid"]
|
"required": ["clientid", "users"]
|
||||||
},
|
},
|
||||||
"uiSchema": {
|
"uiSchema": {
|
||||||
"type": "VerticalLayout",
|
"type": "VerticalLayout",
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
import React, { useCallback, useMemo } from 'react'
|
|
||||||
import {
|
|
||||||
composePaths,
|
|
||||||
computeLabel,
|
|
||||||
createDefaultValue,
|
|
||||||
isObjectArrayWithNesting,
|
|
||||||
isPrimitiveArrayControl,
|
|
||||||
rankWith,
|
|
||||||
findUISchema,
|
|
||||||
Resolve,
|
|
||||||
} from '@jsonforms/core'
|
|
||||||
import {
|
|
||||||
JsonFormsDispatch,
|
|
||||||
withJsonFormsArrayLayoutProps,
|
|
||||||
} from '@jsonforms/react'
|
|
||||||
import range from 'lodash/range'
|
|
||||||
import merge from 'lodash/merge'
|
|
||||||
import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'
|
|
||||||
import { Add, Delete } from '@material-ui/icons'
|
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
arrayItem: {
|
|
||||||
position: 'relative',
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
'&:last-child': {
|
|
||||||
marginBottom: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
deleteButton: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: theme.spacing(1),
|
|
||||||
right: theme.spacing(1),
|
|
||||||
},
|
|
||||||
itemContent: {
|
|
||||||
paddingRight: theme.spacing(4), // Space for delete button
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Default translations for array controls
|
|
||||||
const defaultTranslations = {
|
|
||||||
addTooltip: 'Add',
|
|
||||||
addAriaLabel: 'Add button',
|
|
||||||
removeTooltip: 'Delete',
|
|
||||||
removeAriaLabel: 'Delete button',
|
|
||||||
noDataMessage: 'No data',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simplified array item renderer - clean card layout
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
|
||||||
const ArrayItem = ({
|
|
||||||
index,
|
|
||||||
path,
|
|
||||||
schema,
|
|
||||||
uischema,
|
|
||||||
uischemas,
|
|
||||||
rootSchema,
|
|
||||||
renderers,
|
|
||||||
cells,
|
|
||||||
enabled,
|
|
||||||
removeItems,
|
|
||||||
translations,
|
|
||||||
disableRemove,
|
|
||||||
}) => {
|
|
||||||
const classes = useStyles()
|
|
||||||
const childPath = composePaths(path, `${index}`)
|
|
||||||
|
|
||||||
const foundUISchema = useMemo(
|
|
||||||
() =>
|
|
||||||
findUISchema(
|
|
||||||
uischemas,
|
|
||||||
schema,
|
|
||||||
uischema.scope,
|
|
||||||
path,
|
|
||||||
undefined,
|
|
||||||
uischema,
|
|
||||||
rootSchema,
|
|
||||||
),
|
|
||||||
[uischemas, schema, path, uischema, rootSchema],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box className={classes.arrayItem}>
|
|
||||||
{enabled && !disableRemove && (
|
|
||||||
<Tooltip
|
|
||||||
title={translations.removeTooltip}
|
|
||||||
className={classes.deleteButton}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => removeItems(path, [index])()}
|
|
||||||
size="small"
|
|
||||||
aria-label={translations.removeAriaLabel}
|
|
||||||
>
|
|
||||||
<Delete fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Box className={classes.itemContent}>
|
|
||||||
<JsonFormsDispatch
|
|
||||||
enabled={enabled}
|
|
||||||
schema={schema}
|
|
||||||
uischema={foundUISchema}
|
|
||||||
path={childPath}
|
|
||||||
key={childPath}
|
|
||||||
renderers={renderers}
|
|
||||||
cells={cells}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Array toolbar with add button
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
|
||||||
const ArrayToolbar = ({
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
enabled,
|
|
||||||
addItem,
|
|
||||||
path,
|
|
||||||
createDefault,
|
|
||||||
translations,
|
|
||||||
disableAdd,
|
|
||||||
}) => (
|
|
||||||
<Box mb={1}>
|
|
||||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
|
||||||
<Typography variant="h6">{label}</Typography>
|
|
||||||
{!disableAdd && (
|
|
||||||
<Tooltip
|
|
||||||
title={translations.addTooltip}
|
|
||||||
aria-label={translations.addAriaLabel}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
onClick={addItem(path, createDefault())}
|
|
||||||
disabled={!enabled}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<Add />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
{description && (
|
|
||||||
<Typography variant="caption" color="textSecondary">
|
|
||||||
{description}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
|
|
||||||
const useArrayStyles = makeStyles((theme) => ({
|
|
||||||
container: {
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Main array layout component - items always expanded
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
|
||||||
const AlwaysExpandedArrayLayoutComponent = (props) => {
|
|
||||||
const arrayClasses = useArrayStyles()
|
|
||||||
const {
|
|
||||||
enabled,
|
|
||||||
data,
|
|
||||||
path,
|
|
||||||
schema,
|
|
||||||
uischema,
|
|
||||||
addItem,
|
|
||||||
removeItems,
|
|
||||||
renderers,
|
|
||||||
cells,
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
required,
|
|
||||||
rootSchema,
|
|
||||||
config,
|
|
||||||
uischemas,
|
|
||||||
disableAdd,
|
|
||||||
disableRemove,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const innerCreateDefaultValue = useCallback(
|
|
||||||
() => createDefaultValue(schema, rootSchema),
|
|
||||||
[schema, rootSchema],
|
|
||||||
)
|
|
||||||
|
|
||||||
const appliedUiSchemaOptions = merge({}, config, uischema.options)
|
|
||||||
const doDisableAdd = disableAdd || appliedUiSchemaOptions.disableAdd
|
|
||||||
const doDisableRemove = disableRemove || appliedUiSchemaOptions.disableRemove
|
|
||||||
const translations = defaultTranslations
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={arrayClasses.container}>
|
|
||||||
<ArrayToolbar
|
|
||||||
translations={translations}
|
|
||||||
label={computeLabel(
|
|
||||||
label,
|
|
||||||
required,
|
|
||||||
appliedUiSchemaOptions.hideRequiredAsterisk,
|
|
||||||
)}
|
|
||||||
description={description}
|
|
||||||
path={path}
|
|
||||||
enabled={enabled}
|
|
||||||
addItem={addItem}
|
|
||||||
createDefault={innerCreateDefaultValue}
|
|
||||||
disableAdd={doDisableAdd}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
{data > 0 ? (
|
|
||||||
range(data).map((index) => (
|
|
||||||
<ArrayItem
|
|
||||||
key={index}
|
|
||||||
index={index}
|
|
||||||
path={path}
|
|
||||||
schema={schema}
|
|
||||||
uischema={uischema}
|
|
||||||
uischemas={uischemas}
|
|
||||||
rootSchema={rootSchema}
|
|
||||||
renderers={renderers}
|
|
||||||
cells={cells}
|
|
||||||
enabled={enabled}
|
|
||||||
removeItems={removeItems}
|
|
||||||
translations={translations}
|
|
||||||
disableRemove={doDisableRemove}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Typography color="textSecondary">
|
|
||||||
{translations.noDataMessage}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap with JSONForms HOC
|
|
||||||
const WrappedArrayLayout = withJsonFormsArrayLayoutProps(
|
|
||||||
AlwaysExpandedArrayLayoutComponent,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Custom tester that matches arrays but NOT enum arrays
|
|
||||||
// Enum arrays should be handled by MaterialEnumArrayRenderer (for checkboxes)
|
|
||||||
const isNonEnumArrayControl = (uischema, schema) => {
|
|
||||||
// First check if it matches our base conditions (object array or primitive array)
|
|
||||||
const baseCheck =
|
|
||||||
isObjectArrayWithNesting(uischema, schema) ||
|
|
||||||
isPrimitiveArrayControl(uischema, schema)
|
|
||||||
|
|
||||||
if (!baseCheck) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the actual schema for this control using JSONForms utility
|
|
||||||
const rootSchema = schema
|
|
||||||
const resolved = Resolve.schema(rootSchema, uischema?.scope, rootSchema)
|
|
||||||
|
|
||||||
// Exclude enum arrays (uniqueItems + oneOf/enum) - let MaterialEnumArrayRenderer handle them
|
|
||||||
if (resolved?.uniqueItems && resolved?.items) {
|
|
||||||
const { items } = resolved
|
|
||||||
if (items.oneOf?.every((e) => e.const !== undefined) || items.enum) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export as a renderer entry with high priority (5 > default 4)
|
|
||||||
// Matches both object arrays with nesting and primitive arrays, but NOT enum arrays
|
|
||||||
export const AlwaysExpandedArrayLayout = {
|
|
||||||
tester: rankWith(5, isNonEnumArrayControl),
|
|
||||||
renderer: WrappedArrayLayout,
|
|
||||||
}
|
|
||||||
@@ -4,76 +4,37 @@ import { Card, CardContent, Typography, Box } from '@material-ui/core'
|
|||||||
import Alert from '@material-ui/lab/Alert'
|
import Alert from '@material-ui/lab/Alert'
|
||||||
import { SchemaConfigEditor } from './SchemaConfigEditor'
|
import { SchemaConfigEditor } from './SchemaConfigEditor'
|
||||||
|
|
||||||
// Navigate schema by path parts to find the title for a field
|
// Format error with field title and full path for nested fields
|
||||||
const findFieldTitle = (schema, parts) => {
|
const formatError = (error, schema) => {
|
||||||
|
// Get path parts from various error formats
|
||||||
|
const rawPath =
|
||||||
|
error.dataPath || error.property || error.instancePath?.replace(/\//g, '.')
|
||||||
|
const parts = rawPath?.split('.').filter(Boolean) || []
|
||||||
|
|
||||||
|
// Navigate schema to find field title, build bracket-notation path
|
||||||
let currentSchema = schema
|
let currentSchema = schema
|
||||||
let fieldName = parts[parts.length - 1] // Default to last part
|
let fieldName = parts[parts.length - 1]
|
||||||
|
const pathParts = []
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (!currentSchema) break
|
|
||||||
|
|
||||||
// Skip array indices (just move to items schema)
|
|
||||||
if (/^\d+$/.test(part)) {
|
if (/^\d+$/.test(part)) {
|
||||||
if (currentSchema.items) {
|
pathParts.push(`[${part}]`)
|
||||||
currentSchema = currentSchema.items
|
currentSchema = currentSchema?.items
|
||||||
}
|
} else {
|
||||||
continue
|
fieldName = currentSchema?.properties?.[part]?.title || part
|
||||||
}
|
pathParts.push(part)
|
||||||
|
currentSchema = currentSchema?.properties?.[part]
|
||||||
// Navigate to property and always update fieldName
|
|
||||||
if (currentSchema.properties?.[part]) {
|
|
||||||
const propSchema = currentSchema.properties[part]
|
|
||||||
fieldName = propSchema.title || part
|
|
||||||
currentSchema = propSchema
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fieldName
|
const path = pathParts.join('.').replace(/\.\[/g, '[')
|
||||||
}
|
const isNested = path.includes('[') || path.includes('.')
|
||||||
|
// Replace property name in message with full path for nested fields
|
||||||
|
const message = isNested
|
||||||
|
? error.message.replace(/'[^']+'\s*$/, `'${path}'`)
|
||||||
|
: error.message
|
||||||
|
|
||||||
// Extract human-readable field name from JSONForms error
|
return { fieldName, message }
|
||||||
const getFieldName = (error, schema) => {
|
|
||||||
// JSONForms errors can have different path formats:
|
|
||||||
// - dataPath: "users.1.token" (dot-separated)
|
|
||||||
// - instancePath: "/users/1/token" (slash-separated)
|
|
||||||
// - property: "users.1.username" (dot-separated)
|
|
||||||
const dataPath = error.dataPath || ''
|
|
||||||
const instancePath = error.instancePath || ''
|
|
||||||
const property = error.property || ''
|
|
||||||
|
|
||||||
// Try dataPath first (dot-separated like "users.1.token")
|
|
||||||
if (dataPath) {
|
|
||||||
const parts = dataPath.split('.').filter(Boolean)
|
|
||||||
if (parts.length > 0) {
|
|
||||||
return findFieldTitle(schema, parts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try property (also dot-separated)
|
|
||||||
if (property) {
|
|
||||||
const parts = property.split('.').filter(Boolean)
|
|
||||||
if (parts.length > 0) {
|
|
||||||
return findFieldTitle(schema, parts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to instancePath (slash-separated like "/users/1/token")
|
|
||||||
if (instancePath) {
|
|
||||||
const parts = instancePath.split('/').filter(Boolean)
|
|
||||||
if (parts.length > 0) {
|
|
||||||
return findFieldTitle(schema, parts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to extract from schemaPath like "#/properties/users/items/properties/username/minLength"
|
|
||||||
const schemaPath = error.schemaPath || ''
|
|
||||||
const propMatches = [...schemaPath.matchAll(/\/properties\/([^/]+)/g)]
|
|
||||||
if (propMatches.length > 0) {
|
|
||||||
const parts = propMatches.map((m) => m[1])
|
|
||||||
return findFieldTitle(schema, parts)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfigCard = ({
|
export const ConfigCard = ({
|
||||||
@@ -99,14 +60,10 @@ export const ConfigCard = ({
|
|||||||
|
|
||||||
// Format validation errors with proper field names
|
// Format validation errors with proper field names
|
||||||
const formattedErrors = useMemo(() => {
|
const formattedErrors = useMemo(() => {
|
||||||
if (!hasConfigSchema) {
|
if (!hasConfigSchema) return []
|
||||||
return []
|
return validationErrors.map((error) =>
|
||||||
}
|
formatError(error, manifest.config.schema),
|
||||||
const { schema } = manifest.config
|
)
|
||||||
return validationErrors.map((error) => ({
|
|
||||||
fieldName: getFieldName(error, schema),
|
|
||||||
message: error.message,
|
|
||||||
}))
|
|
||||||
}, [validationErrors, manifest, hasConfigSchema])
|
}, [validationErrors, manifest, hasConfigSchema])
|
||||||
|
|
||||||
if (!hasConfigSchema) {
|
if (!hasConfigSchema) {
|
||||||
@@ -139,12 +96,14 @@ export const ConfigCard = ({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SchemaConfigEditor
|
<Box mt={formattedErrors.length > 0 ? 0 : 2}>
|
||||||
schema={schema}
|
<SchemaConfigEditor
|
||||||
uiSchema={uiSchema}
|
schema={schema}
|
||||||
data={configData}
|
uiSchema={uiSchema}
|
||||||
onChange={handleChange}
|
data={configData}
|
||||||
/>
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,18 +40,14 @@ const useStyles = makeStyles(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for common control state (focus, validation, description visibility)
|
* Hook for common control state (focus, validation, description visibility)
|
||||||
* Tracks "touched" state to only show errors after the user has interacted with the field
|
|
||||||
*/
|
*/
|
||||||
const useControlState = (props) => {
|
const useControlState = (props) => {
|
||||||
const { config, uischema, description, visible, errors } = props
|
const { config, uischema, description, visible, errors } = props
|
||||||
const [isFocused, setIsFocused] = useState(false)
|
const [isFocused, setIsFocused] = useState(false)
|
||||||
const [isTouched, setIsTouched] = useState(false)
|
|
||||||
|
|
||||||
const appliedUiSchemaOptions = merge({}, config, uischema?.options)
|
const appliedUiSchemaOptions = merge({}, config, uischema?.options)
|
||||||
// errors is a string when there are validation errors, empty/undefined when valid
|
// errors is a string when there are validation errors, empty/undefined when valid
|
||||||
const hasErrors = errors && errors.length > 0
|
const showError = errors && errors.length > 0
|
||||||
// Only show as invalid after the field has been touched (blurred)
|
|
||||||
const showError = isTouched && hasErrors
|
|
||||||
|
|
||||||
const showDescription = !isDescriptionHidden(
|
const showDescription = !isDescriptionHidden(
|
||||||
visible,
|
visible,
|
||||||
@@ -63,10 +59,7 @@ const useControlState = (props) => {
|
|||||||
const helperText = showError ? errors : showDescription ? description : ''
|
const helperText = showError ? errors : showDescription ? description : ''
|
||||||
|
|
||||||
const handleFocus = () => setIsFocused(true)
|
const handleFocus = () => setIsFocused(true)
|
||||||
const handleBlur = () => {
|
const handleBlur = () => setIsFocused(false)
|
||||||
setIsFocused(false)
|
|
||||||
setIsTouched(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isFocused,
|
isFocused,
|
||||||
@@ -185,8 +178,17 @@ const OutlinedNumberControl = (props) => {
|
|||||||
// Enum/Select control wrapper
|
// Enum/Select control wrapper
|
||||||
const OutlinedEnumControl = (props) => {
|
const OutlinedEnumControl = (props) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const { data, id, enabled, path, handleChange, options, label, visible } =
|
const {
|
||||||
props
|
data,
|
||||||
|
id,
|
||||||
|
enabled,
|
||||||
|
path,
|
||||||
|
handleChange,
|
||||||
|
options,
|
||||||
|
label,
|
||||||
|
visible,
|
||||||
|
required,
|
||||||
|
} = props
|
||||||
const {
|
const {
|
||||||
appliedUiSchemaOptions,
|
appliedUiSchemaOptions,
|
||||||
showError,
|
showError,
|
||||||
@@ -212,7 +214,12 @@ const OutlinedEnumControl = (props) => {
|
|||||||
labelId={`${id}-label`}
|
labelId={`${id}-label`}
|
||||||
id={id}
|
id={id}
|
||||||
value={data ?? ''}
|
value={data ?? ''}
|
||||||
onChange={(ev) => handleChange(path, ev.target.value)}
|
onChange={(ev) => {
|
||||||
|
handleChange(
|
||||||
|
path,
|
||||||
|
ev.target.value === '' ? undefined : ev.target.value,
|
||||||
|
)
|
||||||
|
}}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
@@ -220,9 +227,11 @@ const OutlinedEnumControl = (props) => {
|
|||||||
label={label}
|
label={label}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
<MenuItem value="">
|
{!required && (
|
||||||
<em>None</em>
|
<MenuItem value="">
|
||||||
</MenuItem>
|
<em>None</em>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{options?.map((option) => (
|
{options?.map((option) => (
|
||||||
<MenuItem key={option.value} value={option.value}>
|
<MenuItem key={option.value} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||||||
import { Typography } from '@material-ui/core'
|
import { Typography } from '@material-ui/core'
|
||||||
import { useTranslate } from 'react-admin'
|
import { useTranslate } from 'react-admin'
|
||||||
import Ajv from 'ajv'
|
import Ajv from 'ajv'
|
||||||
import { AlwaysExpandedArrayLayout } from './AlwaysExpandedArrayLayout'
|
|
||||||
import {
|
import {
|
||||||
OutlinedTextRenderer,
|
OutlinedTextRenderer,
|
||||||
OutlinedNumberRenderer,
|
OutlinedNumberRenderer,
|
||||||
@@ -43,7 +42,7 @@ SchemaErrorBoundary.propTypes = {
|
|||||||
// params.missingProperty. We transform them to point to the field directly
|
// params.missingProperty. We transform them to point to the field directly
|
||||||
// (e.g., "/users/1/username") so JSONForms displays them under the correct input.
|
// (e.g., "/users/1/username") so JSONForms displays them under the correct input.
|
||||||
const ajv = new Ajv({
|
const ajv = new Ajv({
|
||||||
useDefaults: true,
|
useDefaults: false,
|
||||||
allErrors: true,
|
allErrors: true,
|
||||||
verbose: true,
|
verbose: true,
|
||||||
jsonPointers: true,
|
jsonPointers: true,
|
||||||
@@ -135,7 +134,6 @@ const customRenderers = [
|
|||||||
OutlinedNumberRenderer,
|
OutlinedNumberRenderer,
|
||||||
OutlinedEnumRenderer,
|
OutlinedEnumRenderer,
|
||||||
OutlinedOneOfEnumRenderer,
|
OutlinedOneOfEnumRenderer,
|
||||||
AlwaysExpandedArrayLayout,
|
|
||||||
// Then all the standard material renderers
|
// Then all the standard material renderers
|
||||||
...materialRenderers,
|
...materialRenderers,
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user