import { Component, createRef } from 'react';
import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { injectMuiTheme } from './injectMuiTheme';
import { enqueueSnackbar } from 'notistack';
import { backendErrorFrom } from '../../utils/backendError';
import { panelTilePaperSx, TILE_ACTION_PADDING, tileActionIconSx, tileActionSx } from './panelTileLayout';
function valuesEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
/**
* Panel tile with centralized inline-edit chrome.
* Pass `onSave` to enable edit UI; omit it for read-only tiles (no edit icon).
*/
class EditablePanelTileBase extends Component {
constructor(props) {
super(props);
this.inputRef = createRef();
this.state = {
editing: false,
draft: null,
saving: false,
error: null,
};
}
componentDidUpdate(_prevProps, prevState) {
if (!prevState.editing && this.state.editing) {
this.inputRef.current?.focus();
}
}
getSavedValue() {
return this.props.value;
}
isDirty() {
const { draft, editing } = this.state;
if (!editing || draft == null) return false;
if (this.props.isDirty) {
return this.props.isDirty(draft, this.getSavedValue());
}
return !valuesEqual(draft, this.getSavedValue());
}
canSave() {
const { draft, saving } = this.state;
if (saving || !this.isDirty() || draft == null) return false;
if (this.props.canSave) {
return this.props.canSave(draft, this.getSavedValue());
}
return true;
}
handleStartEdit = (event) => {
event.stopPropagation();
this.setState({
editing: true,
draft: structuredClone(this.getSavedValue()),
error: null,
});
};
handleCancel = (event) => {
event?.stopPropagation();
this.setState({
editing: false,
draft: null,
error: null,
});
};
handleDraftChange = (nextDraft) => {
this.setState({ draft: nextDraft, error: null });
};
handleEditKeyDown = (event) => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
this.handleCancel();
return;
}
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
event.stopPropagation();
if (this.canSave()) {
void this.handleSave(event);
}
}
};
handleSave = async (event) => {
event?.stopPropagation();
if (!this.canSave()) return;
const { onSave } = this.props;
const { draft } = this.state;
this.setState({ saving: true, error: null });
try {
await onSave(draft);
this.setState({ editing: false, draft: null, saving: false, error: null });
enqueueSnackbar('Gespeichert.', { variant: 'success' });
} catch (err) {
const normalized = backendErrorFrom(err);
this.setState({ saving: false, error: normalized.message });
enqueueSnackbar(normalized.message, { variant: 'error' });
}
};
renderTitle() {
const { title, onSave } = this.props;
if (!title) return null;
return (
{title}
);
}
renderActions() {
const { onSave } = this.props;
if (!onSave) return null;
const { editing, saving } = this.state;
const iconSx = tileActionIconSx;
if (!editing) {
return (
<>
>
);
}
return (
<>
{saving ? (
) : (
)}
>
);
}
render() {
const { onSave, theme, wide = 1, sx, minHeight = 120, tileKey, renderDisplay, renderEdit } =
this.props;
const { editing, draft, error } = this.state;
const savedValue = this.getSavedValue();
const pending = this.isDirty();
const actionPadding = onSave ? TILE_ACTION_PADDING : 0;
return (
{this.renderActions()}
{this.renderTitle()}
{editing && draft != null
? renderEdit(draft, this.handleDraftChange, this.inputRef)
: renderDisplay(savedValue)}
{error ? (
{error}
) : null}
);
}
}
export const EditablePanelTile = injectMuiTheme(EditablePanelTileBase);