Skip to content

Commit 0b64945

Browse files
[UI] Implement DataViews in editor (#251)
* Add DataViews to rdb editor Signed-off-by: brookewp <[email protected]> * Add filters and type for shopify demo Signed-off-by: brookewp <[email protected]> * Temp add images Signed-off-by: brookewp <[email protected]> * Combine list and search modal Signed-off-by: brookewp <[email protected]> * Clean up and show media with title when present Signed-off-by: brookewp <[email protected]> * Move logic for filtering out IDs so they remain searchable Signed-off-by: brookewp <[email protected]> * Fix no results from search Signed-off-by: brookewp <[email protected]> * Add custom button and remove list view for now Signed-off-by: brookewp <[email protected]> * Set pagination to bottom of modal Signed-off-by: brookewp <[email protected]> * Consolidate and remove filters temporarily Signed-off-by: brookewp <[email protected]> * Update src/blocks/remote-data-container/components/modals/DataViewsModal.tsx Co-authored-by: Max Schmeling <[email protected]> * Update to show search title Signed-off-by: brookewp <[email protected]> * lint fix Signed-off-by: brookewp <[email protected]> --------- Signed-off-by: brookewp <[email protected]> Co-authored-by: Max Schmeling <[email protected]>
1 parent c61e82c commit 0b64945

File tree

10 files changed

+260
-175
lines changed

10 files changed

+260
-175
lines changed

inc/Integrations/Shopify/ShopifyIntegration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public static function register_blocks_for_shopify_data_source( ShopifyDataSourc
116116
'title' => $block_title,
117117
'queries' => [
118118
'display' => $queries['shopify_get_product'],
119-
'list' => $queries['shopify_search_products'],
119+
'search' => $queries['shopify_search_products'],
120120
],
121121
'patterns' => [
122122
[
Lines changed: 141 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,158 @@
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';
25

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';
97

108
interface ItemListProps {
119
blockName: string;
1210
loading: boolean;
13-
noResultsText: string;
1411
onSelect: ( data: RemoteDataQueryInput ) => void;
15-
placeholderText: string;
1612
results?: RemoteData[ 'results' ];
13+
searchTerms: string;
14+
setSearchTerms: ( newValue: string ) => void;
1715
}
1816

1917
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 );
2120

22-
if ( props.loading || ! pattern ) {
23-
return <Spinner />;
24-
}
21+
const instanceId = useInstanceId( ItemList, blockName );
2522

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 ] );
2936

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+
];
33142

34143
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+
/>
51157
);
52158
}

src/blocks/remote-data-container/components/modals/BaseModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { __ } from '@/utils/i18n';
44

55
export interface BaseModalProps {
66
children: JSX.Element;
7+
className?: string;
78
headerActions?: JSX.Element;
89
headerImage?: string;
910
onClose: () => void;
@@ -14,14 +15,14 @@ export interface BaseModalProps {
1415
export function BaseModal( props: BaseModalProps ) {
1516
return (
1617
<Modal
17-
className="remote-data-blocks-modal"
18+
className={ `${ props.className } remote-data-blocks-modal` }
1819
headerActions={
1920
<>
2021
{ props.headerImage && (
2122
<img
2223
alt={ props.title }
2324
src={ props.headerImage }
24-
style={ { height: '90%', marginRight: '2em', objectFit: 'contain' } }
25+
style={ { marginRight: '2em', objectFit: 'contain' } }
2526
/>
2627
) }
2728
{ props.headerActions }
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { __ } from '@wordpress/i18n';
2+
3+
import { ModalWithButtonTrigger } from './BaseModal';
4+
import { useModalState } from '../../hooks/useModalState';
5+
import { ItemList } from '../item-list/ItemList';
6+
import { useSearchResults } from '@/blocks/remote-data-container/hooks/useSearchResults';
7+
import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks';
8+
import { getBlockDataSourceType } from '@/utils/localized-block-data';
9+
10+
interface DataViewsModalProps {
11+
blockName: string;
12+
headerImage?: string;
13+
onSelect: ( data: RemoteDataQueryInput ) => void;
14+
queryKey: string;
15+
title: string;
16+
}
17+
18+
export const DataViewsModal: React.FC< DataViewsModalProps > = props => {
19+
const { blockName, onSelect, queryKey, title } = props;
20+
21+
const { loading, results, searchTerms, setSearchTerms } = useSearchResults( {
22+
blockName,
23+
queryKey,
24+
} );
25+
26+
const { close, isOpen, open } = useModalState();
27+
28+
function onSelectItem( data: RemoteDataQueryInput ): void {
29+
onSelect( data );
30+
sendTracksEvent( 'remotedatablocks_add_block', {
31+
action: 'select_item',
32+
selected_option: 'search_from_list',
33+
data_source_type: getBlockDataSourceType( blockName ),
34+
} );
35+
close();
36+
}
37+
38+
return (
39+
<ModalWithButtonTrigger
40+
buttonText={ __( 'Choose' ) }
41+
className="rdb-editor_data-views-modal"
42+
isOpen={ isOpen }
43+
onClose={ close }
44+
onOpen={ open }
45+
title={ title }
46+
>
47+
<ItemList
48+
blockName={ props.blockName }
49+
loading={ loading }
50+
onSelect={ onSelectItem }
51+
results={ results }
52+
searchTerms={ searchTerms }
53+
setSearchTerms={ setSearchTerms }
54+
/>
55+
</ModalWithButtonTrigger>
56+
);
57+
};

src/blocks/remote-data-container/components/modals/ItemListModal.tsx

Lines changed: 0 additions & 46 deletions
This file was deleted.

src/blocks/remote-data-container/components/modals/ListModal.tsx

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)