diff --git a/packages/jsonforms-renderers/src/renderers/SpreadsheetControl.tsx b/packages/jsonforms-renderers/src/renderers/SpreadsheetControl.tsx index 678629c..c736bb5 100644 --- a/packages/jsonforms-renderers/src/renderers/SpreadsheetControl.tsx +++ b/packages/jsonforms-renderers/src/renderers/SpreadsheetControl.tsx @@ -1,7 +1,7 @@ import { withJsonFormsControlProps } from '@jsonforms/react' import type { ControlProps, JsonSchema } from '@jsonforms/core' -import { Box, Card, Flex, IconButton, Spinner, Table, Text } from '@radix-ui/themes' -import { UploadIcon, FileTextIcon, Cross2Icon, CheckCircledIcon, ExclamationTriangleIcon } from '@radix-ui/react-icons' +import { Box, Flex, IconButton, Spinner, Table, Text, Tooltip } from '@radix-ui/themes' +import { UploadIcon, Cross2Icon, ExclamationTriangleIcon } from '@radix-ui/react-icons' import { useCallback, useRef, useState, type ChangeEvent, type DragEvent } from 'react' import { useClearWhenHidden } from '../hooks/useClearWhenHidden' import { getErrorMessage } from '../utils/error' @@ -67,13 +67,22 @@ const SpreadsheetControl = ({ required, schema, enabled, + readonly, errors, visible = true, }: SpreadsheetControlProps) => { useClearWhenHidden(visible, path, handleChange, null) const isValid = !errors || errors.length === 0 - const isEnabled = enabled !== false + // `enabled` and `readonly` are independently computed by @jsonforms/core + // (see mapStateToControlProps) — `enabled` only happens to reflect a schema + // `readOnly: true` under this library's default `separateReadonlyFromDisabled: + // false` config, and never reflects a uischema READONLY *rule* in any config. + // Since this is a generic, reusable renderer whose consuming app may use + // either, check both explicitly rather than relying on that incidental fold-in. + // For this control, "disabled" and "readonly" mean the same thing: show + // whatever data exists, but don't allow uploading a replacement or removing it. + const canEdit = enabled !== false && readonly !== true const xSpreadsheet: XSpreadsheetOptions = schema?.['x-spreadsheet'] ?? {} const xEvaluate: FormulaConfigEntry[] = schema?.['x-evaluate'] ?? EMPTY_FORMULAS @@ -155,12 +164,12 @@ const SpreadsheetControl = ({ return null } - if (!isEnabled && !hasValue) return null + if (!canEdit && !hasValue) return null const handleDrag = (e: DragEvent) => { e.preventDefault() e.stopPropagation() - if (!isEnabled) return + if (!canEdit) return setDragActive(e.type === 'dragenter' || e.type === 'dragover') } @@ -168,7 +177,7 @@ const SpreadsheetControl = ({ e.preventDefault() e.stopPropagation() setDragActive(false) - if (!isEnabled) return + if (!canEdit) return if (e.dataTransfer.files?.[0]) void processFile(e.dataTransfer.files[0]) } @@ -180,7 +189,7 @@ const SpreadsheetControl = ({ } const handleRemove = () => { - if (!isEnabled) return + if (!canEdit) return setLocalMatrix(null) setError(null) setStatus('empty') @@ -206,47 +215,58 @@ const SpreadsheetControl = ({ {/* ── Header row ── */} - - {label} - {required && *} - + + + {label} + {required && *} + + {hasValue && canEdit && ( + <> + + inputRef.current?.click()} + aria-label="Replace spreadsheet" + > + + + + + + + + + + )} + {formatBytes(maxSize)} max · {formatAccept(accept)} - {/* ── Current-file summary row ── */} - {hasValue && ( - - - - - - - - Spreadsheet uploaded - - - - {isEnabled && ( - - - - )} - - + {/* A replacement upload's validation/parse error has nowhere else to + render once a file already exists — the dropzone (the only other + place `error` is shown) is hidden whenever `hasValue` is true. */} + {hasValue && error && ( + + {error} + )} - {/* ── Drop zone — replaces the file, never edits it in place ── */} - {isEnabled && ( + {/* Hidden file input — triggered by the empty-state dropzone below and + by the compact "replace" button in the header once a file exists. */} + + + {/* ── Drop zone — empty state only; once a file exists, the compact + header controls above handle replace/remove instead ── */} + {canEdit && !hasValue && (
- {status === 'parsing' ? ( <> @@ -295,7 +314,7 @@ const SpreadsheetControl = ({ <> - {hasValue ? 'Click to upload a replacement' : 'Click to upload or drag and drop'} + Click to upload or drag and drop {formatBytes(maxSize)} max · {formatAccept(accept)}