/** * String key type renderer — exact port of Angular key-string.component. * * View mode: Upload, Download, JSON View, Copy, Format JSON, JSON Editor, Edit * Edit mode: Validate JSON toggle, Cancel, Upload, Save * Display: truncated value with format toggle, click to edit */ import { useState, useCallback } from 'react' import { Box, Button, Tooltip, TextField, Switch, FormControlLabel, useMediaQuery } from '@mui/material' import { Upload, Download, TableChart, ContentCopy, FormatLineSpacing, Description, Edit, Cancel, Save, } from '@mui/icons-material' import { useI18nStore } from '../../../stores/i18n.store' import { useRedisStateStore } from '../../../stores/redis-state.store' import { trackPage } from '../../../stores/analytics' import { useSettingsStore } from '../../../stores/settings.store' import { useCommonStore } from '../../../stores/common.store' import { useOverlayStore } from '../../../stores/overlay.store' import { request } from '../../../stores/socket.service' import { KeyTypeProps, formatValue, truncateDisplay, isTruncated, copyValue, downloadBuffer } from './key-type-base' import JsonViewDialog from '../../../dialogs/JsonViewDialog' import JsonEditorDialog from '../../../dialogs/JsonEditorDialog' export default function KeyString({ response, value: initValue, valueBuffer: initBuffer, keyName, valueFormat, onRefresh }: KeyTypeProps) { const strings = useI18nStore(s => s.strings) const connection = useRedisStateStore(s => s.connection) const settings = useSettingsStore() const { toast, generalHandleError, confirm } = useCommonStore() const overlay = useOverlayStore() const isGtSm = useMediaQuery('(min-width: 960px)') const isReadonly = connection?.readonly === true const [editable, setEditable] = useState(false) const [buffer, setBuffer] = useState(false) const [validateJson, setValidateJson] = useState(false) const [value, setValue] = useState(initValue) const [valueBuffer, setValueBuffer] = useState(initBuffer) const [originalValue, setOriginalValue] = useState(null) const [jsonViewOpen, setJsonViewOpen] = useState(false) const [jsonEditorOpen, setJsonEditorOpen] = useState(false) // --- Actions matching Angular exactly --- const edit = useCallback(() => { if (typeof value === 'string' && value.length >= settings.maxValueAsBuffer) { setBuffer(true) setOriginalValue(structuredClone(valueBuffer)) } else { setBuffer(false) setOriginalValue(structuredClone(value)) } setEditable(true) }, [value, valueBuffer, settings.maxValueAsBuffer]) const cancelEdit = useCallback(() => { if (buffer) setValueBuffer(originalValue) else setValue(originalValue) setEditable(false) setBuffer(false) }, [buffer, originalValue]) const save = useCallback(async () => { const v = buffer ? valueBuffer : value try { if (validateJson) JSON.parse(v) overlay.show({ message: strings?.intention?.save }) await request({ action: 'key-set', payload: { type: response?.type, key: keyName, value: v } }) trackPage('/key-set') setEditable(false) setBuffer(false) onRefresh() } catch (e) { generalHandleError(e) } finally { overlay.hide() } }, [buffer, value, valueBuffer, validateJson, response, keyName, strings, onRefresh, generalHandleError]) const setBufferUpload = useCallback(() => { const input = document.createElement('input') input.type = 'file' input.onchange = async () => { const file = input.files?.[0] if (!file) return const reader = new FileReader() reader.onerror = (err) => generalHandleError(err) reader.onload = async (e: any) => { const arrayBuffer = e.target.result try { if (editable) { await confirm({ message: strings?.confirm?.uploadBuffer }) if (buffer) setValueBuffer(arrayBuffer) else setValue(arrayBuffer) toast(strings?.confirm?.uploadBufferDone) return } await confirm({ message: strings?.confirm?.uploadBuffer }) overlay.show() await request({ action: 'key-set', payload: { type: response?.type, value: arrayBuffer, key: keyName } }) trackPage('/key-set') toast(strings?.confirm?.uploadBufferDoneAndSave) onRefresh() } catch (e) { generalHandleError(e) } finally { overlay.hide() } } reader.readAsArrayBuffer(file) } input.click() }, [editable, buffer, response, keyName, strings, confirm, toast, generalHandleError, onRefresh]) const jsonViewer = useCallback(() => setJsonViewOpen(true), []) const jsonEditor = useCallback(() => setJsonEditorOpen(true), []) const formatJson = useCallback(async () => { try { const formatted = JSON.stringify(JSON.parse(value), null, settings.jsonFormat || 2) setValue(formatted) overlay.show({ message: strings?.intention?.save }) await request({ action: 'key-set', payload: { type: response?.type, key: keyName, value: formatted } }) trackPage('/key-set') onRefresh() } catch { toast(strings?.label?.jsonViewNotParsable) } finally { overlay.hide() } }, [value, settings.jsonFormat, response, keyName, strings, onRefresh]) const copyVal = useCallback(() => copyValue(value), [value]) const downloadBufferFile = useCallback(() => downloadBuffer(valueBuffer, keyName), [valueBuffer, keyName]) const bufferDisplay = (): string => { if (valueBuffer?.byteLength !== undefined) { return '(' + (settings.prettyBytes?.(valueBuffer.byteLength) ?? `${valueBuffer.byteLength} bytes`) + ')' } return '' } // --- Responsive button (matches Angular @if(isGtSm) pattern) --- const Btn = ({ icon, label, color = 'primary' as const, onClick }: { icon: React.ReactNode; label: string; color?: 'primary' | 'secondary' | 'error'; onClick: () => void }) => isGtSm ? ( ) : ( ) return ( {/* View mode actions — matches Angular template lines 1-88 */} {!editable && ( {!isReadonly && ( } label={strings?.intention?.setBuffer} onClick={setBufferUpload} /> )} } label={strings?.intention?.downloadBuffer} color="secondary" onClick={downloadBufferFile} /> } label={strings?.intention?.jsonViewShow} color="secondary" onClick={jsonViewer} /> } label={strings?.intention?.copy} color="secondary" onClick={copyVal} /> {!isReadonly && ( } label={strings?.intention?.formatJson} onClick={formatJson} /> )} } label={strings?.intention?.jsonViewEditor} onClick={jsonEditor} /> {!isReadonly && ( } label={strings?.intention?.edit} onClick={edit} /> )} )} {/* Edit mode actions — matches Angular template lines 90-136 */} {editable && ( {!isReadonly && ( setValidateJson(v)} color="secondary" />} label={strings?.label?.validateJson} /> )} } label={strings?.intention?.cancel} color="error" onClick={cancelEdit} /> {!isReadonly && ( <> } label={strings?.intention?.setBuffer} onClick={setBufferUpload} /> } label={strings?.intention?.save} onClick={save} /> )} )} {/* Value display / editor — matches Angular template lines 138-164 */} {editable ? ( {/* Buffer info — shown when value is ArrayBuffer or in buffer mode */} {(String(value) === '[object ArrayBuffer]') && ( {typeof strings?.label?.isBuffer === 'function' ? strings.label.isBuffer({ maxValueAsBuffer: settings.prettyBytes(settings.maxValueAsBuffer) }) : ''} {' '}{bufferDisplay()} )} {buffer && ( {typeof strings?.label?.isBuffer === 'function' ? strings.label.isBuffer({ maxValueAsBuffer: settings.prettyBytes(settings.maxValueAsBuffer) }) : ''} {' '}{bufferDisplay()} )} {buffer ? ( setValueBuffer(e.target.value)} slotProps={{ input: { sx: { fontFamily: "'Roboto Mono', monospace", fontSize: 13 } } }} /> ) : ( setValue(e.target.value)} slotProps={{ input: { sx: { fontFamily: "'Roboto Mono', monospace", fontSize: 13 } } }} /> )} ) : ( {settings.maxValueDisplay === -1 ? ( {strings?.label?.hiddenUntilEdit} ) : ( {formatValue(truncateDisplay(typeof value === 'string' ? value : ''), valueFormat)} {isTruncated(value) && ...} )} )} {/* Dialogs */} setJsonViewOpen(false)} /> { setJsonEditorOpen(false) if (result?.obj) { setValue(result.obj) overlay.show({ message: strings?.intention?.save }) request({ action: 'key-set', payload: { type: response?.type, key: keyName, value: result.obj } }) .then(() => { trackPage('/key-set'); onRefresh() }) .catch(e => generalHandleError(e)) .finally(() => overlay.hide()) } }} /> ) }