|
1 |
| -import { Spinner } from '@wordpress/components'; |
| 1 | +import { useInstanceId } from '@wordpress/compose'; |
| 2 | +import { DataViews, filterSortAndPaginate, View } from '@wordpress/dataviews/wp'; |
| 3 | +import { useEffect, useMemo, useState } from '@wordpress/element'; |
| 4 | +import { __ } from '@wordpress/i18n'; |
2 | 5 |
|
3 |
| -import { ItemPreview } from '@/blocks/remote-data-container/components/item-list/ItemPreview'; |
4 |
| -import { |
5 |
| - cloneBlockWithAttributes, |
6 |
| - usePatterns, |
7 |
| -} from '@/blocks/remote-data-container/hooks/usePatterns'; |
8 |
| -import { __ } from '@/utils/i18n'; |
| 6 | +import { usePatterns } from '@/blocks/remote-data-container/hooks/usePatterns'; |
9 | 7 |
|
10 | 8 | interface ItemListProps {
|
11 | 9 | blockName: string;
|
12 | 10 | loading: boolean;
|
13 |
| - noResultsText: string; |
14 | 11 | onSelect: ( data: RemoteDataQueryInput ) => void;
|
15 |
| - placeholderText: string; |
16 | 12 | results?: RemoteData[ 'results' ];
|
| 13 | + searchTerms: string; |
| 14 | + setSearchTerms: ( newValue: string ) => void; |
17 | 15 | }
|
18 | 16 |
|
19 | 17 | export function ItemList( props: ItemListProps ) {
|
20 |
| - const { defaultPattern: pattern } = usePatterns( props.blockName ); |
| 18 | + const { blockName, loading, onSelect, results, searchTerms, setSearchTerms } = props; |
| 19 | + const { defaultPattern: pattern } = usePatterns( blockName ); |
21 | 20 |
|
22 |
| - if ( props.loading || ! pattern ) { |
23 |
| - return <Spinner />; |
24 |
| - } |
| 21 | + const instanceId = useInstanceId( ItemList, blockName ); |
25 | 22 |
|
26 |
| - if ( ! props.results ) { |
27 |
| - return <p>{ __( props.placeholderText ) }</p>; |
28 |
| - } |
| 23 | + // ensure each result has an 'id' key |
| 24 | + const data = useMemo( () => { |
| 25 | + return ( results ?? [] ).map( ( item: Record< string, unknown > ) => |
| 26 | + item.id |
| 27 | + ? item |
| 28 | + : { |
| 29 | + ...item, |
| 30 | + id: Object.keys( item ).find( key => /(^|_)(id)$/i.test( key ) ) // Regex to match 'id' or part of '_id' |
| 31 | + ? item[ Object.keys( item ).find( key => /(^|_)(id)$/i.test( key ) ) as string ] |
| 32 | + : instanceId, |
| 33 | + } |
| 34 | + ) as RemoteData[ 'results' ]; |
| 35 | + }, [ results ] ); |
29 | 36 |
|
30 |
| - if ( props.results.length === 0 ) { |
31 |
| - return <p>{ __( props.noResultsText ) }</p>; |
32 |
| - } |
| 37 | + // get fields from results data to use as columns |
| 38 | + const { fields, mediaField, tableFields, titleField } = useMemo( () => { |
| 39 | + const getFields: string[] = Array.from( |
| 40 | + new Set( |
| 41 | + data |
| 42 | + ?.flatMap( item => Object.keys( item ) ) |
| 43 | + .filter( ( key: string ) => ! /(^|_)(id)$/i.test( key ) ) // Filters out keys containing 'id' or similar patterns |
| 44 | + ) |
| 45 | + ); |
| 46 | + |
| 47 | + // generic search for title |
| 48 | + const title: string = |
| 49 | + getFields.find( |
| 50 | + ( field: string ) => |
| 51 | + field.toLowerCase().includes( 'title' ) || field.toLowerCase().includes( 'name' ) |
| 52 | + ) || ''; |
| 53 | + |
| 54 | + // generic search for media |
| 55 | + const media: string = |
| 56 | + getFields.find( |
| 57 | + ( field: string ) => |
| 58 | + field.toLowerCase().includes( 'url' ) || field.toLowerCase().includes( 'image' ) |
| 59 | + ) || ''; |
| 60 | + |
| 61 | + const fieldObject: { |
| 62 | + id: string; |
| 63 | + label: string; |
| 64 | + enableGlobalSearch: boolean; |
| 65 | + render?: ( { item }: { item: RemoteData[ 'results' ][ 0 ] } ) => JSX.Element; |
| 66 | + enableSorting: boolean; |
| 67 | + }[] = getFields.map( field => { |
| 68 | + return { |
| 69 | + id: field, |
| 70 | + label: field ?? '', |
| 71 | + enableGlobalSearch: true, |
| 72 | + render: |
| 73 | + field === media |
| 74 | + ? ( { item }: { item: RemoteData[ 'results' ][ 0 ] } ) => { |
| 75 | + return ( |
| 76 | + <img |
| 77 | + // temporary until we pull in more data |
| 78 | + alt="" |
| 79 | + src={ item[ field ] as string } |
| 80 | + /> |
| 81 | + ); |
| 82 | + } |
| 83 | + : undefined, |
| 84 | + enableSorting: field !== media, |
| 85 | + }; |
| 86 | + } ); |
| 87 | + |
| 88 | + return { fields: fieldObject, tableFields: getFields, titleField: title, mediaField: media }; |
| 89 | + }, [ data ] ); |
| 90 | + |
| 91 | + const [ view, setView ] = useState< View >( { |
| 92 | + type: 'table' as const, |
| 93 | + perPage: 8, |
| 94 | + page: 1, |
| 95 | + search: '', |
| 96 | + fields: [], |
| 97 | + filters: [], |
| 98 | + layout: {}, |
| 99 | + titleField, |
| 100 | + mediaField, |
| 101 | + } ); |
| 102 | + |
| 103 | + const defaultLayouts = mediaField |
| 104 | + ? { |
| 105 | + table: {}, |
| 106 | + grid: {}, |
| 107 | + } |
| 108 | + : { table: {} }; |
| 109 | + |
| 110 | + // this prevents just an empty table rendering |
| 111 | + useEffect( () => { |
| 112 | + if ( tableFields.length > 0 ) { |
| 113 | + setView( prevView => ( { |
| 114 | + ...prevView, |
| 115 | + fields: tableFields.filter( field => field !== mediaField ), |
| 116 | + } ) ); |
| 117 | + } |
| 118 | + }, [ mediaField, tableFields ] ); |
| 119 | + |
| 120 | + useEffect( () => { |
| 121 | + if ( view.search !== searchTerms ) { |
| 122 | + setSearchTerms( view.search ?? '' ); |
| 123 | + } |
| 124 | + }, [ view, searchTerms ] ); |
| 125 | + |
| 126 | + // filter, sort and paginate data |
| 127 | + const { data: filteredData, paginationInfo } = useMemo( () => { |
| 128 | + return filterSortAndPaginate( data ?? [], view, fields ); |
| 129 | + }, [ data, view ] ); |
| 130 | + |
| 131 | + const actions = [ |
| 132 | + { |
| 133 | + id: 'choose', |
| 134 | + icon: <>{ __( 'Choose' ) }</>, |
| 135 | + isPrimary: true, |
| 136 | + label: '', |
| 137 | + callback: ( items: RemoteData[ 'results' ] ) => { |
| 138 | + items.map( item => onSelect( item ) ); |
| 139 | + }, |
| 140 | + }, |
| 141 | + ]; |
33 | 142 |
|
34 | 143 | return (
|
35 |
| - <ul> |
36 |
| - { props.results.map( ( result, index ) => { |
37 |
| - const blocks = |
38 |
| - pattern?.blocks.map( block => |
39 |
| - cloneBlockWithAttributes( block, result, props.blockName ) |
40 |
| - ) ?? []; |
41 |
| - |
42 |
| - return ( |
43 |
| - <ItemPreview |
44 |
| - key={ index } |
45 |
| - blocks={ blocks } |
46 |
| - onSelect={ () => props.onSelect( result ) } |
47 |
| - /> |
48 |
| - ); |
49 |
| - } ) } |
50 |
| - </ul> |
| 144 | + <DataViews |
| 145 | + actions={ actions } |
| 146 | + data={ filteredData } |
| 147 | + defaultLayouts={ defaultLayouts } |
| 148 | + fields={ fields } |
| 149 | + getItemId={ ( item: { id?: string } ) => item.id || '' } |
| 150 | + isLoading={ loading || ! pattern || ! results || results.length === 0 } |
| 151 | + isItemClickable={ () => true } |
| 152 | + onClickItem={ item => onSelect( item ) } |
| 153 | + onChangeView={ setView } |
| 154 | + paginationInfo={ paginationInfo } |
| 155 | + view={ view } |
| 156 | + /> |
51 | 157 | );
|
52 | 158 | }
|
0 commit comments