import { attachClosestEdge, type Edge, extractClosestEdge, } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'; import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; import { draggable, dropTargetForElements, } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview'; import clsx from 'clsx'; import React, { CSSProperties, memo, ReactElement, ReactNode, useEffect, useRef, useState, } from 'react'; import { useParams } from 'react-router'; import { CellComponentProps } from 'react-window-v2'; import styles from './item-table-list-column.module.css'; import i18n from '/@/i18n/i18n'; import { useItemSelectionState } from '/@/renderer/components/item-list/helpers/item-list-state'; import { isNoHorizontalPaddingColumn } from '/@/renderer/components/item-list/item-detail-list/utils'; import { ActionsColumn } from '/@/renderer/components/item-list/item-table-list/columns/actions-column'; import { AlbumArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-artists-column'; import { AlbumColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-column'; import { ArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/artists-column'; import { ComposerColumn } from '/@/renderer/components/item-list/item-table-list/columns/composer-column'; import { CountColumn } from '/@/renderer/components/item-list/item-table-list/columns/count-column'; import { AbsoluteDateColumn, DateColumn, RelativeDateColumn, } from '/@/renderer/components/item-list/item-table-list/columns/date-column'; import { DefaultColumn } from '/@/renderer/components/item-list/item-table-list/columns/default-column'; import { DurationColumn } from '/@/renderer/components/item-list/item-table-list/columns/duration-column'; import { FavoriteColumn } from '/@/renderer/components/item-list/item-table-list/columns/favorite-column'; import { GenreBadgeColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-badge-column'; import { GenreColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-column'; import { ImageColumn } from '/@/renderer/components/item-list/item-table-list/columns/image-column'; import { NumericColumn } from '/@/renderer/components/item-list/item-table-list/columns/numeric-column'; import { PathColumn } from '/@/renderer/components/item-list/item-table-list/columns/path-column'; import { PlaylistReorderColumn } from '/@/renderer/components/item-list/item-table-list/columns/playlist-reorder-column'; import { RatingColumn } from '/@/renderer/components/item-list/item-table-list/columns/rating-column'; import { RowIndexColumn } from '/@/renderer/components/item-list/item-table-list/columns/row-index-column'; import { SizeColumn } from '/@/renderer/components/item-list/item-table-list/columns/size-column'; import { TextColumn } from '/@/renderer/components/item-list/item-table-list/columns/text-column'; import { TitleArtistColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-artist-column'; import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-column'; import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column'; import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column'; import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state'; import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types'; import { Flex } from '/@/shared/components/flex/flex'; import { Icon } from '/@/shared/components/icon/icon'; import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Text } from '/@/shared/components/text/text'; import { useDoubleClick } from '/@/shared/hooks/use-double-click'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; import { LibraryItem } from '/@/shared/types/domain-types'; import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop'; import { TableColumn } from '/@/shared/types/types'; export interface ItemTableListColumn extends CellComponentProps { columnType?: TableColumn; } export interface ItemTableListInnerColumn extends ItemTableListColumn { controls: ItemControls; dragRef?: null | React.Ref; isDraggedOver?: 'bottom' | 'top' | null; isDragging?: boolean; type: TableColumn; } const ItemTableListColumnBase = (props: ItemTableListColumn) => { const { playlistId } = useParams() as { playlistId?: string }; const type = props.columnType ?? (props.columns[props.columnIndex].id as TableColumn); const isHeaderEnabled = !!props.enableHeader; const isDataRow = isHeaderEnabled ? props.rowIndex > 0 : true; const item = isDataRow ? (props.getRowItem?.(props.rowIndex) ?? props.data[props.rowIndex]) : null; const shouldEnableDrag = !!props.enableDrag && isDataRow && !!item; const itemType = (item as unknown as { _itemType?: LibraryItem })?._itemType || props.itemType; // Check if this row should render a group header (must be before conditional returns) // Group headers need to be rendered consistently across all grids (pinned left, main, pinned right) // to maintain proper styling and row heights let groupHeader: 'GROUP_HEADER' | null | ReactElement = null; if (props.groups && isDataRow && props.groups.length > 0) { const groupInfo = props.groupHeaderInfoByRowIndex?.get(props.rowIndex); const group = groupInfo ? props.groups[groupInfo.groupIndex] : undefined; if (groupInfo && group) { // Determine where to render the group header content: // - If pinned left columns exist, render in the first pinned left column // - Otherwise, render in the first column of the main grid const hasPinnedLeftColumns = (props.pinnedLeftColumnCount || 0) > 0; const isFirstPinnedLeftColumn = props.columnIndex === 0 && hasPinnedLeftColumns; const isMainGridFirstColumn = !hasPinnedLeftColumns && (props.columnIndex === (props.pinnedLeftColumnCount || 0) || (props.columnIndex === 0 && (props.pinnedLeftColumnCount || 0) === 0)); // Render group header content in the first pinned left column (if exists) or first main grid column if (isFirstPinnedLeftColumn || isMainGridFirstColumn) { groupHeader = group.render({ data: props.getGroupRenderData?.() ?? [], groupIndex: groupInfo.groupIndex, index: props.rowIndex, internalState: props.internalState, startDataIndex: groupInfo.startDataIndex, }); } else { // For other columns, mark as group header row for styled rendering groupHeader = 'GROUP_HEADER'; } } } const { dragRef, isDraggedOver, isDragging } = useItemDragDropState({ enableDrag: !!props.enableDrag, internalState: props.internalState, isDataRow, item, itemType: props.itemType, playerContext: props.playerContext, playlistId, }); const controls = props.controls; const dragProps = { dragRef: shouldEnableDrag ? dragRef : null, isDraggedOver: isDraggedOver === 'top' || isDraggedOver === 'bottom' ? isDraggedOver : null, isDragging, }; if (isHeaderEnabled && props.rowIndex === 0) { return ; } // Render group header if this row should have one if (groupHeader) { if (groupHeader === 'GROUP_HEADER') { // For non-first columns (pinned left, other main columns, pinned right), // render a styled cell that matches the group header styling // This ensures consistent row heights and styling across all grids return
; } // Render the group header spanning full table width // If rendering in pinned left column, extend right to cover all columns // If rendering in main grid, extend left to cover pinned columns const pinnedLeftWidth = props.pinnedLeftColumnWidths?.reduce((sum, width) => sum + width, 0) || 0; // Determine if we're rendering in the first pinned left column const isFirstPinnedLeftColumn = props.columnIndex === 0 && (props.pinnedLeftColumnCount || 0) > 0; if (isFirstPinnedLeftColumn) { return (
{groupHeader}
); } // For main grid, use negative margin to extend left return (
0 ? `-${pinnedLeftWidth}px` : 0, }} > {groupHeader}
); } if (itemType !== LibraryItem.FOLDER) { switch (type) { case TableColumn.ACTIONS: case TableColumn.SKIP: return ; case TableColumn.ALBUM: return ; case TableColumn.ALBUM_ARTIST: return ( ); case TableColumn.ALBUM_COUNT: case TableColumn.PLAY_COUNT: case TableColumn.SONG_COUNT: return ; case TableColumn.ARTIST: return ; case TableColumn.BIOGRAPHY: case TableColumn.COMMENT: return ; case TableColumn.BIT_DEPTH: case TableColumn.BIT_RATE: case TableColumn.BPM: case TableColumn.CHANNELS: case TableColumn.DISC_NUMBER: case TableColumn.SAMPLE_RATE: case TableColumn.TRACK_NUMBER: return ; case TableColumn.COMPOSER: return ; case TableColumn.DATE_ADDED: return ; case TableColumn.DURATION: return ; case TableColumn.GENRE: return ; case TableColumn.GENRE_BADGE: return ( ); case TableColumn.IMAGE: return ; case TableColumn.LAST_PLAYED: return ( ); case TableColumn.PATH: return ; case TableColumn.PLAYLIST_REORDER: return ; case TableColumn.RELEASE_DATE: return ( ); case TableColumn.ROW_INDEX: return ; case TableColumn.SIZE: return ; case TableColumn.TITLE: return ; case TableColumn.TITLE_ARTIST: return ( ); case TableColumn.TITLE_COMBINED: return ( ); case TableColumn.USER_FAVORITE: return ; case TableColumn.USER_RATING: return ; case TableColumn.YEAR: return ; default: return ; } } switch (type) { case TableColumn.ACTIONS: return ; case TableColumn.IMAGE: return ; case TableColumn.ROW_INDEX: return ; case TableColumn.TITLE: return ; case TableColumn.TITLE_ARTIST: return ; case TableColumn.TITLE_COMBINED: return ( ); default: return ; } }; export const ItemTableListColumn = memo(ItemTableListColumnBase, (prevProps, nextProps) => { const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); return ( prevProps.rowIndex === nextProps.rowIndex && prevProps.columnIndex === nextProps.columnIndex && prevProps.data === nextProps.data && prevProps.columns === nextProps.columns && prevProps.style === nextProps.style && prevProps.columnType === nextProps.columnType && prevProps.itemType === nextProps.itemType && prevProps.enableHeader === nextProps.enableHeader && prevProps.enableDrag === nextProps.enableDrag && prevProps.groups === nextProps.groups && prevProps.groupHeaderInfoByRowIndex === nextProps.groupHeaderInfoByRowIndex && prevProps.pinnedLeftColumnCount === nextProps.pinnedLeftColumnCount && prevProps.pinnedLeftColumnWidths === nextProps.pinnedLeftColumnWidths && prevProps.size === nextProps.size && prevProps.enableAlternateRowColors === nextProps.enableAlternateRowColors && prevProps.enableHorizontalBorders === nextProps.enableHorizontalBorders && prevProps.enableVerticalBorders === nextProps.enableVerticalBorders && prevProps.enableRowHoverHighlight === nextProps.enableRowHoverHighlight && prevProps.enableSelection === nextProps.enableSelection && prevProps.enableColumnResize === nextProps.enableColumnResize && prevProps.enableColumnReorder === nextProps.enableColumnReorder && prevProps.cellPadding === nextProps.cellPadding && prevItem === nextItem ); }); const NonMutedColumns = [TableColumn.TITLE, TableColumn.TITLE_ARTIST, TableColumn.TITLE_COMBINED]; export const TableColumnTextContainer = ( props: ItemTableListColumn & { children: React.ReactNode; className?: string; containerClassName?: string; controls: ItemControls; dragRef?: null | React.Ref; isDraggedOver?: 'bottom' | 'top' | null; isDragging?: boolean; type: TableColumn; }, ) => { const containerRef = useRef(null); const isDataRow = props.enableHeader ? props.rowIndex > 0 : true; const dataIndex = props.enableHeader ? props.rowIndex - 1 : props.rowIndex; const item = isDataRow ? (props.getRowItem?.(props.rowIndex) ?? props.data[props.rowIndex]) : null; const itemRowId = item && typeof item === 'object' && 'id' in item ? props.internalState.extractRowId(item) : undefined; const isSelected = useItemSelectionState(props.internalState, itemRowId || undefined); const isDragging = props.isDragging ?? false; const mergedRef = useMergedRef(containerRef, props.dragRef ?? null); const isLastColumn = props.columnIndex === props.columns.length - 1; const isLastRow = isDataRow && (props.enableHeader ? props.rowIndex === props.data.length : props.rowIndex === props.data.length - 1); // Apply dragged over state to all cells in the row so border can span entire row useEffect(() => { if (!isDataRow || !containerRef.current) return; const rowKey = `${props.tableId}-${props.rowIndex}`; const edge = props.isDraggedOver === 'top' || props.isDraggedOver === 'bottom' ? props.isDraggedOver : null; containerRef.current.dispatchEvent( new CustomEvent('itl:row-drag-over', { bubbles: true, detail: { edge, rowKey }, }), ); }, [isDataRow, props.isDraggedOver, props.rowIndex, props.tableId]); const handleClick = useDoubleClick({ onDoubleClick: (event: React.MouseEvent) => { if (isDataRow && item) { const rowId = props.internalState.extractRowId(item); const index = rowId ? props.internalState.findItemIndex(rowId) : -1; props.controls.onDoubleClick?.({ event, index, internalState: props.internalState, item: item as ItemListItem, itemType: props.itemType, }); } }, onSingleClick: (event: React.MouseEvent) => { // Don't trigger row selection if clicking on interactive elements const target = event.target as HTMLElement; const isInteractiveElement = target.closest( 'button, a, input, select, textarea, [role="button"]', ); if (isInteractiveElement) { return; } if (isDataRow && item && props.enableSelection) { const rowId = props.internalState.extractRowId(item); const index = rowId ? props.internalState.findItemIndex(rowId) : -1; props.controls.onClick?.({ event, index, internalState: props.internalState, item: item as ItemListItem, itemType: props.itemType, }); } }, }); const handleContextMenu = (event: React.MouseEvent) => { if (isDataRow && item) { event.preventDefault(); const rowId = props.internalState.extractRowId(item); const index = rowId ? props.internalState.findItemIndex(rowId) : -1; props.controls.onMore?.({ event, index, internalState: props.internalState, item: item as ItemListItem, itemType: props.itemType, }); } }; return (
0 && (props.rowIndex === 1 || !isLastRow), [styles.withVerticalBorder]: props.enableVerticalBorders && !isLastColumn, })} data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined} onClick={handleClick} onContextMenu={handleContextMenu} ref={mergedRef} style={props.style} > {props.children}
); }; export const TableColumnContainer = ( props: ItemTableListColumn & { children: React.ReactNode; className?: string; containerStyle?: CSSProperties; controls: ItemControls; dragRef?: null | React.Ref; isDraggedOver?: 'bottom' | 'top' | null; isDragging?: boolean; type: TableColumn; }, ) => { const containerRef = useRef(null); const isDataRow = props.enableHeader ? props.rowIndex > 0 : true; const dataIndex = props.enableHeader ? props.rowIndex - 1 : props.rowIndex; const item = isDataRow ? (props.getRowItem?.(props.rowIndex) ?? props.data[props.rowIndex]) : null; const itemRowId = item && typeof item === 'object' && 'id' in item ? props.internalState.extractRowId(item) : undefined; const isSelected = useItemSelectionState(props.internalState, itemRowId || undefined); const isDragging = props.isDragging ?? false; const mergedRef = useMergedRef(containerRef, props.dragRef ?? null); const isLastColumn = props.columnIndex === props.columns.length - 1; const isLastRow = isDataRow && (props.enableHeader ? props.rowIndex === props.data.length : props.rowIndex === props.data.length - 1); // Apply dragged over state to all cells in the row so border can span entire row useEffect(() => { if (!isDataRow || !containerRef.current) return; const rowKey = `${props.tableId}-${props.rowIndex}`; const edge = props.isDraggedOver === 'top' || props.isDraggedOver === 'bottom' ? props.isDraggedOver : null; containerRef.current.dispatchEvent( new CustomEvent('itl:row-drag-over', { bubbles: true, detail: { edge, rowKey }, }), ); }, [isDataRow, props.isDraggedOver, props.rowIndex, props.tableId]); const handleClick = useDoubleClick({ onDoubleClick: (event: React.MouseEvent) => { if (isDataRow && item) { const rowId = props.internalState.extractRowId(item); const index = rowId ? props.internalState.findItemIndex(rowId) : -1; props.controls.onDoubleClick?.({ event, index, internalState: props.internalState, item: item as ItemListItem, itemType: props.itemType, }); } }, onSingleClick: (event: React.MouseEvent) => { // Don't trigger row selection if clicking on interactive elements const target = event.target as HTMLElement; const isInteractiveElement = target.closest( 'button, a, input, select, textarea, [role="button"]', ); if (isInteractiveElement) { return; } if (isDataRow && item && props.enableSelection) { const rowId = props.internalState.extractRowId(item); const index = rowId ? props.internalState.findItemIndex(rowId) : -1; props.controls.onClick?.({ event, index, internalState: props.internalState, item: item as ItemListItem, itemType: props.itemType, }); } }, }); const handleContextMenu = (event: React.MouseEvent) => { if (isDataRow && item) { event.preventDefault(); const rowId = props.internalState.extractRowId(item); const index = rowId ? props.internalState.findItemIndex(rowId) : -1; props.controls.onMore?.({ event, index, internalState: props.internalState, item: item as ItemListItem, itemType: props.itemType, }); } }; return (
0 && (props.rowIndex === 1 || !isLastRow), [styles.withVerticalBorder]: props.enableVerticalBorders && !isLastColumn, })} data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined} onClick={handleClick} onContextMenu={handleContextMenu} ref={mergedRef} style={{ ...props.containerStyle, ...props.style }} > {props.children}
); }; interface ColumnResizeHandleProps { columnId: TableColumn; initialWidth: number; onResize: (columnId: TableColumn, width: number) => void; side: 'left' | 'right'; } const ColumnResizeHandle = ({ columnId, initialWidth, onResize, side, }: ColumnResizeHandleProps) => { const [isDragging, setIsDragging] = useState(false); const handleRef = useRef(null); const startWidthRef = useRef(initialWidth); const startXRef = useRef(0); const finalWidthRef = useRef(initialWidth); // Update the ref when initialWidth changes (but not during drag) useEffect(() => { if (!isDragging) { startWidthRef.current = initialWidth; } }, [initialWidth, isDragging]); useEffect(() => { if (!isDragging) return; const handleMouseMove = (event: MouseEvent) => { const deltaX = event.clientX - startXRef.current; const newWidth = Math.min(Math.max(10, startWidthRef.current + deltaX), 1000); finalWidthRef.current = newWidth; }; const handleMouseUp = () => { setIsDragging(false); document.body.style.cursor = ''; document.body.style.userSelect = ''; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); onResize(columnId, finalWidthRef.current); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, [isDragging, columnId, onResize]); const handleMouseDown = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); setIsDragging(true); startWidthRef.current = initialWidth; startXRef.current = event.clientX; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }; return (
); }; export const TableColumnHeaderContainer = ( props: ItemTableListColumn & { className?: string; containerClassName?: string; controls: ItemControls; type: TableColumn; }, ) => { const columnConfig = props.columns[props.columnIndex]; // Use the actual rendered width from style if available, otherwise fall back to config width const currentWidth = (props.style?.width as number | undefined) || columnConfig.width; const handleResize = (columnId: TableColumn, width: number) => { props.controls.onColumnResized?.({ columnId, width }); }; const containerRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [isDraggedOver, setIsDraggedOver] = useState(null); useEffect(() => { if (!containerRef.current || !props.enableColumnReorder) { return; } const handleReorder = ( columnIdFrom: TableColumn, columnIdTo: TableColumn, edge: Edge | null, ) => { props.controls.onColumnReordered?.({ columnIdFrom, columnIdTo, edge }); }; return combine( draggable({ element: containerRef.current, getInitialData: () => { const data = dndUtils.generateDragData( { id: [props.type], operation: [DragOperation.REORDER], type: DragTarget.TABLE_COLUMN, }, { tableId: props.tableId }, ); return data; }, onDragStart: () => { setIsDragging(true); }, onDrop: () => { setIsDragging(false); }, onGenerateDragPreview: (data) => { disableNativeDragPreview({ nativeSetDragImage: data.nativeSetDragImage }); }, }), dropTargetForElements({ canDrop: (args) => { const data = args.source.data as unknown as DragData; const sourceTableId = (data.metadata as { tableId?: string })?.tableId; const isSelf = (args.source.data.id as string[])[0] === props.type; const isSameTable = sourceTableId === props.tableId; return ( dndUtils.isDropTarget(data.type, [DragTarget.TABLE_COLUMN]) && !isSelf && isSameTable ); }, element: containerRef.current, getData: ({ element, input }) => { const data = dndUtils.generateDragData( { id: [props.type], operation: [DragOperation.REORDER], type: DragTarget.TABLE_COLUMN, }, { tableId: props.tableId }, ); return attachClosestEdge(data, { allowedEdges: ['left', 'right'], element, input, }); }, onDrag: (args) => { const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data); setIsDraggedOver(closestEdgeOfTarget); }, onDragLeave: () => { setIsDraggedOver(null); }, onDrop: (args) => { const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data); const from = args.source.data.id as string[]; const to = args.self.data.id as string[]; handleReorder( from[0] as TableColumn, to[0] as TableColumn, closestEdgeOfTarget, ); setIsDraggedOver(null); }, }), ); }, [props.type, props.enableColumnReorder, props.controls, props.tableId]); return ( {columnLabelMap[props.type]} {!columnConfig.autoSize && props.enableColumnResize && ( )} ); }; export const columnLabelMap: Record = { [TableColumn.ACTIONS]: ( ), [TableColumn.ALBUM]: i18n.t('table.column.album', { postProcess: 'upperCase' }) as string, [TableColumn.ALBUM_ARTIST]: i18n.t('table.column.albumArtist', { postProcess: 'upperCase', }) as string, [TableColumn.ALBUM_COUNT]: i18n.t('table.column.albumCount', { postProcess: 'upperCase', }) as string, [TableColumn.ARTIST]: i18n.t('table.column.artist', { postProcess: 'upperCase' }) as string, [TableColumn.BIOGRAPHY]: i18n.t('table.column.biography', { postProcess: 'upperCase', }) as string, [TableColumn.BIT_DEPTH]: i18n.t('table.column.bitDepth', { postProcess: 'upperCase', }) as string, [TableColumn.BIT_RATE]: i18n.t('table.column.bitrate', { postProcess: 'upperCase' }) as string, [TableColumn.BPM]: i18n.t('table.column.bpm', { postProcess: 'upperCase' }) as string, [TableColumn.CHANNELS]: i18n.t('table.column.channels', { postProcess: 'upperCase' }) as string, [TableColumn.CODEC]: i18n.t('table.column.codec', { postProcess: 'upperCase' }) as string, [TableColumn.COMMENT]: i18n.t('table.column.comment', { postProcess: 'upperCase' }) as string, [TableColumn.COMPOSER]: i18n.t('table.config.label.composer', { postProcess: 'upperCase', }) as string, [TableColumn.DATE_ADDED]: i18n.t('table.column.dateAdded', { postProcess: 'upperCase', }) as string, [TableColumn.DISC_NUMBER]: ( ), [TableColumn.DURATION]: ( ), [TableColumn.GENRE]: i18n.t('table.column.genre', { postProcess: 'upperCase' }) as string, [TableColumn.GENRE_BADGE]: i18n.t('table.column.genre', { postProcess: 'upperCase', }) as string, [TableColumn.ID]: 'ID', [TableColumn.IMAGE]: '', [TableColumn.LAST_PLAYED]: i18n.t('table.column.lastPlayed', { postProcess: 'upperCase', }) as string, [TableColumn.OWNER]: i18n.t('table.column.owner', { postProcess: 'upperCase' }) as string, [TableColumn.PATH]: i18n.t('table.column.path', { postProcess: 'upperCase' }) as string, [TableColumn.PLAY_COUNT]: i18n.t('table.column.playCount', { postProcess: 'upperCase', }) as string, [TableColumn.PLAYLIST_REORDER]: ( ), [TableColumn.RELEASE_DATE]: i18n.t('table.column.releaseDate', { postProcess: 'upperCase', }) as string, [TableColumn.ROW_INDEX]: ( ), [TableColumn.SAMPLE_RATE]: i18n.t('table.column.sampleRate', { postProcess: 'upperCase', }) as string, [TableColumn.SIZE]: i18n.t('table.column.size', { postProcess: 'upperCase' }) as string, [TableColumn.SKIP]: '', [TableColumn.SONG_COUNT]: i18n.t('table.column.songCount', { postProcess: 'upperCase', }) as string, [TableColumn.TITLE]: i18n.t('table.column.title', { postProcess: 'upperCase' }) as string, [TableColumn.TITLE_ARTIST]: i18n.t('table.column.title', { postProcess: 'upperCase', }) as string, [TableColumn.TITLE_COMBINED]: i18n.t('table.column.title', { postProcess: 'upperCase', }) as string, [TableColumn.TRACK_NUMBER]: ( ), [TableColumn.USER_FAVORITE]: ( ), [TableColumn.USER_RATING]: ( ), [TableColumn.YEAR]: i18n.t('table.column.releaseYear', { postProcess: 'upperCase' }) as string, }; export const ColumnNullFallback = (props: ItemTableListInnerColumn) => { return  ; }; export const ColumnSkeletonVariable = (props: ItemTableListInnerColumn) => { return ( ); }; export const ColumnSkeletonFixed = (props: ItemTableListInnerColumn) => { return ( ); };