From 3b9d8999aa2071167c1fbfe7b9a787b48ef970ae Mon Sep 17 00:00:00 2001 From: olafpd Date: Sat, 16 Nov 2024 13:46:41 -0500 Subject: [PATCH 1/4] added functionality to edit artist/title for tracks in a playlist --- .../app/app/components/PlaylistView/index.tsx | 146 +++-- .../containers/TrackTableContainer/index.tsx | 201 ++++--- .../GridTrackTable/Cells/EditableTextCell.tsx | 47 ++ .../Cells/EditableTitleCell.tsx | 38 ++ .../lib/components/GridTrackTable/index.tsx | 513 ++++++++++-------- .../ui/lib/components/TrackTable/types.ts | 11 +- 6 files changed, 606 insertions(+), 350 deletions(-) create mode 100644 packages/ui/lib/components/GridTrackTable/Cells/EditableTextCell.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/Cells/EditableTitleCell.tsx diff --git a/packages/app/app/components/PlaylistView/index.tsx b/packages/app/app/components/PlaylistView/index.tsx index a9d783692d..651fc80497 100644 --- a/packages/app/app/components/PlaylistView/index.tsx +++ b/packages/app/app/components/PlaylistView/index.tsx @@ -4,8 +4,15 @@ import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router'; import { Icon } from 'semantic-ui-react'; -import { Playlist } from '@nuclear/core'; -import { Button, ContextPopup, PopupButton, InputDialog, timestampToTimeString, Tooltip } from '@nuclear/ui'; +import { Playlist, PlaylistTrack } from '@nuclear/core'; +import { + Button, + ContextPopup, + PopupButton, + InputDialog, + timestampToTimeString, + Tooltip +} from '@nuclear/ui'; import { Track } from '@nuclear/ui/lib/types'; import artPlaceholder from '../../../resources/media/art_placeholder.png'; @@ -26,7 +33,7 @@ export type PlaylistViewProps = { onReorderTracks: (isource: number, idest: number) => void; isExternal?: boolean; externalSourceName?: string; -} +}; const PlaylistView: React.FC = ({ playlist, @@ -45,16 +52,42 @@ const PlaylistView: React.FC = ({ const { t, i18n } = useTranslation('playlists'); const history = useHistory(); - const onRenamePlaylist = useCallback((name: string) => { - const updatedPlaylist = { - ...playlist, - name - }; - updatePlaylist(updatedPlaylist); - }, [playlist, updatePlaylist]); + const onRenamePlaylist = useCallback( + (name: string) => { + const updatedPlaylist = { + ...playlist, + name + }; + updatePlaylist(updatedPlaylist); + }, + [playlist, updatePlaylist] + ); + const onUpdateTrack = useCallback( + (index: number, updatedTrack: Track) => { + const updatedTracks = playlist.tracks.map((track, i) => { + if (i === index) { + return { + ...updatedTrack, + stream: (track as PlaylistTrack).stream + } as PlaylistTrack; + } + return track; + }); + + const updatedPlaylist = { + ...playlist, + tracks: updatedTracks + } as Playlist; - const onAddAll = useCallback(() => addTracks(playlist.tracks), - [addTracks, playlist]); + updatePlaylist(updatedPlaylist); + }, + [playlist, updatePlaylist] + ); + + const onAddAll = useCallback( + () => addTracks(playlist.tracks), + [addTracks, playlist] + ); const onPlayAll = useCallback(() => { clearQueue(); @@ -64,13 +97,16 @@ const PlaylistView: React.FC = ({ }, [addTracks, clearQueue, playlist, selectSong, startPlayback]); const onDeleteTrack = !isExternal - ? useCallback((trackToRemove: Track, trackIndex: number) => { - const newPlaylist = { - ...playlist, - tracks: playlist.tracks.filter((_, index) => index !== trackIndex) - }; - updatePlaylist(newPlaylist); - }, [playlist, updatePlaylist]) + ? useCallback( + (trackToRemove: Track, trackIndex: number) => { + const newPlaylist = { + ...playlist, + tracks: playlist.tracks.filter((_, index) => index !== trackIndex) + }; + updatePlaylist(newPlaylist); + }, + [playlist, updatePlaylist] + ) : undefined; const onDeletePlaylist = useCallback(() => { @@ -94,27 +130,30 @@ const PlaylistView: React.FC = ({ }, [exportPlaylist, playlist, t]); return ( -
+
- { - isExternal && + {isExternal && (
+
{externalSourceName}
@@ -122,9 +161,11 @@ const PlaylistView: React.FC = ({ position='bottom center' />
- } + )}
- +
{playlist.name} = ({ initialString={playlist.name} onAccept={onRenamePlaylist} trigger={ - !isExternal && -
@@ -149,18 +191,18 @@ const PlaylistView: React.FC = ({ {`${playlist.tracks.length} ${t('number-of-tracks')}`} - { - playlist.lastModified && + {playlist.lastModified && ( <> - - · - + · - {`${t('modified-at')}${timestampToTimeString(playlist.lastModified, i18n.language)}`} + {`${t('modified-at')}${timestampToTimeString( + playlist.lastModified, + i18n.language + )}`} - } + )}
diff --git a/packages/app/app/containers/TrackTableContainer/index.tsx b/packages/app/app/containers/TrackTableContainer/index.tsx index cdfacc2b07..83c724c6f9 100644 --- a/packages/app/app/containers/TrackTableContainer/index.tsx +++ b/packages/app/app/containers/TrackTableContainer/index.tsx @@ -22,6 +22,7 @@ import { safeAddUuid } from '../../actions/helpers'; export type TrackTableContainerProps = TrackTableSettings & { tracks: TrackTableProps['tracks']; onDelete?: TrackTableProps['onDelete']; + onTrackUpdate?: (index: number, updatedTrack: Track) => void; onReorder?: (indexSource: number, indexDest: number) => void; TrackTableComponent?: React.ComponentType>; customColumns?: TrackTableProps['customColumns']; @@ -29,7 +30,7 @@ export type TrackTableContainerProps = TrackTableSettings & { displayAddToFavorites?: boolean; }; -function TrackTableContainer ({ +function TrackTableContainer({ tracks, onDelete, onReorder, @@ -48,55 +49,79 @@ function TrackTableContainer ({ dispatch(favoritesActions.readFavorites()); }, [dispatch]); - const isTrackFavorite = (track: Track) => !_.isNil(favoriteTracks.find(t => areTracksEqualByName(t, track))); + const isTrackFavorite = (track: Track) => + !_.isNil(favoriteTracks.find((t) => areTracksEqualByName(t, track))); - const onAddToQueue = useCallback((track: Track) => { - dispatch(queueActions.addToQueue(queueActions.toQueueItem(track))); - }, [dispatch]); + const onAddToQueue = useCallback( + (track: Track) => { + dispatch(queueActions.addToQueue(queueActions.toQueueItem(track))); + }, + [dispatch] + ); - const onPlayNow = useCallback((track: Track) => { - dispatch(queueActions.playTrack(null, queueActions.toQueueItem(track))); - }, [dispatch]); + const onPlayNow = useCallback( + (track: Track) => { + dispatch(queueActions.playTrack(null, queueActions.toQueueItem(track))); + }, + [dispatch] + ); - const onPlayNext = useCallback((track: Track) => { - dispatch(queueActions.playNext(queueActions.toQueueItem(track))); - }, [dispatch]); + const onPlayNext = useCallback( + (track: Track) => { + dispatch(queueActions.playNext(queueActions.toQueueItem(track))); + }, + [dispatch] + ); - const onPlayAll = useCallback((tracks: Track[]) => { - dispatch(queueActions.clearQueue()); - dispatch(queueActions.addPlaylistTracksToQueue(tracks)); - dispatch(queueActions.selectSong(0)); - dispatch(playerActions.startPlayback(false)); - }, [dispatch]); + const onPlayAll = useCallback( + (tracks: Track[]) => { + dispatch(queueActions.clearQueue()); + dispatch(queueActions.addPlaylistTracksToQueue(tracks)); + dispatch(queueActions.selectSong(0)); + dispatch(playerActions.startPlayback(false)); + }, + [dispatch] + ); - const onAddToFavorites = useCallback((track: Track) => { - dispatch(favoritesActions.addFavoriteTrack(track)); - }, [dispatch]); + const onAddToFavorites = useCallback( + (track: Track) => { + dispatch(favoritesActions.addFavoriteTrack(track)); + }, + [dispatch] + ); - const onRemoveFromFavorites = useCallback((track: Track) => { - dispatch(favoritesActions.removeFavoriteTrack(track)); - }, [dispatch]); + const onRemoveFromFavorites = useCallback( + (track: Track) => { + dispatch(favoritesActions.removeFavoriteTrack(track)); + }, + [dispatch] + ); - const onAddToDownloads = useCallback((track: Track) => { - dispatch(downloadsActions.addToDownloads(null, track)); - }, [dispatch]); + const onAddToDownloads = useCallback( + (track: Track) => { + dispatch(downloadsActions.addToDownloads(null, track)); + }, + [dispatch] + ); - const onAddToPlaylist = useCallback((track: Track, playlist: Playlist ) => { - const clonedTrack = {...safeAddUuid(track)}; - const foundPlaylist = playlists.data?.find(p => p.name === playlist.name); - const newPlaylist = { - ...foundPlaylist, - tracks: [ - ...foundPlaylist.tracks, - clonedTrack - ] - }; - dispatch(playlistActions.updatePlaylist(newPlaylist)); - }, [dispatch, playlists]); + const onAddToPlaylist = useCallback( + (track: Track, playlist: Playlist) => { + const clonedTrack = { ...safeAddUuid(track) }; + const foundPlaylist = playlists.data?.find( + (p) => p.name === playlist.name + ); + const newPlaylist = { + ...foundPlaylist, + tracks: [...foundPlaylist.tracks, clonedTrack] + }; + dispatch(playlistActions.updatePlaylist(newPlaylist)); + }, + [dispatch, playlists] + ); const onCreatePlaylist = useCallback( - (track: Track, { name }: { name: string } ) => { - const clonedTrack = {...safeAddUuid(track)}; + (track: Track, { name }: { name: string }) => { + const clonedTrack = { ...safeAddUuid(track) }; if (clonedTrack.artist.name) { _.set(clonedTrack, 'artist', clonedTrack.artist.name); } @@ -105,10 +130,21 @@ function TrackTableContainer ({ [dispatch] ); - const onDragEnd = useCallback['onDragEnd']>((result) => { - const { source, destination } = result; - onReorder(source.index, destination.index); - }, [onReorder]); + const onDragEnd = useCallback['onDragEnd']>( + (result) => { + const { source, destination } = result; + onReorder(source.index, destination.index); + }, + [onReorder] + ); + const onUpdateTrack = useCallback( + (index: number, updatedTrack: Track) => { + if (settings.onTrackUpdate) { + settings.onTrackUpdate(index, updatedTrack); + } + }, + [settings.onTrackUpdate] + ); const popupTranstation = useTranslation('track-popup').t; const popupStrings = { @@ -129,42 +165,55 @@ function TrackTableContainer ({ const trackTableTranslation = useTranslation('track-table').t; const trackTableStrings = { - addSelectedTracksToQueue: trackTableTranslation('add-selected-tracks-to-queue'), - addSelectedTracksToDownloads: trackTableTranslation('add-selected-tracks-to-downloads'), - addSelectedTracksToFavorites: trackTableTranslation('add-selected-tracks-to-favorites'), + addSelectedTracksToQueue: trackTableTranslation( + 'add-selected-tracks-to-queue' + ), + addSelectedTracksToDownloads: trackTableTranslation( + 'add-selected-tracks-to-downloads' + ), + addSelectedTracksToFavorites: trackTableTranslation( + 'add-selected-tracks-to-favorites' + ), + playSelectedTracksNow: trackTableTranslation('play-selected-tracks-now'), - tracksSelectedLabelSingular: trackTableTranslation('tracks-selected-label-singular'), - tracksSelectedLabelPlural: trackTableTranslation('tracks-selected-label-plural'), + tracksSelectedLabelSingular: trackTableTranslation( + 'tracks-selected-label-singular' + ), + tracksSelectedLabelPlural: trackTableTranslation( + 'tracks-selected-label-plural' + ), filterInputPlaceholder: trackTableTranslation('filter-input-placeholder') }; - return } - thumbnailHeader={} - artistHeader={t('artist')} - titleHeader={t('title')} - albumHeader={t('album')} - durationHeader={t('duration')} - strings={trackTableStrings} - playlists={playlists.data} - customColumns={customColumns} - onAddToQueue={onAddToQueue} - onPlay={onPlayNow} - onPlayNext={onPlayNext} - onPlayAll={onPlayAll} - onAddToFavorites={Boolean(displayAddToFavorites) && onAddToFavorites} - onRemoveFromFavorites={onRemoveFromFavorites} - onAddToDownloads={Boolean(displayAddToDownloads) && onAddToDownloads} - onAddToPlaylist={onAddToPlaylist} - onCreatePlaylist={onCreatePlaylist} - onDelete={onDelete} - onDragEnd={Boolean(onReorder) && onDragEnd} - popupActionStrings={popupStrings} - - isTrackFavorite={isTrackFavorite} - />; + return ( + } + thumbnailHeader={} + artistHeader={t('artist')} + titleHeader={t('title')} + albumHeader={t('album')} + durationHeader={t('duration')} + strings={trackTableStrings} + playlists={playlists.data} + customColumns={customColumns} + onAddToQueue={onAddToQueue} + onPlay={onPlayNow} + onPlayNext={onPlayNext} + onPlayAll={onPlayAll} + onAddToFavorites={Boolean(displayAddToFavorites) && onAddToFavorites} + onRemoveFromFavorites={onRemoveFromFavorites} + onAddToDownloads={Boolean(displayAddToDownloads) && onAddToDownloads} + onAddToPlaylist={onAddToPlaylist} + onCreatePlaylist={onCreatePlaylist} + onDelete={onDelete} + onDragEnd={Boolean(onReorder) && onDragEnd} + popupActionStrings={popupStrings} + isTrackFavorite={isTrackFavorite} + onTrackUpdate={onUpdateTrack} + /> + ); } export default TrackTableContainer; diff --git a/packages/ui/lib/components/GridTrackTable/Cells/EditableTextCell.tsx b/packages/ui/lib/components/GridTrackTable/Cells/EditableTextCell.tsx new file mode 100644 index 0000000000..e5d70bfb61 --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/Cells/EditableTextCell.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { CellProps } from 'react-table'; +import { Track } from '@nuclear/ui/lib/types'; +import { TextCell } from './TextCell'; + +export const EditableArtistCell: React.FC< + CellProps & { + onTrackUpdate?: (index: number, updatedTrack: Track) => void; + } +> = (props) => { + const { cell, row, onTrackUpdate } = props; + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState( + typeof cell.value === 'string' ? cell.value : cell.value.name + ); + + if (!onTrackUpdate) { + return ; + } + + return isEditing ? ( + setEditValue(e.target.value)} + onBlur={() => { + setIsEditing(false); + if (editValue !== cell.value) { + const updatedTrack = { ...row.original }; + if (typeof updatedTrack.artist === 'string') { + updatedTrack.artist = editValue; + } else { + updatedTrack.artist = { name: editValue }; + } + onTrackUpdate(row.index, updatedTrack); + } + }} + autoFocus + /> + ) : ( +
setIsEditing(true)} + > + {editValue} +
+ ); +}; diff --git a/packages/ui/lib/components/GridTrackTable/Cells/EditableTitleCell.tsx b/packages/ui/lib/components/GridTrackTable/Cells/EditableTitleCell.tsx new file mode 100644 index 0000000000..01c7fcebc9 --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/Cells/EditableTitleCell.tsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import { CellProps } from 'react-table'; +import { Track } from '@nuclear/ui/lib/types'; +import { TitleCell } from './TitleCell'; + +export const EditableTitleCell: React.FC< + CellProps & { + onTrackUpdate?: (index: number, updatedTrack: Track) => void; + } +> = (props) => { + const { cell, row, onTrackUpdate } = props; + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(cell.value); + + if (!onTrackUpdate) { + return ; + } + + return isEditing ? ( + setEditValue(e.target.value)} + onBlur={() => { + setIsEditing(false); + if (editValue !== cell.value) { + const updatedTrack = { ...row.original }; + updatedTrack.title = editValue; + onTrackUpdate(row.index, updatedTrack); + } + }} + autoFocus + /> + ) : ( +
setIsEditing(true)}> + +
+ ); +}; diff --git a/packages/ui/lib/components/GridTrackTable/index.tsx b/packages/ui/lib/components/GridTrackTable/index.tsx index eafc22967e..47af6f5bf7 100644 --- a/packages/ui/lib/components/GridTrackTable/index.tsx +++ b/packages/ui/lib/components/GridTrackTable/index.tsx @@ -1,15 +1,42 @@ -import { Column, ColumnInstance, HeaderGroup, Row, TableInstance, TableState, UseGlobalFiltersInstanceProps, UseRowSelectInstanceProps, UseRowSelectRowProps, UseSortByColumnProps, UseSortByInstanceProps, UseSortByState, useGlobalFilter, useRowSelect, useSortBy, useTable } from 'react-table'; +import { + Column, + ColumnInstance, + HeaderGroup, + Row, + TableInstance, + TableState, + UseGlobalFiltersInstanceProps, + UseRowSelectInstanceProps, + UseRowSelectRowProps, + UseSortByColumnProps, + UseSortByInstanceProps, + UseSortByState, + useGlobalFilter, + useRowSelect, + useSortBy, + useTable +} from 'react-table'; import React, { useMemo, useState } from 'react'; -import { DragDropContext, DragDropContextProps, Droppable } from 'react-beautiful-dnd'; +import { + DragDropContext, + DragDropContextProps, + Droppable +} from 'react-beautiful-dnd'; import { FixedSizeList } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; - -import { TrackTableColumn, TrackTableExtraProps, TrackTableHeaders, TrackTableSettings, TrackTableStrings } from '../TrackTable/types'; + +import { + TrackTableColumn, + TrackTableExtraProps, + TrackTableHeaders, + TrackTableSettings, + TrackTableStrings +} from '../TrackTable/types'; import { TextHeader } from './Headers/TextHeader'; import { TextCell } from './Cells/TextCell'; import { Track } from '../../types'; import { getTrackThumbnail } from '../TrackRow'; - + import styles from './styles.scss'; import artPlaceholder from '../../../resources/media/art_placeholder.png'; import { ThumbnailCell } from './Cells/ThumbnailCell'; @@ -22,147 +49,199 @@ import { PositionCell } from './Cells/PositionCell'; import { GridTrackTableRowClone } from './GridTrackTableRowClone'; import { DeleteCell } from './Cells/DeleteCell'; import { FavoriteCell } from './Cells/FavoriteCell'; -import { TitleCell } from './Cells/TitleCell'; -import { Input } from 'semantic-ui-react'; -import Button from '../Button'; - +import { EditableTitleCell } from './Cells/EditableTitleCell'; +import { EditableArtistCell } from './Cells/EditableTextCell'; + +import { Button, Modal, Form, Input } from 'semantic-ui-react'; + export type GridTrackTableProps = { className?: string; tracks: T[]; isTrackFavorite: (track: T) => boolean; onDragEnd?: DragDropContextProps['onDragEnd']; + onTrackUpdate?: (index: number, updatedTrack: T) => void; strings: TrackTableStrings; - customColumns?: (ColumnWithWidth)[]; -} & TrackTableHeaders - & TrackTableSettings - & TrackTableExtraProps; - -type ColumnWithWidth = Column & { columnWidth: string; }; -type TrackTableColumnInstance = ColumnInstance & UseSortByColumnProps; -type TrackTableHeaderGroup = HeaderGroup & UseSortByColumnProps; -type TrackTableInstance = TableInstance & UseGlobalFiltersInstanceProps & UseSortByInstanceProps & UseRowSelectInstanceProps; + customColumns?: ColumnWithWidth[]; +} & TrackTableHeaders & + TrackTableSettings & + TrackTableExtraProps; + +type ColumnWithWidth = Column & { columnWidth: string }; +type TrackTableColumnInstance = ColumnInstance & + UseSortByColumnProps; +type TrackTableHeaderGroup = HeaderGroup & + UseSortByColumnProps; +type TrackTableInstance = TableInstance & + UseGlobalFiltersInstanceProps & + UseSortByInstanceProps & + UseRowSelectInstanceProps; type TrackTableState = TableState & UseSortByState; export type TrackTableRow = Row & UseRowSelectRowProps; - + export const GridTrackTable = ({ className, tracks, - customColumns=[], + customColumns = [], isTrackFavorite, onDragEnd, - + onTrackUpdate, + positionHeader, thumbnailHeader, artistHeader, titleHeader, albumHeader, durationHeader, - - displayHeaders=true, - displayDeleteButton=true, - displayPosition=true, - displayFavorite=true, - displayArtist=true, - displayAlbum=true, - displayThumbnail=true, - displayDuration=true, - displayCustom=true, - selectable=true, - searchable=false, - + + displayHeaders = true, + displayDeleteButton = true, + displayPosition = true, + displayFavorite = true, + displayArtist = true, + displayAlbum = true, + displayThumbnail = true, + displayDuration = true, + displayCustom = true, + selectable = true, + searchable = false, + ...extraProps }: GridTrackTableProps) => { - const shouldDisplayDuration = displayDuration && tracks.every(track => Boolean(track.duration)); - const columns = useMemo(() => [ - selectable && { - id: TrackTableColumn.Selection, - Header: SelectionHeader, - Cell: SelectionCell, - columnWidth: '7.5em' - }, - displayPosition && { - id: TrackTableColumn.Position, - Header: ({ column }) => } - header={positionHeader} - isCentered - />, - accessor: (track: T) => track.position, - Cell: PositionCell, - enableSorting: true, - columnWidth: '4em' - } as Column, - displayThumbnail && { - id: TrackTableColumn.Thumbnail, - Header: ({ column }) => , - accessor: (track: T) => getTrackThumbnail(track) || artPlaceholder, - Cell: ThumbnailCell, - columnWidth: '3em' - }, - displayFavorite && { - id: TrackTableColumn.Favorite, - accessor: isTrackFavorite, - Cell: FavoriteCell, - columnWidth: '3em' - }, - { - id: TrackTableColumn.Title, - Header: ({ column }) => , - accessor: (track: T) => track.title ?? track.name, - Cell: TitleCell, - enableSorting: true, - columnWidth: 'minmax(8em, 1fr)' - }, - displayArtist && { - id: TrackTableColumn.Artist, - Header: ({ column }) => , - accessor: (track: T) => isString(track.artist) - ? track.artist - : track.artist.name, - Cell: TextCell, - enableSorting: true, - columnWidth: '6em' - }, - displayAlbum && { - id: TrackTableColumn.Album, - Header: ({ column }: { column: TrackTableColumnInstance }) => , - accessor: (track: T) => track.album, - enableSorting: true, - Cell: TextCell, - columnWidth: '6em' - } as Column, - shouldDisplayDuration && { - id: TrackTableColumn.Duration, - Header: ({ column }) => , - accessor: (track: T) => { - if (isString(track.duration)) { - return track.duration; - } else if (isNumber(track.duration)) { - return formatDuration(track.duration); - } else { - return null; - } - }, - Cell: TextCell, - columnWidth: '6em' - }, - ...customColumns, - displayDeleteButton && { - id: TrackTableColumn.Delete, - Cell: DeleteCell, - columnWidth: '3em' - } as Column - ].filter(Boolean), [displayDeleteButton, displayPosition, displayThumbnail, displayFavorite, isTrackFavorite, titleHeader, displayArtist, artistHeader, displayAlbum, albumHeader, shouldDisplayDuration, durationHeader, selectable, positionHeader, thumbnailHeader]); - + const shouldDisplayDuration = + displayDuration && tracks.every((track) => Boolean(track.duration)); + const columns = useMemo( + () => + [ + selectable && { + id: TrackTableColumn.Selection, + Header: SelectionHeader, + Cell: SelectionCell, + columnWidth: '7.5em' + }, + displayPosition && + ({ + id: TrackTableColumn.Position, + Header: ({ column }) => ( + } + header={positionHeader} + isCentered + /> + ), + accessor: (track: T) => track.position, + Cell: PositionCell, + enableSorting: true, + columnWidth: '4em' + } as Column), + displayThumbnail && { + id: TrackTableColumn.Thumbnail, + Header: ({ column }) => ( + + ), + accessor: (track: T) => getTrackThumbnail(track) || artPlaceholder, + Cell: ThumbnailCell, + columnWidth: '3em' + }, + displayFavorite && { + id: TrackTableColumn.Favorite, + accessor: isTrackFavorite, + Cell: FavoriteCell, + columnWidth: '3em' + }, + { + id: TrackTableColumn.Title, + Header: ({ column }) => ( + + ), + accessor: (track: T) => track.title ?? track.name, + Cell: (props) => ( + + ), + enableSorting: true, + columnWidth: 'minmax(8em, 1fr)' + }, + displayArtist && { + id: TrackTableColumn.Artist, + Header: ({ column }) => ( + + ), + accessor: (track: T) => + isString(track.artist) ? track.artist : track.artist.name, + Cell: (props) => ( + + ), + enableSorting: true, + columnWidth: '6em' + }, + displayAlbum && + ({ + id: TrackTableColumn.Album, + Header: ({ column }: { column: TrackTableColumnInstance }) => ( + + ), + accessor: (track: T) => track.album, + enableSorting: true, + Cell: TextCell, + columnWidth: '6em' + } as Column), + shouldDisplayDuration && { + id: TrackTableColumn.Duration, + Header: ({ column }) => ( + + ), + accessor: (track: T) => { + if (isString(track.duration)) { + return track.duration; + } else if (isNumber(track.duration)) { + return formatDuration(track.duration); + } else { + return null; + } + }, + Cell: TextCell, + columnWidth: '6em' + }, + ...customColumns, + displayDeleteButton && + ({ + id: TrackTableColumn.Delete, + Cell: DeleteCell, + columnWidth: '3em' + } as Column) + ].filter(Boolean), + [ + displayDeleteButton, + displayPosition, + displayThumbnail, + displayFavorite, + isTrackFavorite, + titleHeader, + displayArtist, + artistHeader, + displayAlbum, + albumHeader, + shouldDisplayDuration, + durationHeader, + selectable, + positionHeader, + thumbnailHeader + ] + ); + const data = useMemo(() => tracks, [tracks]); - + const initialState: Partial & UseSortByState> = { sortBy: [{ id: TrackTableColumn.Position, desc: false }] }; - const table = useTable({ columns, data, initialState }, useGlobalFilter, useSortBy, useRowSelect); + const table = useTable( + { columns, data, initialState }, + useGlobalFilter, + useSortBy, + useRowSelect + ); const [globalFilter, setGlobalFilterState] = useState(''); // Required, because useGlobalFilter does not provide a way to get the current filter value - + const { getTableProps, getTableBodyProps, @@ -173,113 +252,113 @@ export const GridTrackTable = ({ state: tableState, selectedFlatRows } = table as TrackTableInstance; - + const onFilterClick = () => { setGlobalFilter(''); setGlobalFilterState(''); }; - - const gridTemplateColumns = columns.map((column: ColumnWithWidth) => column.columnWidth ?? '1fr').join(' '); - + + const gridTemplateColumns = columns + .map((column: ColumnWithWidth) => column.columnWidth ?? '1fr') + .join(' '); + // Disabled when there are selected rows, or when sorted by anything other than position - const isDragDisabled = !onDragEnd || selectedFlatRows.length > 0 || (tableState as TrackTableState).sortBy[0]?.id !== TrackTableColumn.Position; - - return
- { - searchable && -
- { - setGlobalFilter(e.target.value); - setGlobalFilterState(e.target.value); - }} - value={globalFilter} - /> -
- } -
-
- {headerGroups.map(headerGroup => ( -
- { - headerGroup.headers.map((column: TrackTableHeaderGroup) => ( + const isDragDisabled = + !onDragEnd || + selectedFlatRows.length > 0 || + (tableState as TrackTableState).sortBy[0]?.id !== + TrackTableColumn.Position; + + return ( +
+ {searchable && ( +
+ { + setGlobalFilter(e.target.value); + setGlobalFilterState(e.target.value); + }} + value={globalFilter} + /> +
+ )} +
+
+ {headerGroups.map((headerGroup) => ( +
+ {headerGroup.headers.map((column: TrackTableHeaderGroup) => (
{column.render('Header', extraProps)} -
)) - } -
- ))} -
- - - {(droppableProvided, droppableSnapshot) => ( -
- - {({ height, width }) => - [], - prepareRow: prepareRow as (row: Row) => void, - gridTemplateColumns, - isDragDisabled, - extraProps - }} - outerRef={droppableProvided.innerRef} - > - {GridTrackTableRow} - - } - +
+ ))}
- )} - - + ))} +
+ + + {(droppableProvided, droppableSnapshot) => ( +
+ + {({ height, width }) => ( + [], + prepareRow: prepareRow as (row: Row) => void, + gridTemplateColumns, + isDragDisabled, + extraProps + }} + outerRef={droppableProvided.innerRef} + > + {GridTrackTableRow} + + )} + +
+ )} +
+
+
-
; + ); }; - diff --git a/packages/ui/lib/components/TrackTable/types.ts b/packages/ui/lib/components/TrackTable/types.ts index 840d888144..79ac11a7de 100644 --- a/packages/ui/lib/components/TrackTable/types.ts +++ b/packages/ui/lib/components/TrackTable/types.ts @@ -23,13 +23,14 @@ export type TrackTableExtraProps = { onRemoveFromFavorites?: (track: T) => void; onAddToPlaylist?: (track: T, { name }: { name: string }) => void; onCreatePlaylist?: (track: T, { name }: { name: string }) => void; + onTrackUpdate?: (index: number, updatedTrack: T) => void; onAddToDownloads?: (track: T) => void; onDelete?: (track: T, idx: number) => void; playlists?: Array<{ name: string; }>; - popupActionStrings?: TrackPopupStrings -} + popupActionStrings?: TrackPopupStrings; +}; export type TrackTableStrings = { addSelectedTracksToQueue: string; @@ -39,7 +40,7 @@ export type TrackTableStrings = { tracksSelectedLabelSingular: string; tracksSelectedLabelPlural: string; filterInputPlaceholder: string; -} +}; export type TrackTableHeaders = { positionHeader: React.ReactNode; @@ -48,7 +49,7 @@ export type TrackTableHeaders = { titleHeader: string; albumHeader: string; durationHeader: string; -} +}; export type TrackTableSettings = { displayHeaders?: boolean; @@ -63,5 +64,3 @@ export type TrackTableSettings = { selectable?: boolean; searchable?: boolean; }; - - From 8a3beed4d534180da877fd15bb87faca540fdb0e Mon Sep 17 00:00:00 2001 From: olafpd Date: Sat, 16 Nov 2024 19:39:20 -0500 Subject: [PATCH 2/4] update snapshots --- .../AlbumViewContainer.test.tsx.snap | 186 +++++++------ .../ArtistViewContainer.test.tsx.snap | 186 +++++++------ .../DashboardContainer.test.tsx.snap | 126 ++++----- .../DeezerPlaylistAdapter.test.tsx.snap | 128 ++++----- .../FavoritesContainer.tracks.test.tsx.snap | 256 +++++++++--------- .../LibraryViewContainer.test.tsx.snap | 189 ++++++------- .../PlaylistViewContainer.test.tsx.snap | 128 ++++----- .../SpotifyPlaylistAdapter.test.tsx.snap | 128 ++++----- 8 files changed, 677 insertions(+), 650 deletions(-) diff --git a/packages/app/app/containers/AlbumViewContainer/__snapshots__/AlbumViewContainer.test.tsx.snap b/packages/app/app/containers/AlbumViewContainer/__snapshots__/AlbumViewContainer.test.tsx.snap index e3d17fecb1..39278d0148 100644 --- a/packages/app/app/containers/AlbumViewContainer/__snapshots__/AlbumViewContainer.test.tsx.snap +++ b/packages/app/app/containers/AlbumViewContainer/__snapshots__/AlbumViewContainer.test.tsx.snap @@ -292,42 +292,44 @@ exports[`Album view container should display an album 1`] = ` />
-
- +
- test track 1 - - -