diff --git a/packages/ui/lib/components/Button/styles.scss b/packages/ui/lib/components/Button/styles.scss index 54144613cc..31bba140ef 100644 --- a/packages/ui/lib/components/Button/styles.scss +++ b/packages/ui/lib/components/Button/styles.scss @@ -1,12 +1,12 @@ - @import "../../common.scss"; button.nuclear.ui.button { position: relative; - + background: $bglight; color: $white; white-space: nowrap; + margin: 0; &:hover { background: mix($bglight, $bgdark, 90%); @@ -53,7 +53,7 @@ button.nuclear.ui.button { "purple": $purple, "pink": $pink, "orange": $orange, - "red": $red, + "red": $red ); @each $color in $colors { $button-color: map-get($colorvars, $color); @@ -88,7 +88,9 @@ button.nuclear.ui.button { padding: 0; margin: 0 0.5em; - &:hover, &:focus, &:active { + &:hover, + &:focus, + &:active { background: transparent; } diff --git a/packages/ui/lib/components/GridTrackTable/Cells/DeleteCell.tsx b/packages/ui/lib/components/GridTrackTable/Cells/DeleteCell.tsx new file mode 100644 index 0000000000..afc01ad097 --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/Cells/DeleteCell.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { CellProps } from 'react-table'; +import cx from 'classnames'; + +import styles from '../styles.scss'; +import { Track } from '../../../types'; +import { TrackTableExtraProps } from '../../TrackTable/types'; +import Button from '../../Button'; + +export const DeleteCell: React.FC & TrackTableExtraProps> = ({ + cell, + row, + onDelete +}) =>
+
; diff --git a/packages/ui/lib/components/GridTrackTable/Cells/FavoriteCell.tsx b/packages/ui/lib/components/GridTrackTable/Cells/FavoriteCell.tsx new file mode 100644 index 0000000000..4d4f3ebd71 --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/Cells/FavoriteCell.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { CellProps } from 'react-table'; +import cx from 'classnames'; + +import { TrackTableExtraProps } from '../../TrackTable/types'; +import { Track } from '../../../types'; +import Button from '../../Button'; +import styles from '../styles.scss'; + +export const FavoriteCell: React.FC & TrackTableExtraProps> = ({ + cell, + row, + value, + onAddToFavorites, + onRemoveFromFavorites +}) =>
+
; diff --git a/packages/ui/lib/components/GridTrackTable/Cells/PositionCell.tsx b/packages/ui/lib/components/GridTrackTable/Cells/PositionCell.tsx index be2d8a0401..00aca6f452 100644 --- a/packages/ui/lib/components/GridTrackTable/Cells/PositionCell.tsx +++ b/packages/ui/lib/components/GridTrackTable/Cells/PositionCell.tsx @@ -14,7 +14,7 @@ export const PositionCell: React.FC & TrackTableExtraProps
; diff --git a/packages/ui/lib/components/GridTrackTable/GridTrackTableRow.tsx b/packages/ui/lib/components/GridTrackTable/GridTrackTableRow.tsx index 523eda9298..e0b2860693 100644 --- a/packages/ui/lib/components/GridTrackTable/GridTrackTableRow.tsx +++ b/packages/ui/lib/components/GridTrackTable/GridTrackTableRow.tsx @@ -33,7 +33,7 @@ export const GridTrackTableRow = memo(({ index, style, data }:
(props: GridTrackTableRowC return
= { className?: string; - column: ColumnInstance & UseSortByColumnProps; + column: ColumnInstance & UseSortByColumnProps; header: string | React.ReactNode; isCentered?: boolean; 'data-testid'?: string; }; -export const TextHeader: React.FC = ({ +export const TextHeader: (props: TextHeaderProps) => React.ReactElement> = ({ className, column, header, diff --git a/packages/ui/lib/components/GridTrackTable/index.tsx b/packages/ui/lib/components/GridTrackTable/index.tsx index 19b003d48c..6701f23dea 100644 --- a/packages/ui/lib/components/GridTrackTable/index.tsx +++ b/packages/ui/lib/components/GridTrackTable/index.tsx @@ -1,5 +1,5 @@ -import { Column, Row, TableInstance, TableOptions, TableState, UseSortByInstanceProps, UseSortByState, useRowSelect, useSortBy, useTable } from 'react-table'; -import React, { useMemo, memo } from 'react'; +import { Column, Row, TableInstance, TableOptions, TableState, UseGlobalFiltersInstanceProps, UseSortByInstanceProps, UseSortByState, useGlobalFilter, useRowSelect, useSortBy, useTable } from 'react-table'; +import React, { useMemo, memo, useState } from 'react'; import { DragDropContext, DragDropContextProps, Draggable, Droppable } from 'react-beautiful-dnd'; import { FixedSizeList } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -20,6 +20,11 @@ import { SelectionHeader } from './Headers/SelectionHeader'; import { formatDuration } from '../../utils'; 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'; export type GridTrackTableProps = { className?: string; @@ -32,6 +37,8 @@ export type GridTrackTableProps = { & TrackTableSettings & TrackTableExtraProps; + type ColumnWithWidth = Column & { columnWidth: string; }; + export const GridTrackTable = ({ className, tracks, @@ -62,6 +69,11 @@ export const GridTrackTable = ({ }: GridTrackTableProps) => { const shouldDisplayDuration = displayDuration && tracks.every(track => Boolean(track.duration)); const columns = useMemo(() => [ + displayDeleteButton && { + id: TrackTableColumn.Delete, + Cell: DeleteCell, + columnWidth: '3em' + }, displayPosition && { id: TrackTableColumn.Position, Header: ({ column }) => ({ Cell: PositionCell, enableSorting: true, columnWidth: '4em' - }, + } as Column, displayThumbnail && { id: TrackTableColumn.Thumbnail, Header: ({ column }) => , @@ -81,13 +93,19 @@ export const GridTrackTable = ({ 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: TextCell, + Cell: TitleCell, enableSorting: true, - columnWidth: '1fr' + columnWidth: 'minmax(8em, 1fr)' }, displayArtist && { id: TrackTableColumn.Artist, @@ -106,7 +124,7 @@ export const GridTrackTable = ({ enableSorting: true, Cell: TextCell, columnWidth: '1fr' - }, + } as Column, shouldDisplayDuration && { id: TrackTableColumn.Duration, Header: ({ column }) => , @@ -135,18 +153,54 @@ export const GridTrackTable = ({ const initialState: Partial & UseSortByState> = { sortBy: [{ id: TrackTableColumn.Title, desc: false }] }; - const table = useTable({ columns, data, initialState }, useSortBy, useRowSelect) as (TableInstance & UseSortByInstanceProps); + const table = useTable({ columns, data, initialState }, useGlobalFilter, useSortBy, useRowSelect) as (TableInstance & UseSortByInstanceProps & UseGlobalFiltersInstanceProps); + const [globalFilter, setGlobalFilterState] = useState(''); // Required, because useGlobalFilter does not provide a way to get the current filter value - const {getTableProps, getTableBodyProps, headerGroups, rows, prepareRow} = table; + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + setGlobalFilter + } = table; + + const onFilterClick = () => { + setGlobalFilter(''); + setGlobalFilterState(''); + }; - const gridTemplateColumns = columns.map(column => 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 || table.selectedFlatRows.length > 0 || table.state.sortBy[0]?.id !== TrackTableColumn.Position; return
+ { + searchable && +
+ { + setGlobalFilter(e.target.value); + setGlobalFilterState(e.target.value); + }} + value={globalFilter} + /> +
+ }
diff --git a/packages/ui/lib/components/GridTrackTable/styles.scss b/packages/ui/lib/components/GridTrackTable/styles.scss index 0e33b3c14d..1e0cff0bf5 100644 --- a/packages/ui/lib/components/GridTrackTable/styles.scss +++ b/packages/ui/lib/components/GridTrackTable/styles.scss @@ -8,7 +8,46 @@ height: 100%; } -.track_table { +.track_table_wrapper .track_table_filter_row { + display: flex; + flex-flow: row; + justify-content: flex-end; + align-items: center; + margin-bottom: 0.5em; + + .track_table_filter_input { + input { + background: rgba($bglighter, 0.25); + color: $white; + border: 1px solid transparent; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + &::placeholder { + color: rgba($white, 0.5); + } + + &::selection { + background-color: $blue; + color: $white; + } + + &:active, + &:focus { + &::placeholder { + color: rgba($white, 0.2); + } + } + } + } + + .track_table_filter_button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +} + +.grid_track_table { position: relative; display: flex; flex-flow: column; @@ -59,7 +98,7 @@ overflow-x: visible !important; } -.track_table_row { +.grid_track_table_row { display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); @@ -77,11 +116,15 @@ .play_button { display: inline-flex; } + + .title_cell { + .title_cell_buttons { + display: inline-flex; + } + } } - &.is_dragging, - &:active, - &:focus { + &.is_dragging { background: darken($bgdefault, 5%); border: 1px solid rgba($pink, 0.8); border-radius: 4px; @@ -92,9 +135,14 @@ } } -.track_table_cell { +.grid_track_table_cell { display: flex; align-items: center; + + button.nuclear.button { + width: 2.5em; + height: 2.5em; + } } .text_header { @@ -110,6 +158,8 @@ } .text_cell { + @include ellipsis; + white-space: nowrap; padding: 0 1em; } @@ -155,22 +205,61 @@ .play_button { display: none; - margin: 0; - width: 32px; - height: 32px; - i.play.icon { - width: 1em !important; - height: 1em !important; + // Manual adjustments to center the icon + padding-top: 0.75em !important; + i.play.icon { // Manual adjustments to center the icon - margin-left: 0.125em !important; - margin-top: 0.0625em !important; + margin-left: 0.06em !important; + } + } +} - &::before { - width: 1em !important; - height: 1em !important; - } +.delete_cell { + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; +} + +.favorite_cell { + justify-content: center; + padding-left: 1em; +} + +.grid_track_table_cell.title_cell { + .title_cell_content { + display: inline-flex; + flex-flow: row; + align-items: center; + width: 100%; + } + + .title_cell_value { + @include ellipsis; + white-space: nowrap; + flex: 1 1 auto; + } + + .title_cell_buttons { + display: none; + flex-flow: row; + flex: 0 0 auto; + } + .title_cell_button { + display: flex; + justify-content: center; + align-items: center; + padding: 0; + width: 2.5em; + height: 2.5em; + margin: 0; + border: none; + background: none; + + i.icon { + margin: 0 0 0.5px 0 !important; } } } diff --git a/packages/ui/stories/components/GridTrackTable.stories.tsx b/packages/ui/stories/components/GridTrackTable.stories.tsx index 6a3cbbdf3c..6693fefcce 100644 --- a/packages/ui/stories/components/GridTrackTable.stories.tsx +++ b/packages/ui/stories/components/GridTrackTable.stories.tsx @@ -5,6 +5,7 @@ import { GridTrackTableProps } from '../../lib/components/GridTrackTable'; import { Track } from '../../lib/types'; import { swap } from '../storyUtils'; import { Icon } from 'semantic-ui-react'; +import { action } from '@storybook/addon-actions'; export default { title: 'Components/GridTrackTable', @@ -37,6 +38,17 @@ const tracks = [ } as Track ]; +const callbacks = { + onPlay: action('Started playing'), + onPlayAll: action('Started playing all'), + onAddToQueue: action('Added to queue'), + onAddToFavorites: action('Added to favorites'), + onRemoveFromFavorites: action('Removed from favorites'), + onAddToDownloads: action('Added to downloads'), + onAddToPlaylist: action('Added to playlist'), + onDelete: action('Deleted') +}; + const gridTrackTableStrings = { addSelectedTracksToQueue: 'Add selected to queue', addSelectedTracksToDownloads: 'Add selected to downloads', @@ -57,6 +69,7 @@ const TrackTableTemplate = (args: Partial false} strings={gridTrackTableStrings} + {...callbacks} {...args} />; @@ -83,3 +96,10 @@ export const DragAndDropVirtualized = () => { />
; }; + +export const Searchable = () =>
+ +
;