-
-
setColumnData({ ...columnData, title: e.target.value }),
+ callbackDeps);
+
+ const changeExpression = useCallback(
+ e => setColumnData({ ...columnData, expression: e.target.value }),
+ callbackDeps);
+
+ const changeSize = useCallback(
+ e => {
+ const value = Number(e.target.value);
+
+ if (!isNaN(value) && value >= 0)
+ setColumnData({ ...columnData, size: value });
+ },
+ callbackDeps);
+
+ const changeAlign = useCallback(
+ e => setColumnData({ ...columnData, align: e.target.value }),
+ callbackDeps);
+
+ const changeBold = useCallback(
+ e => setColumnData({ ...columnData, bold: e.target.checked }),
+ callbackDeps);
+
+ const changeItalic = useCallback(
+ e => setColumnData({ ...columnData, italic: e.target.checked }),
+ callbackDeps);
+
+ const changeSmall = useCallback(
+ e => setColumnData({ ...columnData, small: e.target.checked }),
+ callbackDeps);
+
+ const handleOpen = useCallback(
+ () => {
+ setColumnData({ ...model.getColumn(columnId) });
+ setDialogOpen(true);
+ },
+ callbackDeps)
+
+ const handleOk = useCallback(
+ () => {
+ model.updateColumn(columnData);
+ setColumnData(null);
+ setDialogOpen(false);
+ },
+ callbackDeps);
+
+ const handleCancel = useCallback(
+ () => {
+ setColumnData(null);
+ setDialogOpen(false);
+ },
+ callbackDeps);
+
+ const icon =
;
+
+ if (!dialogOpen)
+ return icon;
+
+ return <>
+ {icon}
+
+
+
+ >;
}
-ColumnEditorDialog.propTypes = {
- isOpen: PropTypes.bool.isRequired,
- column: PropTypes.object.isRequired,
- onOk: PropTypes.func.isRequired,
- onCancel: PropTypes.func.isRequired,
- onUpdate: PropTypes.func.isRequired,
+ColumnEditButton.propTypes = {
+ columnId: PropTypes.number.isRequired,
};
-function ColumnEditorDragHandle_()
+function ColumnDeleteButton(props)
{
- return
;
+ const { columnId } = props;
+ const model = useColumnsSettingsModel();
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const column = useColumn(columnId);
+
+ const handleOpen = useCallback(() => setDialogOpen(true), []);
+
+ const handleOk = useCallback(
+ () => {
+ model.removeColumn(columnId)
+ setDialogOpen(false);
+ },
+ [columnId]);
+
+ const handleCancel = useCallback(() => setDialogOpen(false), [])
+
+ const icon =
;
+
+ if (!dialogOpen)
+ return icon;
+
+ const columnName = column.lineBreak ? 'line break' : `column ${column.title}`;
+
+ return <>
+ {icon}
+
+ >
}
-const ColumnEditorDragHandle = SortableHandle(ColumnEditorDragHandle_);
+ColumnDeleteButton.propTypes = {
+ columnId: PropTypes.number.isRequired,
+};
-class ColumnEditor_ extends React.PureComponent
+function EditableColumn(props)
{
- constructor(props)
- {
- super(props);
-
- this.state = Object.assign(
- { deleteDialogOpen: false }, ColumnEditor_.editDialogClosed());
-
- bindHandlers(this);
- }
-
- static editDialogClosed()
- {
- return {
- editDialogOpen: false,
- editedColumn: {
- title: '',
- expression: '',
- size: 1
- }
- };
- }
-
- handleEdit()
+ const { columnId } = props;
+ const column = useColumn(columnId);
+
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ } = useSortable({ id: columnId });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ };
+
+ let editButton;
+ let columnInfo;
+
+ if (column.lineBreak)
{
- this.setState({
- editDialogOpen: true,
- editedColumn: structuredClone(this.props.column),
- });
+ columnInfo =
+ {'\u2E3A Line break \u2E3A'}
+
;
}
-
- handleEditOk()
+ else
{
- this.props.onUpdate(this.props.columnIndex, this.state.editedColumn);
- this.setState(ColumnEditor_.editDialogClosed);
- }
+ columnInfo =
+ { column.title }
+ { column.expression }
+
;
- handleEditCancel()
- {
- this.setState(ColumnEditor_.editDialogClosed);
+ editButton =
;
}
- handleEditUpdate(patch)
- {
- this.setState(state => ({ editedColumn: Object.assign({}, state.editedColumn, patch) }));
- }
-
- handleDelete()
- {
- this.setState({ deleteDialogOpen: true });
- }
-
- handleDeleteOk()
- {
- this.props.onDelete(this.props.columnIndex);
- this.setState({ deleteDialogOpen: false });
- }
-
- handleDeleteCancel()
- {
- this.setState({ deleteDialogOpen: false });
- }
-
- render()
- {
- const { column } = this.props;
- const { deleteDialogOpen, editDialogOpen, editedColumn } = this.state;
-
- let editButton;
- let columnInfo;
- let columnName;
-
- if (column.lineBreak)
- {
- columnInfo =
- {'\u2E3A Line break \u2E3A'}
-
;
-
- columnName = 'line break';
- }
- else
- {
- columnInfo =
- { column.title }
- { column.expression }
-
;
-
- editButton =
;
- columnName = `column ${column.title}`;
- }
-
- return (
-
-
-
+ return (
+
+ { columnInfo }
+
+
+ { editButton }
+
- { columnInfo }
-
-
-
- );
- }
+
+ );
}
-ColumnEditor_.propTypes = {
- columnIndex: PropTypes.number.isRequired,
- column: PropTypes.object.isRequired,
- onUpdate: PropTypes.func.isRequired,
- onDelete: PropTypes.func.isRequired,
+EditableColumn.propTypes = {
+ columnId: PropTypes.number.isRequired,
};
-const ColumnEditor = SortableElement(ColumnEditor_);
-
-class ColumnEditorList_ extends React.PureComponent
+export function ColumnsSettings()
{
- static contextType = ServiceContext;
-
- constructor(props, context)
- {
- super(props, context);
- this.state = this.getStateFromModel();
- bindHandlers(this);
- }
-
- getStateFromModel()
- {
- const { columns } = this.context.columnsSettingsModel;
- return { columns };
- }
+ const sensors = useDefaultSensors();
+ const model = useColumnsSettingsModel();
+ const columns = useColumnList();
+ const columnElements = columns.map(c =>
);
- handleUpdate(index, patch)
- {
- this.context.columnsSettingsModel.updateColumn(index, patch);
- }
+ const handleDragEnd = useCallback(e => model.moveColumn(e.active.id, e.over.id), []);
- handleDelete(index)
- {
- this.context.columnsSettingsModel.removeColumn(index);
- }
+ useDispose(() => model.applyChanges());
- render()
- {
- const editors = this.state.columns.map((c, i) => (
-
- ));
-
- return (
+ return (
+
- {editors}
+ {columnElements}
- );
- }
-}
-
-const ColumnEditorList = SortableContainer(ModelBinding(
- ColumnEditorList_, { columnsSettingsModel: 'change' }));
-
-export default class ColumnsSettings extends React.PureComponent
-{
- static contextType = ServiceContext;
-
- constructor(props)
- {
- super(props);
-
- this.state = {};
-
- bindHandlers(this);
- }
-
- handleSortEnd(e)
- {
- this.context.columnsSettingsModel.moveColumn(e.oldIndex, e.newIndex);
- }
-
- componentWillUnmount()
- {
- this.context.columnsSettingsModel.applyChanges();
- }
-
- render()
- {
- return (
-
- );
- }
+
+ );
}
diff --git a/js/webui/src/columns_settings_model.js b/js/webui/src/columns_settings_model.js
index b985a049..d115d1e4 100644
--- a/js/webui/src/columns_settings_model.js
+++ b/js/webui/src/columns_settings_model.js
@@ -1,4 +1,4 @@
-import { arrayMove } from 'react-sortable-hoc';
+import { arrayMove } from '@dnd-kit/sortable';
import { arrayRemove } from './utils.js';
import ModelBase from './model_base.js';
import { MediaSize } from './settings_model.js';
@@ -56,6 +56,16 @@ export default class ColumnsSettingsModel extends ModelBase
this.revertChanges();
}
+ getColumn(id)
+ {
+ return this.columns.find(c => c.id === id);
+ }
+
+ getColumnIndex(id)
+ {
+ return this.columns.findIndex(c => c.id === id);
+ }
+
setLayout(mediaSize)
{
this.layout = mediaSize;
@@ -72,8 +82,8 @@ export default class ColumnsSettingsModel extends ModelBase
setColumns(columns)
{
- this.columns = columns;
this.config[this.layout].columns = columns;
+ this.columns = columns;
this.emit('change');
}
@@ -108,20 +118,37 @@ export default class ColumnsSettingsModel extends ModelBase
this.setColumns([... this.columns, column]);
}
- updateColumn(index, patch)
+ updateColumn(column)
{
+ const index = this.getColumnIndex(column.id);
+ if (index < 0)
+ return;
+
const newColumns = [... this.columns];
- newColumns[index] = Object.assign({}, this.columns[index], patch);
+ newColumns[index] = structuredClone(column);
this.setColumns(newColumns);
}
- moveColumn(oldIndex, newIndex)
+ moveColumn(oldId, newId)
{
+ if (oldId === newId)
+ return;
+
+ const oldIndex = this.getColumnIndex(oldId);
+ const newIndex = this.getColumnIndex(newId);
+
+ if (oldIndex < 0 || newIndex < 0)
+ return;
+
this.setColumns(arrayMove(this.columns, oldIndex, newIndex));
}
- removeColumn(index)
+ removeColumn(id)
{
+ const index = this.getColumnIndex(id);
+ if (index < 0)
+ return;
+
this.setColumns(arrayRemove(this.columns, index));
}
}
diff --git a/js/webui/src/elements.js b/js/webui/src/elements.js
index 57828769..b76f7be8 100644
--- a/js/webui/src/elements.js
+++ b/js/webui/src/elements.js
@@ -1,4 +1,4 @@
-import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
+import React, { forwardRef, useLayoutEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types'
import spriteSvg from 'open-iconic/sprite/sprite.svg'
import { generateElementId, makeClassName } from './dom_utils.js';
@@ -19,7 +19,7 @@ function makeClickHandler(callback)
};
}
-export function Icon(props)
+export const Icon = forwardRef(function Icon(props, ref)
{
const { name, className } = props;
const fullClassName = 'icon icon-' + name + (className ? ' ' + className : '');
@@ -30,11 +30,11 @@ export function Icon(props)
const href = `${spriteSvg}#${name}`;
return (
-