From 80888751bae6b035c20d5d143af313156b45398d Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Sat, 24 Aug 2024 01:53:00 +0200 Subject: [PATCH 01/32] Create GridTrackTable - to replace TrackTables in the future --- packages/app/webpack.config.ts | 10 +- .../GridTrackTable/Cells/TextCell.tsx | 19 ++ .../GridTrackTable/Cells/ThumbnailCell.tsx | 16 ++ .../GridTrackTable/Headers/TextHeader.tsx | 30 ++++ .../lib/components/GridTrackTable/index.tsx | 168 ++++++++++++++++++ .../lib/components/GridTrackTable/styles.scss | 75 ++++++++ packages/ui/lib/index.ts | 1 + .../components/GridTrackTable.stories.tsx | 73 ++++++++ 8 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 packages/ui/lib/components/GridTrackTable/Cells/TextCell.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/Cells/ThumbnailCell.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/Headers/TextHeader.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/index.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/styles.scss create mode 100644 packages/ui/stories/components/GridTrackTable.stories.tsx diff --git a/packages/app/webpack.config.ts b/packages/app/webpack.config.ts index b2ca44b1d6..537b0e7de4 100644 --- a/packages/app/webpack.config.ts +++ b/packages/app/webpack.config.ts @@ -75,7 +75,7 @@ module.exports = (env) => { '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread' ], - ignore: [/node_modules\/(?!@nuclear).*/] + include: [APP_DIR, ...NUCLEAR_MODULES] } }; const contentSecurity = @@ -246,9 +246,17 @@ module.exports = (env) => { static: { publicPath: '/' }, + watchFiles: ['../../packages/**/*.{js,jsx,ts,tsx}'], allowedHosts: 'all' }; + + config.watchOptions = { + ignored: ['node_modules', 'dist'], + aggregateTimeout: 300, + poll: 1000 + }; } + return config; }; diff --git a/packages/ui/lib/components/GridTrackTable/Cells/TextCell.tsx b/packages/ui/lib/components/GridTrackTable/Cells/TextCell.tsx new file mode 100644 index 0000000000..ec34b1f47f --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/Cells/TextCell.tsx @@ -0,0 +1,19 @@ + +import React from 'react'; +import { CellProps } from 'react-table'; +import cx from 'classnames'; + +import { Track } from '@nuclear/core'; + +import styles from '../styles.scss'; + + +export const TextCell: React.FC> = ({ + cell, + value +}) =>
+ {value} +
; diff --git a/packages/ui/lib/components/GridTrackTable/Cells/ThumbnailCell.tsx b/packages/ui/lib/components/GridTrackTable/Cells/ThumbnailCell.tsx new file mode 100644 index 0000000000..ae78144228 --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/Cells/ThumbnailCell.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { CellProps } from 'react-table'; +import cx from 'classnames'; + +import { Track } from '../../../types'; +import styles from '../styles.scss'; + +export const ThumbnailCell: React.FC> = ({ + cell, + value +}) =>
+ +
; diff --git a/packages/ui/lib/components/GridTrackTable/Headers/TextHeader.tsx b/packages/ui/lib/components/GridTrackTable/Headers/TextHeader.tsx new file mode 100644 index 0000000000..8f0f138c1c --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/Headers/TextHeader.tsx @@ -0,0 +1,30 @@ +import { ColumnInstance, UseSortByColumnProps } from 'react-table'; +import { Track } from '../../../types'; +import React from 'react'; +import { Icon } from 'semantic-ui-react'; + +type TextHeaderProps = { + column: ColumnInstance & UseSortByColumnProps; + header: string | React.ReactNode; + 'data-testid'?: string; +}; + +export const TextHeader: React.FC = ({ + column, + header, + 'data-testid': dataTestId +}) => { + const { isSorted, isSortedDesc } = column; + const name = isSortedDesc + ? 'sort content descending' : 'sort content ascending'; + + return ( +
+ {header} + { + isSorted && + + } +
+ ); +}; diff --git a/packages/ui/lib/components/GridTrackTable/index.tsx b/packages/ui/lib/components/GridTrackTable/index.tsx new file mode 100644 index 0000000000..6d88f65c36 --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/index.tsx @@ -0,0 +1,168 @@ +import { Row, TableInstance, TableState, UseSortByInstanceProps, UseSortByState, useSortBy, useTable } from 'react-table'; +import React, { useMemo, memo } from 'react'; +import { DragDropContext, DragDropContextProps, Draggable, Droppable } from 'react-beautiful-dnd'; +import cx from 'classnames'; +import { FixedSizeList, FixedSizeList as List } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; + +import { TrackTableColumn, TrackTableHeaders, TrackTableSettings } 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'; + +type GridTrackTableRowProps = { + data: { + rows: TableInstance['rows']; + prepareRow: TableInstance['prepareRow']; + extraProps: TrackTableSettings; + }; + index: number; + style: React.CSSProperties; +} + +const GridTrackTableRow = memo(({ index, style, data }: GridTrackTableRowProps) => { + const row = data.rows[index]; + data.prepareRow(row); + return + { + (draggableProvided, draggableSnapshot) => ( +
+ {row.cells.map((cell, i) => cell.render( + 'Cell', + { ...data.extraProps, key: i } + ))} +
+ )} +
; +}); + +export type GridTrackTableProps = { +tracks: T[]; +onDragEnd?: DragDropContextProps['onDragEnd']; +} & TrackTableHeaders & TrackTableSettings; + +export const GridTrackTable = ({ + tracks, + + titleHeader, + thumbnailHeader, + + displayArtist=true, + displayThumbnail=true, + + ...extraProps +}: GridTrackTableProps) => { + const columns = useMemo(() => [ + displayThumbnail && { + id: TrackTableColumn.Thumbnail, + Header: () => {thumbnailHeader}, + accessor: (track) => getTrackThumbnail(track) || artPlaceholder, + Cell: ThumbnailCell + }, + { + id: TrackTableColumn.Title, + Header: ({ column }) => , + accessor: (track) => track.title ?? track.name, + Cell: TextCell, + enableSorting: true + } + ], [displayArtist, displayThumbnail]); + + const data = useMemo(() => tracks, [tracks]); + + const initialState: Partial & UseSortByState> = { + sortBy: [{ id: TrackTableColumn.Title, desc: false }] + }; + const table = useTable({ columns, data, initialState }, useSortBy) as (TableInstance & UseSortByInstanceProps); + + const {getTableProps, getTableBodyProps, headerGroups, rows, prepareRow} = table; + + return
+
+
+ {headerGroups.map(headerGroup => ( +
+ { + headerGroup.headers.map(column => ( +
+ {column.render('Header')} +
)) + } +
+ ))} +
+ {}}> + + {(droppableProvided) => ( +
+ + {({ height, width }) => + <> + [], + prepareRow: prepareRow as (row: Row) => void, + extraProps + }} + outerRef={droppableProvided.innerRef} + > + {GridTrackTableRow} + + {droppableProvided.placeholder} + + } + +
+ )} +
+
+
+
; +}; + diff --git a/packages/ui/lib/components/GridTrackTable/styles.scss b/packages/ui/lib/components/GridTrackTable/styles.scss new file mode 100644 index 0000000000..7d934a541b --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/styles.scss @@ -0,0 +1,75 @@ +@import "../../common.scss"; + +.track_table_wrapper { + position: relative; + display: flex; + flex-flow: column; + overflow: hidden; + height: 100%; +} + +.track_table { + position: relative; + display: flex; + flex-flow: column; + width: 100%; + flex: 1 1 auto; + + .track_table_head { + display: grid; + background: $bgdefault; + color: $white; + grid-template-columns: 1fr; + flex: 0 0 auto; + } + + .track_table_header_row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); + width: 100%; + } + + .track_table_header_cell { + display: table-cell; + padding: 1em; + text-align: left; + } + + .track_table_body { + display: block; + gap: 0.25em; + overflow: auto; + flex: 1 1 auto; + + &.body_dragged_over { + background: $bgdark; + } + } + + .track_table_row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); + gap: 1em; + + color: $white; + + &:hover { + background: rgba($bgdefault, 0.25); + } + } + + .track_table_cell { + display: flex; + align-items: center; + } +} + +.thumbnail_cell { + height: 3em; + .thumbnail_cell_thumbnail { + display: block; + width: 3em; + height: 3em; + object-fit: contain; + } +} diff --git a/packages/ui/lib/index.ts b/packages/ui/lib/index.ts index e91dfbdbf6..d0cb7e4774 100644 --- a/packages/ui/lib/index.ts +++ b/packages/ui/lib/index.ts @@ -47,6 +47,7 @@ export { default as FullscreenLayer } from './components/FullscreenLayer'; export { default as FullscreenForm } from './components/FullscreenForm'; export { default as FormInput } from './components/FormInput'; export { default as InputDialog } from './components/InputDialog'; +export { GridTrackTable } from './components/GridTrackTable'; export {NuclearSignInForm} from './forms/NuclearSignInForm'; export {NuclearSignUpForm} from './forms/NuclearSignUpForm'; diff --git a/packages/ui/stories/components/GridTrackTable.stories.tsx b/packages/ui/stories/components/GridTrackTable.stories.tsx new file mode 100644 index 0000000000..037f13b4c9 --- /dev/null +++ b/packages/ui/stories/components/GridTrackTable.stories.tsx @@ -0,0 +1,73 @@ +/* eslint-disable no-console */ +import {GridTrackTable} from '../..'; +import React, { useState } from 'react'; +import { GridTrackTableProps } from '../../lib/components/GridTrackTable'; +import { Track } from '../../lib/types'; +import { swap } from '../storyUtils'; + +export default { + title: 'Components/GridTrackTable', + component: GridTrackTable +}; + +const tracks = [ + { + position: 1, + thumbnail: 'https://i.imgur.com/4euOws2.jpg', + artist: 'Test Artist', + title: 'Test Title', + album: 'Test Album', + duration: '1:00' + }, { + position: 2, + thumbnail: 'https://i.imgur.com/4euOws2.jpg', + artist: 'Test Artist 2', + name: 'Test Title 2', + album: 'Test Album', + duration: '1:00' + } as Track, + { + position: 3, + thumbnail: 'https://i.imgur.com/4euOws2.jpg', + artist: { name: 'Test Artist 3' }, + name: 'Test Title 3', + album: 'Test Album', + duration: '1:00' + } as Track +]; + + +const TrackTableTemplate = (args: Partial>) => ; + +export const Basic = () => ( +
+ +
+); + +export const DragAndDropVirtualized = () => { + const [trackRows, setTrackRows] = useState([...tracks]); + + return
+ ({ + ...trackRows[i % trackRows.length], + position: i + 1 + }))} + onDragEnd={(result) => { + const { source, destination } = result; + console.log({ source, destination }); + setTrackRows(swap(trackRows, source.index, destination!.index)); + }} + /> +
; +}; From fe50cca88ab53af649e8e96ffdd3c77df365555f Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Thu, 29 Aug 2024 23:49:17 +0200 Subject: [PATCH 02/32] Implement most of the existing track table functionalities in the grid version --- packages/ui/lib/common.scss | 29 ++- .../GridTrackTable/Cells/PositionCell.tsx | 32 +++ .../GridTrackTable/Cells/SelectionCell.tsx | 20 ++ .../GridTrackTable/Cells/TextCell.tsx | 2 +- .../GridTrackTable/GridTrackTableRow.tsx | 59 +++++ .../GridTrackTable/GridTrackTableRowClone.tsx | 41 ++++ .../Headers/SelectionHeader.tsx | 75 ++++++ .../GridTrackTable/Headers/TextHeader.tsx | 23 +- .../lib/components/GridTrackTable/index.tsx | 221 +++++++++++------- .../lib/components/GridTrackTable/styles.scss | 121 +++++++++- .../TrackTable/Cells/SelectionCell.tsx | 5 +- .../components/TrackTable/TrackTableRow.tsx | 46 ++++ .../ui/lib/components/TrackTable/styles.scss | 8 + .../components/GridTrackTable.stories.tsx | 20 +- .../stories/components/trackTable.stories.tsx | 26 +++ 15 files changed, 617 insertions(+), 111 deletions(-) create mode 100644 packages/ui/lib/components/GridTrackTable/Cells/PositionCell.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/Cells/SelectionCell.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/GridTrackTableRow.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/GridTrackTableRowClone.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/Headers/SelectionHeader.tsx create mode 100644 packages/ui/lib/components/TrackTable/TrackTableRow.tsx diff --git a/packages/ui/lib/common.scss b/packages/ui/lib/common.scss index a08423690b..2e5ce3d0db 100644 --- a/packages/ui/lib/common.scss +++ b/packages/ui/lib/common.scss @@ -58,17 +58,36 @@ $small_screen: 600px; } @mixin shadow { - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.12), 0 4px 4px rgba(0, 0, 0, 0.12), - 0 8px 8px rgba(0, 0, 0, 0.12), 0 16px 16px rgba(0, 0, 0, 0.12) !important; + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.12), + 0 2px 2px rgba(0, 0, 0, 0.12), + 0 4px 4px rgba(0, 0, 0, 0.12), + 0 8px 8px rgba(0, 0, 0, 0.12), + 0 16px 16px rgba(0, 0, 0, 0.12) !important; } @mixin textShadow { - text-shadow: 0 2px 8px rgba(0, 0, 0, 0.25), 0 4px 8px rgba(0, 0, 0, 0.25); + text-shadow: + 0 2px 8px rgba(0, 0, 0, 0.25), + 0 4px 8px rgba(0, 0, 0, 0.25); } @mixin high-shadow { - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), 0 2px 2px rgba(0, 0, 0, 0.1), 0 4px 4px rgba(0, 0, 0, 0.1), - 0 8px 8px rgba(0, 0, 0, 0.1), 0 16px 16px rgba(0, 0, 0, 0.1), 0 32px 32px rgba(0, 0, 0, 0.1) !important; + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.1), + 0 2px 2px rgba(0, 0, 0, 0.1), + 0 4px 4px rgba(0, 0, 0, 0.1), + 0 8px 8px rgba(0, 0, 0, 0.1), + 0 16px 16px rgba(0, 0, 0, 0.1), + 0 32px 32px rgba(0, 0, 0, 0.1) !important; +} + +@mixin low-shadow { + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.1), + 0 2px 2px rgba(0, 0, 0, 0.1), + 0 4px 4px rgba(0, 0, 0, 0.1), + 0 8px 8px rgba(0, 0, 0, 0.1) !important; } .nuclear { diff --git a/packages/ui/lib/components/GridTrackTable/Cells/PositionCell.tsx b/packages/ui/lib/components/GridTrackTable/Cells/PositionCell.tsx new file mode 100644 index 0000000000..be2d8a0401 --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/Cells/PositionCell.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { CellProps } from 'react-table'; +import cx from 'classnames'; + +import Button from '../../Button'; +import { TrackTableExtraProps } from '../../TrackTable/types'; +import { Track } from '../../../types'; +import styles from '../styles.scss'; + +export const PositionCell: React.FC & TrackTableExtraProps> = ({ + cell, + row, + value, + onPlay +}) =>
+
; diff --git a/packages/ui/lib/components/GridTrackTable/Cells/SelectionCell.tsx b/packages/ui/lib/components/GridTrackTable/Cells/SelectionCell.tsx new file mode 100644 index 0000000000..aeee72c7ac --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/Cells/SelectionCell.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { CellProps, UseRowSelectRowProps } from 'react-table'; + +import { Track } from '../../../types'; +import Checkbox, { CheckboxProps } from '../../Checkbox'; +import styles from '../styles.scss'; + +export const SelectionCell: React.FC & UseRowSelectRowProps> = ({ + cell, + row +}) =>
+ +
; + diff --git a/packages/ui/lib/components/GridTrackTable/Cells/TextCell.tsx b/packages/ui/lib/components/GridTrackTable/Cells/TextCell.tsx index ec34b1f47f..d5a8c23b41 100644 --- a/packages/ui/lib/components/GridTrackTable/Cells/TextCell.tsx +++ b/packages/ui/lib/components/GridTrackTable/Cells/TextCell.tsx @@ -13,7 +13,7 @@ export const TextCell: React.FC> = ({ value }) =>
{value}
; diff --git a/packages/ui/lib/components/GridTrackTable/GridTrackTableRow.tsx b/packages/ui/lib/components/GridTrackTable/GridTrackTableRow.tsx new file mode 100644 index 0000000000..523eda9298 --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/GridTrackTableRow.tsx @@ -0,0 +1,59 @@ +import React, { memo } from 'react'; +import { TableInstance } from 'react-table'; +import { Draggable } from 'react-beautiful-dnd'; +import cx from 'classnames'; + +import { TrackTableExtraProps } from '../TrackTable/types'; +import { Track } from '../../types'; +import styles from './styles.scss'; + +type GridTrackTableRowProps = { + data: { + rows: TableInstance['rows']; + prepareRow: TableInstance['prepareRow']; + gridTemplateColumns: string; + isDragDisabled: boolean; + extraProps: TrackTableExtraProps; + }; + index: number; + style: React.CSSProperties; + } + +export const GridTrackTableRow = memo(({ index, style, data }: GridTrackTableRowProps) => { + const row = data.rows[index]; + data.prepareRow(row); + return + { + (draggableProvided, draggableSnapshot) => ( +
+ {row.cells.map((cell, i) => cell.render( + 'Cell', + { ...data.extraProps, key: i } + ))} +
+ )} +
; +}); diff --git a/packages/ui/lib/components/GridTrackTable/GridTrackTableRowClone.tsx b/packages/ui/lib/components/GridTrackTable/GridTrackTableRowClone.tsx new file mode 100644 index 0000000000..93b7c056c0 --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/GridTrackTableRowClone.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { DraggableChildrenFn } from 'react-beautiful-dnd'; + +import cx from 'classnames'; +import styles from './styles.scss'; +import { Row } from 'react-table'; +import { Track } from '../../types'; +import { TrackTableExtraProps } from '../TrackTable/types'; + +export type GridTrackTableRowCloneProps = { + rows: Row[]; + gridTemplateColumns: string; + extraProps: TrackTableExtraProps; +} + +export const GridTrackTableRowClone: (props: GridTrackTableRowCloneProps) => DraggableChildrenFn = ({ + rows, + gridTemplateColumns, + extraProps +}) => (provided, snapshot, rubric) => { + const row = rows[rubric.source.index]; + return
+ {row.cells.map((cell, i) => cell.render( + 'Cell', + { ...extraProps, key: i } + ))} +
; +}; diff --git a/packages/ui/lib/components/GridTrackTable/Headers/SelectionHeader.tsx b/packages/ui/lib/components/GridTrackTable/Headers/SelectionHeader.tsx new file mode 100644 index 0000000000..18f13aded1 --- /dev/null +++ b/packages/ui/lib/components/GridTrackTable/Headers/SelectionHeader.tsx @@ -0,0 +1,75 @@ +import { Track } from '@nuclear/core'; +import { HeaderProps, UseRowSelectInstanceProps } from 'react-table'; + +import { TrackTableExtraProps, TrackTableStrings } from '../../TrackTable/types'; +import styles from '../styles.scss'; +import React from 'react'; +import Checkbox, { CheckboxProps } from '../../Checkbox'; +import Button from '../../Button'; +import ContextPopup from '../../ContextPopup'; +import PopupButton from '../../PopupButton'; + +export const SelectionHeader: React.FC & UseRowSelectInstanceProps & TrackTableExtraProps & { strings: TrackTableStrings }> = ({ + getToggleAllRowsSelectedProps, + selectedFlatRows, + onAddToQueue, + onPlayAll, + onAddToFavorites, + onAddToDownloads, + strings +}) => { + const checkboxProps = getToggleAllRowsSelectedProps(); + const selectedTracks = selectedFlatRows.map(row => row.original); + + return
+ { + (checkboxProps.checked || checkboxProps.indeterminate) &&
+ 1 ? strings.tracksSelectedLabelPlural : strings.tracksSelectedLabelSingular}`} + trigger={ +
+ } + +
; +}; diff --git a/packages/ui/lib/components/GridTrackTable/Headers/TextHeader.tsx b/packages/ui/lib/components/GridTrackTable/Headers/TextHeader.tsx index 8f0f138c1c..08a26d721c 100644 --- a/packages/ui/lib/components/GridTrackTable/Headers/TextHeader.tsx +++ b/packages/ui/lib/components/GridTrackTable/Headers/TextHeader.tsx @@ -1,17 +1,24 @@ -import { ColumnInstance, UseSortByColumnProps } from 'react-table'; -import { Track } from '../../../types'; import React from 'react'; +import { ColumnInstance, UseSortByColumnProps } from 'react-table'; import { Icon } from 'semantic-ui-react'; +import cx from 'classnames'; + +import { Track } from '../../../types'; +import styles from '../styles.scss'; type TextHeaderProps = { + className?: string; column: ColumnInstance & UseSortByColumnProps; - header: string | React.ReactNode; - 'data-testid'?: string; + header: string | React.ReactNode; + isCentered?: boolean; + 'data-testid'?: string; }; export const TextHeader: React.FC = ({ + className, column, - header, + header, + isCentered, 'data-testid': dataTestId }) => { const { isSorted, isSortedDesc } = column; @@ -19,11 +26,13 @@ export const TextHeader: React.FC = ({ ? 'sort content descending' : 'sort content ascending'; return ( -
+
{header} { isSorted && - + }
); diff --git a/packages/ui/lib/components/GridTrackTable/index.tsx b/packages/ui/lib/components/GridTrackTable/index.tsx index 6d88f65c36..19b003d48c 100644 --- a/packages/ui/lib/components/GridTrackTable/index.tsx +++ b/packages/ui/lib/components/GridTrackTable/index.tsx @@ -1,11 +1,10 @@ -import { Row, TableInstance, TableState, UseSortByInstanceProps, UseSortByState, useSortBy, useTable } from 'react-table'; +import { Column, Row, TableInstance, TableOptions, TableState, UseSortByInstanceProps, UseSortByState, useRowSelect, useSortBy, useTable } from 'react-table'; import React, { useMemo, memo } from 'react'; import { DragDropContext, DragDropContextProps, Draggable, Droppable } from 'react-beautiful-dnd'; -import cx from 'classnames'; -import { FixedSizeList, FixedSizeList as List } from 'react-window'; +import { FixedSizeList } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { TrackTableColumn, TrackTableHeaders, TrackTableSettings } 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'; @@ -14,93 +13,137 @@ import { getTrackThumbnail } from '../TrackRow'; import styles from './styles.scss'; import artPlaceholder from '../../../resources/media/art_placeholder.png'; import { ThumbnailCell } from './Cells/ThumbnailCell'; - -type GridTrackTableRowProps = { - data: { - rows: TableInstance['rows']; - prepareRow: TableInstance['prepareRow']; - extraProps: TrackTableSettings; - }; - index: number; - style: React.CSSProperties; -} - -const GridTrackTableRow = memo(({ index, style, data }: GridTrackTableRowProps) => { - const row = data.rows[index]; - data.prepareRow(row); - return - { - (draggableProvided, draggableSnapshot) => ( -
- {row.cells.map((cell, i) => cell.render( - 'Cell', - { ...data.extraProps, key: i } - ))} -
- )} -
; -}); +import { GridTrackTableRow } from './GridTrackTableRow'; +import { isNumber, isString } from 'lodash'; +import { SelectionCell } from './Cells/SelectionCell'; +import { SelectionHeader } from './Headers/SelectionHeader'; +import { formatDuration } from '../../utils'; +import { PositionCell } from './Cells/PositionCell'; +import { GridTrackTableRowClone } from './GridTrackTableRowClone'; export type GridTrackTableProps = { -tracks: T[]; -onDragEnd?: DragDropContextProps['onDragEnd']; -} & TrackTableHeaders & TrackTableSettings; + className?: string; + tracks: T[]; + isTrackFavorite: (track: T) => boolean; + onDragEnd?: DragDropContextProps['onDragEnd']; + strings: TrackTableStrings; + customColumns?: (Column & { columnWidth: string; })[]; +} & TrackTableHeaders + & TrackTableSettings + & TrackTableExtraProps; export const GridTrackTable = ({ + className, tracks, + customColumns=[], + isTrackFavorite, + onDragEnd, - titleHeader, + 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, ...extraProps }: GridTrackTableProps) => { + const shouldDisplayDuration = displayDuration && tracks.every(track => Boolean(track.duration)); const columns = useMemo(() => [ + displayPosition && { + id: TrackTableColumn.Position, + Header: ({ column }) => , + accessor: 'position', + Cell: PositionCell, + enableSorting: true, + columnWidth: '4em' + }, displayThumbnail && { id: TrackTableColumn.Thumbnail, - Header: () => {thumbnailHeader}, - accessor: (track) => getTrackThumbnail(track) || artPlaceholder, - Cell: ThumbnailCell + Header: ({ column }) => , + accessor: (track: T) => getTrackThumbnail(track) || artPlaceholder, + Cell: ThumbnailCell, + columnWidth: '3em' }, { id: TrackTableColumn.Title, Header: ({ column }) => , - accessor: (track) => track.title ?? track.name, + accessor: (track: T) => track.title ?? track.name, Cell: TextCell, - enableSorting: true + enableSorting: true, + columnWidth: '1fr' + }, + displayArtist && { + id: TrackTableColumn.Artist, + Header: ({ column }) => , + accessor: (track: T) => isString(track.artist) + ? track.artist + : track.artist.name, + Cell: TextCell, + enableSorting: true, + columnWidth: '1fr' + }, + displayAlbum && { + id: TrackTableColumn.Album, + Header: ({ column }) => , + accessor: 'album', + enableSorting: true, + Cell: TextCell, + columnWidth: '1fr' + }, + 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 + }, + ...customColumns, + selectable && { + id: TrackTableColumn.Selection, + Header: SelectionHeader, + Cell: SelectionCell, + columnWidth: '6em' } - ], [displayArtist, displayThumbnail]); + ], [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.Title, desc: false }] }; - const table = useTable({ columns, data, initialState }, useSortBy) as (TableInstance & UseSortByInstanceProps); + const table = useTable({ columns, data, initialState }, useSortBy, useRowSelect) as (TableInstance & UseSortByInstanceProps); const {getTableProps, getTableBodyProps, headerGroups, rows, prepareRow} = table; + const gridTemplateColumns = columns.map(column => 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
({
{ @@ -118,17 +162,26 @@ export const GridTrackTable = ({
- {column.render('Header')} + {column.render('Header', extraProps)}
)) } +
))}
{}}> - - {(droppableProvided) => ( + + {(droppableProvided, droppableSnapshot) => (
({ {...getTableBodyProps()} {...droppableProvided.droppableProps} > - + {({ height, width }) => - <> - [], - prepareRow: prepareRow as (row: Row) => void, - extraProps - }} - outerRef={droppableProvided.innerRef} - > - {GridTrackTableRow} - - {droppableProvided.placeholder} - + [], + prepareRow: prepareRow as (row: Row) => void, + gridTemplateColumns, + isDragDisabled, + extraProps + }} + outerRef={droppableProvided.innerRef} + > + {GridTrackTableRow} + }
diff --git a/packages/ui/lib/components/GridTrackTable/styles.scss b/packages/ui/lib/components/GridTrackTable/styles.scss index 7d934a541b..0e33b3c14d 100644 --- a/packages/ui/lib/components/GridTrackTable/styles.scss +++ b/packages/ui/lib/components/GridTrackTable/styles.scss @@ -30,42 +30,92 @@ } .track_table_header_cell { - display: table-cell; padding: 1em; text-align: left; + user-select: none; + } + + .track_table_header_spacer { + width: 10px; } .track_table_body { display: block; gap: 0.25em; - overflow: auto; + overflow-y: auto; flex: 1 1 auto; &.body_dragged_over { background: $bgdark; } } +} - .track_table_row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); - gap: 1em; +.track_table_auto_sizer { + position: relative; +} - color: $white; +.track_table_virtualized_list { + overflow-x: visible !important; +} + +.track_table_row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); + + color: $white; + user-select: none; + outline: none; + + &:hover { + background: rgba($bgdefault, 0.25); - &:hover { - background: rgba($bgdefault, 0.25); + .position_cell_value { + display: none; } + + .play_button { + display: inline-flex; + } + } + + &.is_dragging, + &:active, + &:focus { + background: darken($bgdefault, 5%); + border: 1px solid rgba($pink, 0.8); + border-radius: 4px; + } + + &.is_dragging { + @include low-shadow; } +} + +.track_table_cell { + display: flex; + align-items: center; +} - .track_table_cell { +.text_header { + &.centered { display: flex; + justify-content: center; align-items: center; } + + .text_header_icon { + margin-left: 0.5em; + } +} + +.text_cell { + padding: 0 1em; } .thumbnail_cell { height: 3em; + .thumbnail_cell_thumbnail { display: block; width: 3em; @@ -73,3 +123,54 @@ object-fit: contain; } } + +.selection_header, +.selection_cell { + position: relative; + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; +} + +.selection_header_buttons { + position: absolute; + right: 4em; +} + +.selection_header_popup_trigger { + position: relative; + margin: 0 !important; +} + +.position_cell { + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + + .position_cell_value { + display: inline-block; + } + + .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 + margin-left: 0.125em !important; + margin-top: 0.0625em !important; + + &::before { + width: 1em !important; + height: 1em !important; + } + } + } +} diff --git a/packages/ui/lib/components/TrackTable/Cells/SelectionCell.tsx b/packages/ui/lib/components/TrackTable/Cells/SelectionCell.tsx index 72f0cbc243..1fde4ec8b6 100644 --- a/packages/ui/lib/components/TrackTable/Cells/SelectionCell.tsx +++ b/packages/ui/lib/components/TrackTable/Cells/SelectionCell.tsx @@ -12,7 +12,10 @@ const SelectionCell: React.FC & UseRowSelectRowProps> = row }) => } className={cx(styles.select_cell, styles.narrow)}> {/* @ts-ignore */} - + ; export default SelectionCell; diff --git a/packages/ui/lib/components/TrackTable/TrackTableRow.tsx b/packages/ui/lib/components/TrackTable/TrackTableRow.tsx new file mode 100644 index 0000000000..5174201ff8 --- /dev/null +++ b/packages/ui/lib/components/TrackTable/TrackTableRow.tsx @@ -0,0 +1,46 @@ +import { memo } from 'react'; +import React from 'react'; +import { TableInstance } from 'react-table'; +import { Draggable } from 'react-beautiful-dnd'; +import cx from 'classnames'; + +import { TrackTableColumn, TrackTableExtraProps } from './types'; +import { Track } from '../../types'; +import styles from './styles.scss'; + +type TrackTableRowProps = { + data: { + rows: TableInstance['rows']; + prepareRow: TableInstance['prepareRow']; + extraProps: TrackTableExtraProps; + }; + index: number; + style: React.CSSProperties; +} + +export const TrackTableRow = memo(({ data, index, style }: TrackTableRowProps) => { + const row = data.rows[index]; + data.prepareRow(row); + + return + {(draggableProvided, draggableSnapshot) => { + return + { + row.cells.map((cell, i) => (cell.render('Cell', { ...data.extraProps, key: i }))) + } + ; + }} + ; +}); diff --git a/packages/ui/lib/components/TrackTable/styles.scss b/packages/ui/lib/components/TrackTable/styles.scss index 89565bd15d..0e6c5c99b2 100644 --- a/packages/ui/lib/components/TrackTable/styles.scss +++ b/packages/ui/lib/components/TrackTable/styles.scss @@ -51,9 +51,14 @@ thead { background: $bgdefault; color: $white; + + tr { + width: 100%; + } } tbody { + height: 100%; &.body_dragged_over { background: $bgdark; } @@ -89,7 +94,10 @@ } td { + display: table-cell; padding: 0 1em; + height: 3em; + max-height: 3em; } &:hover { diff --git a/packages/ui/stories/components/GridTrackTable.stories.tsx b/packages/ui/stories/components/GridTrackTable.stories.tsx index 037f13b4c9..6a3cbbdf3c 100644 --- a/packages/ui/stories/components/GridTrackTable.stories.tsx +++ b/packages/ui/stories/components/GridTrackTable.stories.tsx @@ -4,6 +4,7 @@ import React, { useState } from 'react'; import { GridTrackTableProps } from '../../lib/components/GridTrackTable'; import { Track } from '../../lib/types'; import { swap } from '../storyUtils'; +import { Icon } from 'semantic-ui-react'; export default { title: 'Components/GridTrackTable', @@ -31,20 +32,31 @@ const tracks = [ thumbnail: 'https://i.imgur.com/4euOws2.jpg', artist: { name: 'Test Artist 3' }, name: 'Test Title 3', - album: 'Test Album', + album: 'Test Album2', duration: '1:00' } as Track ]; +const gridTrackTableStrings = { + addSelectedTracksToQueue: 'Add selected to queue', + addSelectedTracksToDownloads: 'Add selected to downloads', + addSelectedTracksToFavorites: 'Add selected to favorites', + playSelectedTracksNow: 'Play selected now', + tracksSelectedLabelSingular: 'track selected', + tracksSelectedLabelPlural: 'tracks selected', + filterInputPlaceholder: 'Search...' +}; const TrackTableTemplate = (args: Partial>) => } + thumbnailHeader={} artistHeader='Artist' albumHeader='Album' titleHeader='Title' durationHeader='Length' + isTrackFavorite={() => false} + strings={gridTrackTableStrings} {...args} />; diff --git a/packages/ui/stories/components/trackTable.stories.tsx b/packages/ui/stories/components/trackTable.stories.tsx index c42d510170..62bb7e96aa 100644 --- a/packages/ui/stories/components/trackTable.stories.tsx +++ b/packages/ui/stories/components/trackTable.stories.tsx @@ -180,3 +180,29 @@ export const ListeningHistory = () =>
displayDeleteButton={false} />
; + +export const DragAndDropVirtualized = () => { + const [trackRows, setTrackRows] = useState([...tracks]); + + return
+ ({ + ...trackRows[i % trackRows.length], + position: i + 1 + }))} + positionHeader={} + thumbnailHeader={} + + isTrackFavorite={(track: Track) => track.artist === 'Test Artist 2'} + playlists={playlists} + strings={trackTableStrings} + onDragEnd={(result) => { + const { source, destination } = result; + setTrackRows(swap(trackRows, source.index, destination!.index)); + }} + {...callbacks} + displayDeleteButton={false} + displayThumbnail={false} + /> +
; +}; From 7b8b200956fd48b02b4cfb5064a2983551e43816 Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Sat, 31 Aug 2024 01:40:25 +0200 Subject: [PATCH 03/32] Grid track table - complete functionality --- packages/ui/lib/components/Button/styles.scss | 10 +- .../GridTrackTable/Cells/DeleteCell.tsx | 27 ++++ .../GridTrackTable/Cells/FavoriteCell.tsx | 28 ++++ .../GridTrackTable/Cells/PositionCell.tsx | 2 +- .../GridTrackTable/Cells/TextCell.tsx | 2 +- .../GridTrackTable/Cells/ThumbnailCell.tsx | 2 +- .../GridTrackTable/Cells/TitleCell.tsx | 82 ++++++++++++ .../GridTrackTable/GridTrackTableRow.tsx | 2 +- .../GridTrackTable/GridTrackTableRowClone.tsx | 2 +- .../GridTrackTable/Headers/TextHeader.tsx | 6 +- .../lib/components/GridTrackTable/index.tsx | 74 +++++++++-- .../lib/components/GridTrackTable/styles.scss | 125 +++++++++++++++--- .../components/GridTrackTable.stories.tsx | 20 +++ 13 files changed, 342 insertions(+), 40 deletions(-) create mode 100644 packages/ui/lib/components/GridTrackTable/Cells/DeleteCell.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/Cells/FavoriteCell.tsx create mode 100644 packages/ui/lib/components/GridTrackTable/Cells/TitleCell.tsx 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 = () =>
+ +
; From 94e776b744b3e4f24e3e27480051abf2eb374f8e Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Thu, 5 Sep 2024 00:14:24 +0200 Subject: [PATCH 04/32] Use the new grid track table in place of existing playlists --- .../ArtistView/PopularTracks/index.tsx | 2 +- .../ListeningHistoryView/styles.scss | 4 + .../app/components/PlaylistView/styles.scss | 3 +- .../containers/TrackTableContainer/index.tsx | 4 +- .../lib/components/ContextPopup/styles.scss | 4 +- .../lib/components/GridTrackTable/index.tsx | 11 +- .../lib/components/GridTrackTable/styles.scss | 13 +- .../components/TrackTable/HistoryTable.tsx | 18 +- .../gridTrackTable.test.tsx.snap | 759 ++++++++++++++++++ packages/ui/test/gridTrackTable.test.tsx | 54 ++ packages/ui/test/helpers.tsx | 4 +- 11 files changed, 854 insertions(+), 22 deletions(-) create mode 100644 packages/ui/test/__snapshots__/gridTrackTable.test.tsx.snap create mode 100644 packages/ui/test/gridTrackTable.test.tsx diff --git a/packages/app/app/components/ArtistView/PopularTracks/index.tsx b/packages/app/app/components/ArtistView/PopularTracks/index.tsx index 044c490940..bf6adc2d8a 100644 --- a/packages/app/app/components/ArtistView/PopularTracks/index.tsx +++ b/packages/app/app/components/ArtistView/PopularTracks/index.tsx @@ -71,7 +71,7 @@ const PopularTracks: React.FC = ({ const customColumns = useMemo(() => [{ id: PlaycountColumnId, Header: t('count'), - accessor: (track: ArtistTopTrack) => track.playcount, + accessor: 'playcount', Cell: ({value, ...rest}: CellProps) => }], []); diff --git a/packages/app/app/components/ListeningHistoryView/styles.scss b/packages/app/app/components/ListeningHistoryView/styles.scss index 45e75f7183..6ad327dfbd 100644 --- a/packages/app/app/components/ListeningHistoryView/styles.scss +++ b/packages/app/app/components/ListeningHistoryView/styles.scss @@ -14,6 +14,10 @@ .listening_history_header_actions { display: flex; align-items: center; + + button.nuclear.ui.button:not(:last-child) { + margin-right: 1em; + } } .listening_history_pagination { diff --git a/packages/app/app/components/PlaylistView/styles.scss b/packages/app/app/components/PlaylistView/styles.scss index faa48d9e41..599bf975ec 100644 --- a/packages/app/app/components/PlaylistView/styles.scss +++ b/packages/app/app/components/PlaylistView/styles.scss @@ -3,12 +3,11 @@ .playlist_view_container { width: 100%; height: 100%; - margin: 1em 0em; .playlist { display: flex; flex-flow: column; - margin-right: 1em; + height: 100%; } .playlist_view_info { diff --git a/packages/app/app/containers/TrackTableContainer/index.tsx b/packages/app/app/containers/TrackTableContainer/index.tsx index 956282da4b..cdfacc2b07 100644 --- a/packages/app/app/containers/TrackTableContainer/index.tsx +++ b/packages/app/app/containers/TrackTableContainer/index.tsx @@ -5,7 +5,7 @@ import { Icon } from 'semantic-ui-react'; import _ from 'lodash'; import { Playlist } from '@nuclear/core'; -import { TrackTable, areTracksEqualByName } from '@nuclear/ui'; +import { GridTrackTable, areTracksEqualByName } from '@nuclear/ui'; import { TrackTableProps } from '@nuclear/ui/lib/components/TrackTable'; import { TrackTableSettings } from '@nuclear/ui/lib/components/TrackTable/types'; import { Track } from '@nuclear/ui/lib/types'; @@ -33,7 +33,7 @@ function TrackTableContainer ({ tracks, onDelete, onReorder, - TrackTableComponent = TrackTable, + TrackTableComponent = GridTrackTable, customColumns, displayAddToDownloads = true, displayAddToFavorites = true, diff --git a/packages/ui/lib/components/ContextPopup/styles.scss b/packages/ui/lib/components/ContextPopup/styles.scss index 975438f226..5ba65cdd1c 100644 --- a/packages/ui/lib/components/ContextPopup/styles.scss +++ b/packages/ui/lib/components/ContextPopup/styles.scss @@ -1,4 +1,4 @@ -@import '../../common'; +@import "../../common"; .context_popup { @include shadow; @@ -80,7 +80,7 @@ display: flex; flex-flow: column; flex: 0 0 auto; - padding: 0.5rem 0; + padding-top: 0.5rem; height: auto !important; } } diff --git a/packages/ui/lib/components/GridTrackTable/index.tsx b/packages/ui/lib/components/GridTrackTable/index.tsx index 6701f23dea..2a4545c4b0 100644 --- a/packages/ui/lib/components/GridTrackTable/index.tsx +++ b/packages/ui/lib/components/GridTrackTable/index.tsx @@ -32,7 +32,7 @@ export type GridTrackTableProps = { isTrackFavorite: (track: T) => boolean; onDragEnd?: DragDropContextProps['onDragEnd']; strings: TrackTableStrings; - customColumns?: (Column & { columnWidth: string; })[]; + customColumns?: (Column)[]; } & TrackTableHeaders & TrackTableSettings & TrackTableExtraProps; @@ -137,7 +137,8 @@ export const GridTrackTable = ({ return null; } }, - Cell: TextCell + Cell: TextCell, + columnWidth: '6em' }, ...customColumns, selectable && { @@ -146,12 +147,12 @@ export const GridTrackTable = ({ Cell: SelectionCell, columnWidth: '6em' } - ], [displayDeleteButton, displayPosition, displayThumbnail, displayFavorite, isTrackFavorite, titleHeader, displayArtist, artistHeader, displayAlbum, albumHeader, shouldDisplayDuration, durationHeader, selectable, positionHeader, thumbnailHeader]); + ].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.Title, desc: false }] + sortBy: [{ id: TrackTableColumn.Position, desc: false }] }; 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 @@ -225,7 +226,7 @@ export const GridTrackTable = ({
))}
- {}}> + ; +type HistoryTableProps = TrackTableProps & { + dateHeader?: string; +}; -const HistoryTable: React.FC = ({ tracks, ...props }) => { +const HistoryTable: React.FC = ({ tracks, dateHeader, ...props }) => { const customColumns = useMemo(() => [{ id: TrackTableColumn.Date, - Header: 'Date', + Header: ({ column }) => , accessor: (track: HistoryTableTrack) => track.createdAt.toLocaleString(), - Cell: DateCell - }], []); + Cell: DateCell, + columnnWidth: '3em' + } as Column], []); - return +
+
+
+
+
+   +
+
+
+ Position +
+
+
+
+ Thumbnail +
+
+
+   +
+
+
+ Title +