feat: drag-to-reorder table groups in zone tabs
Backend:
- tables.py: PUT /api/tables/groups/reorder accepts an ordered list
of group IDs and updates sort_order accordingly
Frontend (TablesConfigTab):
- Zone tabs are now draggable for group tabs (not All/Ungrouped)
- HTML5 drag-and-drop with visual drop-target highlight and a
grab cursor; fires reorderGroups mutation on drop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useRef } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
import client from '../api/client'
|
||||
@@ -43,6 +43,8 @@ export default function TablesPage() {
|
||||
const [activeTab, setActiveTab] = useState('all') // 'all' | group.id
|
||||
const [selected, setSelected] = useState(new Set())
|
||||
const [anyHovered, setAnyHovered] = useState(false)
|
||||
const dragGroupId = useRef(null)
|
||||
const [dragOverId, setDragOverId] = useState(null)
|
||||
|
||||
const { data: tables = [], isLoading } = useQuery({
|
||||
queryKey: ['tables-all', showInactive],
|
||||
@@ -102,6 +104,12 @@ export default function TablesPage() {
|
||||
onError: () => toast.error('Σφάλμα'),
|
||||
})
|
||||
|
||||
const reorderGroups = useMutation({
|
||||
mutationFn: (ids) => client.put('/api/tables/groups/reorder', ids),
|
||||
onSuccess: invalidateGroups,
|
||||
onError: () => toast.error('Σφάλμα αναδιάταξης'),
|
||||
})
|
||||
|
||||
// Filter tables for the active tab
|
||||
const visibleTables = activeTab === 'all'
|
||||
? tables
|
||||
@@ -156,23 +164,47 @@ export default function TablesPage() {
|
||||
|
||||
{/* Zone tabs */}
|
||||
<div className="flex gap-0 flex-wrap border-b border-slate-200 px-4 flex-shrink-0">
|
||||
{zoneTabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => { setActiveTab(tab.id); clearSelect() }}
|
||||
className={`flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||
activeTab === tab.id
|
||||
? 'border-sky-500 text-sky-600'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
{tab.color && <span className="w-2 h-2 rounded-full shrink-0" style={{ background: tab.color }} />}
|
||||
{tab.label}
|
||||
<span className="ml-0.5 text-xs text-slate-400">
|
||||
({tab.id === 'all' ? tables.length : tab.id === 'ungrouped' ? tables.filter(t => !t.group_id).length : tables.filter(t => t.group_id === tab.id).length})
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
{zoneTabs.map(tab => {
|
||||
const isGroup = tab.id !== 'all' && tab.id !== 'ungrouped'
|
||||
const isDragOver = dragOverId === tab.id
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => { setActiveTab(tab.id); clearSelect() }}
|
||||
draggable={isGroup}
|
||||
onDragStart={isGroup ? () => { dragGroupId.current = tab.id } : undefined}
|
||||
onDragOver={isGroup ? (e) => { e.preventDefault(); setDragOverId(tab.id) } : undefined}
|
||||
onDragLeave={isGroup ? () => setDragOverId(null) : undefined}
|
||||
onDrop={isGroup ? (e) => {
|
||||
e.preventDefault()
|
||||
setDragOverId(null)
|
||||
const fromId = dragGroupId.current
|
||||
if (!fromId || fromId === tab.id) return
|
||||
const groupIds = groups.map(g => g.id)
|
||||
const fromIdx = groupIds.indexOf(fromId)
|
||||
const toIdx = groupIds.indexOf(tab.id)
|
||||
if (fromIdx === -1 || toIdx === -1) return
|
||||
const reordered = [...groupIds]
|
||||
reordered.splice(fromIdx, 1)
|
||||
reordered.splice(toIdx, 0, fromId)
|
||||
reorderGroups.mutate(reordered)
|
||||
} : undefined}
|
||||
onDragEnd={isGroup ? () => { dragGroupId.current = null; setDragOverId(null) } : undefined}
|
||||
className={`flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||
activeTab === tab.id
|
||||
? 'border-sky-500 text-sky-600'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
||||
} ${isDragOver ? 'bg-sky-50 border-sky-300' : ''} ${isGroup ? 'cursor-grab active:cursor-grabbing' : ''}`}
|
||||
>
|
||||
{isGroup && <span className="text-slate-300 text-xs mr-0.5 select-none">⠿</span>}
|
||||
{tab.color && <span className="w-2 h-2 rounded-full shrink-0" style={{ background: tab.color }} />}
|
||||
{tab.label}
|
||||
<span className="ml-0.5 text-xs text-slate-400">
|
||||
({tab.id === 'all' ? tables.length : tab.id === 'ungrouped' ? tables.filter(t => !t.group_id).length : tables.filter(t => t.group_id === tab.id).length})
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Zone action bar (when viewing a specific zone) */}
|
||||
|
||||
Reference in New Issue
Block a user