import React, {useContext} from 'react'; import {Link} from 'react-router'; import {Button, Message, Placeholder, Table} from '../ui'; import _ from 'lodash'; import {SortableTable} from '../SortableTable'; import {formatFrequency, humanFileSize} from '../Common'; import {Media} from '../../contexts/contexts'; import {TagsContext} from '../../Tags'; /** * Get the ID field from routes configuration. * @param {Object} routes - Routes configuration * @returns {string} The ID field name */ function getIdField(routes) { return routes?.id_field || 'id'; } /** * Generate a search link for a collection based on routes configuration. * Supports both query parameter-based (e.g., ?domain=...) and route-based (e.g., /channel/:id/video) linking. * * @param {Object} collection - The collection object * @param {Object} routes - Routes configuration * @param {string} primaryKey - The primary column key to use for the value * @returns {string|null} The generated link, or null if no link can be generated */ function getCollectionSearchLink(collection, routes, primaryKey) { const searchRoute = routes?.search; if (!searchRoute) { return null; } // Check if routes specifies a query parameter to use if (routes.searchParam) { // Query parameter-based linking (e.g., /archive?domain=example.com) return `${searchRoute}?${routes.searchParam}=${collection[primaryKey]}`; } else if (searchRoute.includes(':id')) { // Route parameter-based linking (e.g., /videos/channel/123/video) const idField = getIdField(routes); return searchRoute.replace(':id', collection[idField]); } // No linking strategy available return null; } /** * Convert a relative column width (1-16, sixteenths) into a CSS percentage. * @param {number} width - Relative width, 1-16 * @returns {Object|undefined} A style object, or undefined when no width is configured */ function widthStyle(width) { return width ? {width: `${(width / 16) * 100}%`} : undefined; } /** * Render a single table row for desktop view. * * @param {Object} collection - The collection data * @param {Array} columns - Column configurations * @param {Object} routes - Routes configuration * @param {Function} SingleTag - Tag component from context * @param {Function} onRowClick - Optional click handler */ function renderRow(collection, columns, routes, SingleTag, onRowClick) { const cells = columns.map((col) => { let value = collection[col.key]; // Handle actions column - render action buttons if (col.type === 'actions') { const idField = getIdField(routes); const editRoute = routes?.edit?.replace(':id', collection[idField]); return {editRoute && } ; } // Format the value based on column configuration if (col.render) { value = col.render(collection); } else if (col.format === 'bytes') { value = humanFileSize(value); } else if (col.format === 'frequency') { value = formatFrequency(value); } // Special handling for tag_name column - render SingleTag component if (col.key === 'tag_name' && value) { value = ; } // Special handling for the primary column (usually domain/name) if (col.key === columns[0].key) { const searchLink = getCollectionSearchLink(collection, routes, col.key); if (searchLink) { value = {value}; } } return {value || '-'} ; }); return onRowClick && onRowClick(collection)} className={onRowClick ? 'clickable' : ''} > {cells} ; } /** * Mobile row component for collections - stacked layout for better wrapping of long names */ function MobileCollectionRow({collection, mobileColumns, routes}) { const {SingleTag} = useContext(TagsContext); const primaryColumn = mobileColumns[0]; const idField = getIdField(routes); const editRoute = routes?.edit?.replace(':id', collection[idField]); const searchLink = getCollectionSearchLink(collection, routes, primaryColumn.key); return {searchLink ? ( {collection[primaryColumn.key]} ) : ( {collection[primaryColumn.key]} )} {collection.tag_name && <> } {mobileColumns .filter(col => col.type !== 'actions' && col.key !== primaryColumn.key && col.key !== 'tag_name') .map(col => { let value = collection[col.key]; if (col.render) { value = col.render(collection); } else if (col.format === 'bytes') { value = humanFileSize(value); } else if (col.format === 'frequency') { value = formatFrequency(value); } return (
{col.label}: {value || '-'}
); }) }
{editRoute && }
; } /** * Reusable table component for displaying collections (Domains, Channels, etc). * * @param {Array} collections - Array of collection objects * @param {Array} columns - Column configurations for the table. A column's optional `render(collection)` displays * the cell; the column still sorts by `collection[key]`. * @param {Object} routes - Routes configuration for navigation (edit, search, etc.) * @param {String} searchStr - Search filter string (managed by parent) * @param {Function} onRowClick - Optional callback when a row is clicked * @param {String} emptyMessage - Message to display when there are no collections */ export function CollectionTable({ collections, columns, routes = {}, searchStr = '', onRowClick, emptyMessage = 'No items yet' }) { const {SingleTag} = useContext(TagsContext); // Loading state if (collections === null) { return ; } // Error state if (collections === undefined) { return ; } // Empty state if (collections && collections.length === 0) { return ; } // No columns configured if (!columns || columns.length === 0) { return ; } // Filter collections by search string let filteredCollections = collections; if (searchStr) { const re = new RegExp(_.escapeRegExp(searchStr), 'i'); filteredCollections = collections.filter(collection => { // Search across all string fields return Object.values(collection).some(value => { if (typeof value === 'string') { return re.test(value); } return false; }); }); } // Build table headers from columns (desktop) const headers = columns.map((col) => ({ key: col.key, text: col.label, sortBy: col.sortable ? col.key : null, width: col.width, })); // Build mobile columns (exclude hideOnMobile) and simplified 2-column headers const mobileColumns = columns.filter(col => !col.hideOnMobile); const primaryColumn = columns[0]; const mobileHeaders = [ { key: primaryColumn.key, text: primaryColumn.label, sortBy: primaryColumn.sortable ? primaryColumn.key : null }, { key: 'manage', text: 'Manage' } ]; // Get default sort column (first column key) const defaultSortColumn = columns[0]?.key || 'id'; return <> } rowKey='id' tableHeaders={mobileHeaders} defaultSortColumn={defaultSortColumn} /> renderRow(collection, columns, routes, SingleTag, onRowClick)} rowKey='id' tableHeaders={headers} defaultSortColumn={defaultSortColumn} /> ; }