navidrome/ui/src/plugin/ToggleEnabledSwitch.jsx
Deluan Quintão f1e75c40dc
feat(plugins): add JSONForms-based plugin configuration UI (#4911)
* feat(plugins): add JSONForms schema for plugin configuration

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: enhance error handling by formatting validation errors with field names

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: enforce required fields in config validation and improve error handling

Signed-off-by: Deluan <deluan@navidrome.org>

* format JS code

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: add config schema validation and enhance manifest structure

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: refactor plugin config parsing and add unit tests

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: add config validation error message in Portuguese

* feat: enhance AlwaysExpandedArrayLayout with description support and improve array control testing

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: update Discord Rust plugin configuration to use JSONForm for user tokens and enhance schema validation

Signed-off-by: Deluan <deluan@navidrome.org>

* fix: resolve React Hooks linting issues in plugin UI components

* Apply suggestions from code review

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* format code

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: migrate schema validation to use santhosh-tekuri/jsonschema and improve error formatting

Signed-off-by: Deluan <deluan@navidrome.org>

* address PR comments

Signed-off-by: Deluan <deluan@navidrome.org>

* fix flaky test

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: enhance array layout and configuration handling with AJV defaults

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: implement custom tester to exclude enum arrays from AlwaysExpandedArrayLayout

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: add error boundary for schema rendering and improve error messages

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: refine non-enum array control logic by utilizing JSONForms schema resolution

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: add error styling to ToggleEnabledSwitch for disabled state

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: adjust label positioning and styling in SchemaConfigEditor for improved layout

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: implement outlined input controls renderers to replace custom fragile CSS

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: remove margin from last form control inside array items for better spacing

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: enhance AJV error handling to transform required errors for field-level validation

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: set default value for User Tokens in manifest.json to improve user experience

Signed-off-by: Deluan <deluan@navidrome.org>

* format

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: add margin to outlined input controls for improved spacing

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: remove redundant margin rule for last form control in array items

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: adjust font size of label elements in SchemaConfigEditor for improved readability

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-19 20:51:00 -05:00

198 lines
5.2 KiB
JavaScript

import React, { useCallback, useMemo } from 'react'
import {
useUpdate,
useNotify,
useRefresh,
useRecordContext,
useTranslate,
useResourceContext,
} from 'react-admin'
import Switch from '@material-ui/core/Switch'
import { makeStyles } from '@material-ui/core/styles'
import { Tooltip, FormControlLabel } from '@material-ui/core'
import PropTypes from 'prop-types'
const useStyles = makeStyles((theme) => ({
enabledSwitch: {
'& .MuiSwitch-colorSecondary.Mui-checked': {
color: theme.palette.success?.main || theme.palette.primary.main,
},
'& .MuiSwitch-colorSecondary.Mui-checked + .MuiSwitch-track': {
backgroundColor:
theme.palette.success?.main || theme.palette.primary.main,
},
},
errorSwitch: {
'& .MuiSwitch-thumb': {
backgroundColor: theme.palette.warning.main,
},
'& .MuiSwitch-track': {
backgroundColor: theme.palette.warning.light,
opacity: 0.7,
},
},
}))
/**
* Shared toggle switch for enabling/disabling plugins.
* Used in both PluginList (compact) and PluginShow (with label).
*
* @param {Object} props
* @param {boolean} [props.showLabel=false] - Whether to show the enable/disable label
* @param {string} [props.size='small'] - Switch size ('small' or 'medium')
* @param {Object} [props.manifest=null] - Parsed manifest object for permission checking
*/
const ToggleEnabledSwitch = ({
showLabel = false,
size = 'small',
manifest = null,
}) => {
const resource = useResourceContext()
const record = useRecordContext()
const notify = useNotify()
const refresh = useRefresh()
const translate = useTranslate()
const classes = useStyles()
const [toggleEnabled, { loading }] = useUpdate(
resource,
record?.id,
{ enabled: !record?.enabled },
record,
{
undoable: false,
onSuccess: () => {
refresh()
notify(
record?.enabled
? 'resources.plugin.notifications.disabled'
: 'resources.plugin.notifications.enabled',
'info',
)
},
onFailure: (error) => {
refresh()
notify(
error?.message || 'resources.plugin.notifications.error',
'warning',
)
},
},
)
const handleClick = useCallback(
(e) => {
e.stopPropagation()
toggleEnabled()
},
[toggleEnabled],
)
const hasError = !!record?.lastError
// Check if users permission is required but not configured
const usersPermissionRequired = useMemo(() => {
if (!manifest?.permissions?.users) return false
if (record?.allUsers) return false
// Check if users array is empty or not set
if (!record?.users) return true
try {
const users = JSON.parse(record.users)
return users.length === 0
} catch {
return true
}
}, [manifest, record?.allUsers, record?.users])
// Check if library permission is required but not configured
const libraryPermissionRequired = useMemo(() => {
if (!manifest?.permissions?.library) return false
if (record?.allLibraries) return false
// Check if libraries array is empty or not set
if (!record?.libraries) return true
try {
const libraries = JSON.parse(record.libraries)
return libraries.length === 0
} catch {
return true
}
}, [manifest, record?.allLibraries, record?.libraries])
const permissionRequired =
usersPermissionRequired || libraryPermissionRequired
const isDisabled =
loading || hasError || (permissionRequired && !record?.enabled)
const tooltipTitle = useMemo(() => {
if (hasError) {
return translate('resources.plugin.actions.disabledDueToError')
}
if (usersPermissionRequired && !record?.enabled) {
return translate('resources.plugin.actions.disabledUsersRequired')
}
if (libraryPermissionRequired && !record?.enabled) {
return translate('resources.plugin.actions.disabledLibrariesRequired')
}
if (!showLabel) {
return translate(
record?.enabled
? 'resources.plugin.actions.disable'
: 'resources.plugin.actions.enable',
)
}
return ''
}, [
hasError,
usersPermissionRequired,
libraryPermissionRequired,
showLabel,
record?.enabled,
translate,
])
const switchElement = (
<Switch
checked={record?.enabled ?? false}
onClick={handleClick}
disabled={isDisabled}
className={isDisabled ? classes.errorSwitch : classes.enabledSwitch}
size={size}
color="primary"
/>
)
if (showLabel) {
const showTooltip = hasError || (permissionRequired && !record?.enabled)
return (
<Tooltip
title={tooltipTitle}
disableHoverListener={!showTooltip}
disableFocusListener={!showTooltip}
>
<FormControlLabel
control={switchElement}
label={translate(
record?.enabled
? 'resources.plugin.actions.disable'
: 'resources.plugin.actions.enable',
)}
/>
</Tooltip>
)
}
return (
<Tooltip title={tooltipTitle}>
<span>{switchElement}</span>
</Tooltip>
)
}
ToggleEnabledSwitch.propTypes = {
showLabel: PropTypes.bool,
size: PropTypes.oneOf(['small', 'medium']),
manifest: PropTypes.object,
}
export default ToggleEnabledSwitch