Spaces:
Running
Running
| /** | |
| * Data Table Component with Selection & Inline Editing | |
| * For dashboard-like interfaces | |
| */ | |
| const { useState, useEffect, useMemo } = window.PreactLib || {}; | |
| export function DashboardTable({ | |
| data = [], | |
| columns = [], | |
| onSelectionChange, | |
| onRowUpdate, | |
| onRowDelete, | |
| actions = [], | |
| className = '' | |
| }) { | |
| const html = window.html; | |
| const [selectedIds, setSelectedIds] = useState(new Set()); | |
| const [editingId, setEditingId] = useState(null); | |
| const [editValue, setEditValue] = useState(''); | |
| const allSelected = data.length > 0 && selectedIds.size === data.length; | |
| const someSelected = selectedIds.size > 0 && selectedIds.size < data.length; | |
| const toggleSelectAll = () => { | |
| if (allSelected) { | |
| setSelectedIds(new Set()); | |
| } else { | |
| setSelectedIds(new Set(data.map(row => row.id))); | |
| } | |
| }; | |
| const toggleSelect = (id) => { | |
| const newSelected = new Set(selectedIds); | |
| if (newSelected.has(id)) { | |
| newSelected.delete(id); | |
| } else { | |
| newSelected.add(id); | |
| } | |
| setSelectedIds(newSelected); | |
| }; | |
| const startEditing = (row, columnKey) => { | |
| setEditingId(row.id); | |
| setEditValue(row[columnKey]); | |
| }; | |
| const saveEdit = (columnKey) => { | |
| if (onRowUpdate) { | |
| onRowUpdate(editingId, { [columnKey]: editValue }); | |
| } | |
| setEditingId(null); | |
| }; | |
| const cancelEdit = () => { | |
| setEditingId(null); | |
| setEditValue(''); | |
| }; | |
| useEffect(() => { | |
| if (onSelectionChange) { | |
| onSelectionChange(Array.from(selectedIds)); | |
| } | |
| }, [selectedIds]); | |
| return html` | |
| <div class="table-responsive ${className}"> | |
| <table class="table table-hover table-striped"> | |
| <thead> | |
| <tr> | |
| <th style="width: 3%;"> | |
| <input | |
| type="checkbox" | |
| class="form-check-input" | |
| checked=${allSelected} | |
| indeterminate=${someSelected} | |
| onChange=${toggleSelectAll} | |
| /> | |
| </th> | |
| ${columns.map(col => html` | |
| <th scope="col" style="${col.style || ''}">${col.header}</th> | |
| `)} | |
| ${actions.length > 0 ? html`<th scope="col">Actions</th>` : ''} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${data.length === 0 ? html` | |
| <tr> | |
| <td colSpan=${columns.length + 2} class="text-center py-4 text-muted"> | |
| No data available | |
| </td> | |
| </tr> | |
| ` : data.map(row => html` | |
| <tr key=${row.id} class="${selectedIds.has(row.id) ? 'table-active' : ''}"> | |
| <td> | |
| <input | |
| type="checkbox" | |
| class="form-check-input" | |
| checked=${selectedIds.has(row.id)} | |
| onChange=${() => toggleSelect(row.id)} | |
| /> | |
| </td> | |
| ${columns.map(col => html` | |
| <td> | |
| ${editingId === row.id && col.editable ? html` | |
| <div class="input-group input-group-sm"> | |
| <input | |
| type="text" | |
| class="form-control" | |
| value=${editValue} | |
| onInput=${(e) => setEditValue(e.target.value)} | |
| onBlur=${() => saveEdit(col.key)} | |
| onKeyDown=${(e) => { | |
| if (e.key === 'Enter') saveEdit(col.key); | |
| if (e.key === 'Escape') cancelEdit(); | |
| }} | |
| autofocus | |
| /> | |
| </div> | |
| ` : html` | |
| <div | |
| style="${col.editable ? 'cursor: pointer;' : ''}" | |
| onClick=${() => col.editable && startEditing(row, col.key)} | |
| > | |
| ${col.render ? col.render(row[col.key], row) : row[col.key]} | |
| ${col.editable ? html`<i class="fas fa-pencil-alt ms-2 text-muted" style="font-size: 0.8rem;"></i>` : ''} | |
| </div> | |
| `} | |
| </td> | |
| `)} | |
| ${actions.length > 0 ? html` | |
| <td> | |
| <div class="btn-group btn-group-sm"> | |
| ${actions.map(action => { | |
| // Skip hidden actions | |
| if (action.hidden && action.hidden(row)) { | |
| return null; | |
| } | |
| return html` | |
| <button | |
| class="btn ${action.variant || 'btn-outline-primary'}" | |
| title=${action.title || action.label} | |
| onClick=${(e) => { | |
| e.stopPropagation(); | |
| action.onClick(row); | |
| }} | |
| > | |
| ${action.icon ? html`<i class="${action.icon}"></i>` : action.label} | |
| </button> | |
| `; | |
| })} | |
| </div> | |
| </td> | |
| ` : ''} | |
| </tr> | |
| `)} | |
| </tbody> | |
| </table> | |
| </div> | |
| `; | |
| } | |
| // === Filter Tabs Component === | |
| export function FilterTabs({ | |
| filters = [], | |
| activeFilter = 'all', | |
| onChange, | |
| className = '' | |
| }) { | |
| const html = window.html; | |
| return html` | |
| <ul class="nav nav-pills mb-3 ${className}"> | |
| ${filters.map(filter => html` | |
| <li class="nav-item"> | |
| <button | |
| class="nav-link ${activeFilter === filter.value ? 'active' : ''}" | |
| onClick=${() => onChange(filter.value)} | |
| > | |
| ${filter.icon ? html`<i class="${filter.icon} me-1"></i>` : ''} | |
| ${filter.label} | |
| </button> | |
| </li> | |
| `)} | |
| </ul> | |
| `; | |
| } | |
| // === Bulk Actions Bar === | |
| export function BulkActionsBar({ | |
| selectedCount = 0, | |
| actions = [], | |
| onClearSelection, | |
| className = '' | |
| }) { | |
| const html = window.html; | |
| if (selectedCount === 0) return null; | |
| return html` | |
| <div class="alert alert-info d-flex align-items-center justify-content-between ${className}"> | |
| <span> | |
| <i class="bi bi-check-circle-fill me-2"></i> | |
| <strong>${selectedCount}</strong> item${selectedCount > 1 ? 's' : ''} selected | |
| </span> | |
| <div class="btn-group"> | |
| ${actions.map(action => html` | |
| <button | |
| class="btn ${action.variant || 'btn-outline-primary'}" | |
| onClick=${action.onClick} | |
| > | |
| ${action.icon ? html`<i class="${action.icon} me-1"></i>` : ''} | |
| ${action.label} | |
| </button> | |
| `)} | |
| <button | |
| class="btn btn-outline-secondary" | |
| onClick=${onClearSelection} | |
| > | |
| <i class="bi bi-x-lg me-1"></i>Clear | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| export default DashboardTable; | |