Add module ordering feature with drag-and-drop
- Add modules_order setting in backend settings registry - Update ModulesContext with moduleOrder state and save functions - Create configuration tab in Features page with drag-and-drop reordering - Make feature tabs dynamic based on order (updates in real-time) - Sort sidebar modules based on saved order - Add order-cards CSS with vertical layout - Update API client to support string[] type for modules_order 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,40 +2,32 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import { useModules } from '../../contexts/ModulesContext';
|
||||
import { useModules, TOGGLEABLE_MODULES } from '../../contexts/ModulesContext';
|
||||
import type { ModuleId } from '../../contexts/ModulesContext';
|
||||
import Feature1Tab from '../../components/admin/Feature1Tab';
|
||||
import '../../styles/AdminPanel.css';
|
||||
|
||||
type TabId = 'feature1' | 'feature2' | 'feature3';
|
||||
type TabId = 'config' | 'feature1' | 'feature2' | 'feature3';
|
||||
|
||||
export default function Features() {
|
||||
const { user: currentUser } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const { toggleMobileMenu } = useSidebar();
|
||||
const { moduleStates, setModuleEnabled, saveModulesToBackend, hasInitialized, isLoading } = useModules();
|
||||
const [activeTab, setActiveTab] = useState<TabId>('feature1');
|
||||
const { moduleStates, moduleOrder, setModuleEnabled, setModuleOrder, saveModulesToBackend, saveModuleOrder, hasInitialized, isLoading } = useModules();
|
||||
const [activeTab, setActiveTab] = useState<TabId>('config');
|
||||
const hasUserMadeChanges = useRef(false);
|
||||
const saveRef = useRef(saveModulesToBackend);
|
||||
const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({
|
||||
text: '',
|
||||
left: 0,
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const handleTabMouseEnter = (text: string, e: React.MouseEvent) => {
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
setTooltip({
|
||||
text,
|
||||
left: rect.left + rect.width / 2,
|
||||
visible: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTabMouseLeave = () => {
|
||||
setTooltip((prev) => ({ ...prev, visible: false }));
|
||||
};
|
||||
const [draggedItem, setDraggedItem] = useState<string | null>(null);
|
||||
const [localOrder, setLocalOrder] = useState<string[]>([]);
|
||||
const [hasOrderChanges, setHasOrderChanges] = useState(false);
|
||||
|
||||
// Sync local order with context order
|
||||
useEffect(() => {
|
||||
if (moduleOrder.length > 0) {
|
||||
setLocalOrder(moduleOrder);
|
||||
setHasOrderChanges(false);
|
||||
}
|
||||
}, [moduleOrder]);
|
||||
// Keep saveRef updated with latest function
|
||||
useEffect(() => {
|
||||
saveRef.current = saveModulesToBackend;
|
||||
@@ -118,11 +110,118 @@ export default function Features() {
|
||||
);
|
||||
};
|
||||
|
||||
// Drag and drop handlers for ordering
|
||||
const handleDragStart = (e: React.DragEvent, moduleId: string) => {
|
||||
setDraggedItem(moduleId);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', moduleId);
|
||||
// Add dragging class after a small delay for visual feedback
|
||||
setTimeout(() => {
|
||||
(e.target as HTMLElement).classList.add('dragging');
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleDragEnd = (e: React.DragEvent) => {
|
||||
setDraggedItem(null);
|
||||
(e.target as HTMLElement).classList.remove('dragging');
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, targetModuleId: string) => {
|
||||
e.preventDefault();
|
||||
if (!draggedItem || draggedItem === targetModuleId) return;
|
||||
|
||||
const newOrder = [...localOrder];
|
||||
const draggedIndex = newOrder.indexOf(draggedItem);
|
||||
const targetIndex = newOrder.indexOf(targetModuleId);
|
||||
|
||||
if (draggedIndex === -1 || targetIndex === -1) return;
|
||||
|
||||
// Remove dragged item and insert at target position
|
||||
newOrder.splice(draggedIndex, 1);
|
||||
newOrder.splice(targetIndex, 0, draggedItem);
|
||||
|
||||
setLocalOrder(newOrder);
|
||||
setHasOrderChanges(true);
|
||||
};
|
||||
|
||||
const handleApplyOrder = async () => {
|
||||
try {
|
||||
setModuleOrder(localOrder);
|
||||
await saveModuleOrder(localOrder);
|
||||
setHasOrderChanges(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to save order:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getModuleInfo = (moduleId: string) => {
|
||||
const module = TOGGLEABLE_MODULES.find(m => m.id === moduleId);
|
||||
return module || { id: moduleId, icon: 'extension', defaultEnabled: true };
|
||||
};
|
||||
|
||||
const renderConfigTab = () => {
|
||||
return (
|
||||
<div className="theme-tab-content">
|
||||
<div className="theme-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">{t.featuresPage?.orderSection || 'Ordine nella Sidebar'}</h3>
|
||||
</div>
|
||||
<div className="order-cards">
|
||||
{localOrder.map((moduleId, index) => {
|
||||
const moduleInfo = getModuleInfo(moduleId);
|
||||
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
|
||||
return (
|
||||
<div
|
||||
key={moduleId}
|
||||
className={`order-card ${draggedItem === moduleId ? 'dragging' : ''}`}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, moduleId)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleDrop(e, moduleId)}
|
||||
>
|
||||
<div className="order-card-preview">
|
||||
<span className="order-card-number">{index + 1}</span>
|
||||
</div>
|
||||
<div className="order-card-info">
|
||||
<span className="order-card-name">{moduleName}</span>
|
||||
<span className="order-card-desc">{t.featuresPage?.orderDesc || 'Trascina per riordinare'}</span>
|
||||
</div>
|
||||
<div className="order-card-icon">
|
||||
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
||||
</div>
|
||||
<div className="order-card-handle">
|
||||
<span className="material-symbols-outlined">drag_indicator</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{hasOrderChanges && (
|
||||
<div className="order-actions">
|
||||
<button className="btn-primary" onClick={handleApplyOrder}>
|
||||
<span className="material-symbols-outlined">check</span>
|
||||
{t.featuresPage?.applyOrder || 'Applica'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTabContent = () => {
|
||||
if (!hasInitialized || isLoading) {
|
||||
return <div className="loading">Loading...</div>;
|
||||
}
|
||||
switch (activeTab) {
|
||||
case 'config':
|
||||
return renderConfigTab();
|
||||
case 'feature1':
|
||||
return (
|
||||
<>
|
||||
@@ -163,52 +262,37 @@ export default function Features() {
|
||||
|
||||
return (
|
||||
<main className="main-content admin-panel-root">
|
||||
{/* Tab Tooltip */}
|
||||
{tooltip.visible && (
|
||||
<div
|
||||
className="admin-tab-tooltip"
|
||||
style={{ left: tooltip.left }}
|
||||
>
|
||||
{tooltip.text}
|
||||
</div>
|
||||
)}
|
||||
<div className="admin-tabs-container">
|
||||
<div className="admin-tabs-slider">
|
||||
<div className="page-tabs-container">
|
||||
<div className="page-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="admin-title-section">
|
||||
<div className="page-title-section">
|
||||
<span className="material-symbols-outlined">extension</span>
|
||||
<span className="admin-title-text">{t.featuresPage.title}</span>
|
||||
<span className="page-title-text">{t.featuresPage.title}</span>
|
||||
</div>
|
||||
<div className="admin-tabs-divider"></div>
|
||||
<div className="page-tabs-divider"></div>
|
||||
<button
|
||||
className={`admin-tab-btn ${activeTab === 'feature1' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('feature1')}
|
||||
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature1, e)}
|
||||
onMouseLeave={handleTabMouseLeave}
|
||||
className={`page-tab-btn ${activeTab === 'config' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('config')}
|
||||
>
|
||||
<span className="material-symbols-outlined">playlist_play</span>
|
||||
<span>{t.sidebar.feature1}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`admin-tab-btn ${activeTab === 'feature2' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('feature2')}
|
||||
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature2, e)}
|
||||
onMouseLeave={handleTabMouseLeave}
|
||||
>
|
||||
<span className="material-symbols-outlined">download</span>
|
||||
<span>{t.sidebar.feature2}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`admin-tab-btn ${activeTab === 'feature3' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('feature3')}
|
||||
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature3, e)}
|
||||
onMouseLeave={handleTabMouseLeave}
|
||||
>
|
||||
<span className="material-symbols-outlined">cast</span>
|
||||
<span>{t.sidebar.feature3}</span>
|
||||
<span className="material-symbols-outlined">tune</span>
|
||||
<span>{t.featuresPage?.configTab || 'Configurazione'}</span>
|
||||
</button>
|
||||
{(localOrder.length > 0 ? localOrder : ['feature1', 'feature2', 'feature3']).map((moduleId) => {
|
||||
const moduleInfo = getModuleInfo(moduleId);
|
||||
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
|
||||
return (
|
||||
<button
|
||||
key={moduleId}
|
||||
className={`page-tab-btn ${activeTab === moduleId ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(moduleId as TabId)}
|
||||
>
|
||||
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
||||
<span>{moduleName}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user