Modernize to 2025 standards: React 19, Vite 6, ESLint 9, @dnd-kit migration

This commit is contained in:
alexsparkes
2025-10-27 23:19:35 +00:00
parent 7d432dc6f5
commit 04679ba1f6
12 changed files with 591 additions and 261 deletions

View File

@@ -96,17 +96,31 @@ class Favourite extends PureComponent {
}
}
componentDidMount() {
this.updateTooltip();
}
componentDidUpdate(prevProps, prevState) {
if (prevState.favourited !== this.state.favourited) {
this.updateTooltip();
}
}
updateTooltip() {
if (this.props.tooltipText) {
this.props.tooltipText(
localStorage.getItem('favourite')
? variables.getMessage('widgets.quote.unfavourite')
: variables.getMessage('widgets.quote.favourite'),
);
}
}
render() {
if (localStorage.getItem('backgroundType') === 'colour') {
return null;
}
this.props.tooltipText(
localStorage.getItem('favourite')
? variables.getMessage('widgets.quote.unfavourite')
: variables.getMessage('widgets.quote.favourite'),
);
return this.state.favourited;
}
}

View File

@@ -1,6 +1,21 @@
import { useState, useEffect, useCallback } from 'react';
import { MdOutlineDragIndicator } from 'react-icons/md';
import { sortableContainer, sortableElement } from '@muetab/react-sortable-hoc';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { toast } from 'react-toastify';
import variables from 'config/variables';
@@ -22,16 +37,21 @@ const widget_name = {
message: variables.getMessage('modals.main.settings.sections.message.title'),
};
const SortableItem = sortableElement(({ value }) => (
<li className="sortableItem">
{widget_name[value]}
<MdOutlineDragIndicator />
</li>
));
const SortableItem = ({ id }) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const SortableContainer = sortableContainer(({ children }) => (
<ul className="sortableContainer">{children}</ul>
));
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<li ref={setNodeRef} style={style} {...attributes} {...listeners} className="sortableItem">
{widget_name[id]}
<MdOutlineDragIndicator />
</li>
);
};
const Overview = () => {
const [items, setItems] = useState(
@@ -55,15 +75,23 @@ const Overview = () => {
const [newsDone, setNewsDone] = useState(false);
const [loading, setLoading] = useState(true);
const arrayMove = (array, oldIndex, newIndex) => {
const result = Array.from(array);
const [removed] = result.splice(oldIndex, 1);
result.splice(newIndex, 0, removed);
return result;
};
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const onSortEnd = ({ oldIndex, newIndex }) => {
setItems((prevItems) => arrayMove(prevItems, oldIndex, newIndex));
const handleDragEnd = (event) => {
const { active, over } = event;
if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
const reset = () => {
@@ -159,19 +187,18 @@ const Overview = () => {
<span className="title">
{variables.getMessage('modals.main.settings.sections.order.title')}
</span>
<SortableContainer
onSortEnd={onSortEnd}
lockAxis="y"
lockToContainerEdges
disableAutoscroll
>
{items.map((value, index) => {
if (!enabled(value)) {
return null;
}
return <SortableItem key={`item-${value}`} index={index} value={value} />;
})}
</SortableContainer>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
<ul className="sortableContainer">
{items.map((value) => {
if (!enabled(value)) {
return null;
}
return <SortableItem key={value} id={value} />;
})}
</ul>
</SortableContext>
</DndContext>
</div>
</div>
</>

View File

@@ -14,12 +14,65 @@ import { Tooltip } from 'components/Elements';
import Checkbox from '@mui/material/Checkbox';
import { shift, useFloating } from '@floating-ui/react-dom';
import { sortableContainer, sortableElement, sortableHandle } from '@muetab/react-sortable-hoc';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import EventBus from 'utils/eventbus';
const SortableItem = sortableElement(({ value }) => <div>{value}</div>);
const SortableContainer = sortableContainer(({ children }) => <div>{children}</div>);
const SortableHandle = sortableHandle(() => <MdOutlineDragIndicator />);
const DragHandle = () => (
<div className="todo-drag-handle" {...arguments[0]}>
<MdOutlineDragIndicator />
</div>
);
const SortableItem = ({ id, children }) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={style}>
{typeof children === 'function' ? children({ attributes, listeners }) : children}
</div>
);
};
const SortableList = ({ items, onDragEnd, children }) => {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
<div>{children}</div>
</SortableContext>
</DndContext>
);
};
class Todo extends PureComponent {
constructor() {
@@ -70,10 +123,17 @@ class Todo extends PureComponent {
return result;
}
onSortEnd = ({ oldIndex, newIndex }) => {
this.setState({
todo: this.arrayMove(this.state.todo, oldIndex, newIndex),
});
handleDragEnd = (event) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = Number(active.id);
const newIndex = Number(over.id);
this.setState({
todo: arrayMove(this.state.todo, oldIndex, newIndex),
});
}
};
showTodo() {
@@ -189,31 +249,25 @@ class Todo extends PureComponent {
</div>
</div>
) : (
<SortableContainer
onSortEnd={this.onSortEnd}
lockAxis="y"
lockToContainerEdges
disableAutoscroll
useDragHandle
<SortableList
items={this.state.todo.map((_, index) => index)}
onDragEnd={this.handleDragEnd}
>
{this.state.todo.map((_value, index) => (
<SortableItem
key={`item-${index}`}
index={index}
value={
{this.state.todo.map((todo, index) => (
<SortableItem key={index} id={index}>
{({ attributes, listeners }) => (
<div
className={'todoRow' + (this.state.todo[index].done ? ' done' : '')}
key={index}
className={'todoRow' + (todo.done ? ' done' : '')}
>
<Checkbox
checked={this.state.todo[index].done}
checked={todo.done}
onClick={() => this.updateTodo('done', index)}
/>
<TextareaAutosize
placeholder={variables.getMessage('widgets.navbar.notes.placeholder')}
value={this.state.todo[index].value}
value={todo.value}
onChange={(data) => this.updateTodo('set', index, data)}
readOnly={this.state.todo[index].done}
readOnly={todo.done}
/>
<Tooltip
title={variables.getMessage(
@@ -222,12 +276,12 @@ class Todo extends PureComponent {
>
<MdDelete onClick={() => this.updateTodo('remove', index)} />
</Tooltip>
<SortableHandle />
<DragHandle {...attributes} {...listeners} />
</div>
}
/>
)}
</SortableItem>
))}
</SortableContainer>
</SortableList>
)}
</div>
</div>

View File

@@ -1,7 +1,23 @@
import variables from 'config/variables';
import { PureComponent, createRef } from 'react';
import { MdAddLink, MdLinkOff, MdOutlineDragIndicator, MdEdit, MdDelete } from 'react-icons/md';
import { sortableContainer, sortableElement } from '@muetab/react-sortable-hoc';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings';
import { Checkbox, Dropdown } from 'components/Form/Settings';
import { Button } from 'components/Elements';
@@ -30,13 +46,31 @@ const DragHandle = () => (
</div>
);
const SortableItem = sortableElement(({ value, enabled, startEditLink, deleteLink }) => {
const SortableItem = ({ value, enabled, startEditLink, deleteLink }) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: value.key,
disabled: !enabled,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
const getIconUrl = (item) => {
return item.icon || 'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', '');
};
return (
<div className={`quicklink-item ${!enabled ? 'disabled' : ''}`} role="listitem">
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={`quicklink-item ${!enabled ? 'disabled' : ''}`}
role="listitem"
>
<DragHandle />
<div className="quicklink-icon">
<img src={getIconUrl(value)} alt={value.name} draggable={false} />
@@ -77,11 +111,38 @@ const SortableItem = sortableElement(({ value, enabled, startEditLink, deleteLin
</div>
</div>
);
});
};
const SortableContainer = sortableContainer(({ children }) => (
<div className="quicklinks-list" role="list">{children}</div>
));
const SortableList = ({ items, enabled, onDragEnd, startEditLink, deleteLink }) => {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 6,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
<SortableContext items={items.map((item) => item.key)} strategy={verticalListSortingStrategy}>
<div className="quicklinks-list" role="list">
{items.map((item) => (
<SortableItem
key={item.key}
value={item}
enabled={enabled}
startEditLink={startEditLink}
deleteLink={deleteLink}
/>
))}
</div>
</SortableContext>
</DndContext>
);
};
class QuickLinksOptions extends PureComponent {
constructor() {
@@ -188,24 +249,28 @@ class QuickLinksOptions extends PureComponent {
return result;
};
onSortStart = () => {
if (this.quicklinksContainer && this.quicklinksContainer.current) {
this.quicklinksContainer.current.classList.add('dragging');
}
};
handleDragEnd = (event) => {
const { active, over } = event;
if (!over || !this.state.enabled) return;
if (active.id === over.id) return;
onSortEnd = ({ oldIndex, newIndex }) => {
if (!this.state.enabled) return;
if (oldIndex === newIndex) return;
const newItems = this.arrayMove(this.state.items, oldIndex, newIndex);
const oldIndex = this.state.items.findIndex((item) => item.key === active.id);
const newIndex = this.state.items.findIndex((item) => item.key === over.id);
this.silenceEvent = true;
this.setState({ items: newItems }, () => {
localStorage.setItem('quicklinks', JSON.stringify(newItems));
EventBus.emit('refresh', 'quicklinks');
setTimeout(() => { this.silenceEvent = false; }, 0);
});
};
if (oldIndex === -1 || newIndex === -1) return;
const newItems = arrayMove(this.state.items, oldIndex, newIndex);
this.silenceEvent = true;
this.setState({ items: newItems }, () => {
localStorage.setItem('quicklinks', JSON.stringify(newItems));
EventBus.emit('refresh', 'quicklinks');
setTimeout(() => {
this.silenceEvent = false;
}, 0);
});
};
componentDidMount() {
@@ -339,27 +404,13 @@ onSortEnd = ({ oldIndex, newIndex }) => {
aria-hidden={!enabled}
>
<div className={`messagesContainer ${!enabled ? 'disabled' : ''}`}>
<SortableContainer
onSortStart={this.onSortStart}
onSortEnd={this.onSortEnd}
lockAxis="y"
lockToContainerEdges
disableAutoscroll
helperClass="sortable-helper"
distance={6}
disabled={!enabled}
>
{this.state.items.map((item, index) => (
<SortableItem
key={item.key}
index={index}
value={item}
enabled={enabled}
startEditLink={(data) => this.startEditLink(data)}
deleteLink={(key, e) => this.deleteLink(key, e)}
/>
))}
</SortableContainer>
<SortableList
items={this.state.items}
enabled={enabled}
onDragEnd={this.handleDragEnd}
startEditLink={(data) => this.startEditLink(data)}
deleteLink={(key, e) => this.deleteLink(key, e)}
/>
</div>
</div>