diff --git a/inc/Integrations/Shopify/ShopifyIntegration.php b/inc/Integrations/Shopify/ShopifyIntegration.php
index 994bda20..c02930be 100644
--- a/inc/Integrations/Shopify/ShopifyIntegration.php
+++ b/inc/Integrations/Shopify/ShopifyIntegration.php
@@ -116,7 +116,7 @@ public static function register_blocks_for_shopify_data_source( ShopifyDataSourc
'title' => $block_title,
'queries' => [
'display' => $queries['shopify_get_product'],
- 'list' => $queries['shopify_search_products'],
+ 'search' => $queries['shopify_search_products'],
],
'patterns' => [
[
diff --git a/src/blocks/remote-data-container/components/item-list/ItemList.tsx b/src/blocks/remote-data-container/components/item-list/ItemList.tsx
index b55b7d6f..45a6abdd 100644
--- a/src/blocks/remote-data-container/components/item-list/ItemList.tsx
+++ b/src/blocks/remote-data-container/components/item-list/ItemList.tsx
@@ -1,52 +1,158 @@
-import { Spinner } from '@wordpress/components';
+import { useInstanceId } from '@wordpress/compose';
+import { DataViews, filterSortAndPaginate, View } from '@wordpress/dataviews/wp';
+import { useEffect, useMemo, useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
-import { ItemPreview } from '@/blocks/remote-data-container/components/item-list/ItemPreview';
-import {
- cloneBlockWithAttributes,
- usePatterns,
-} from '@/blocks/remote-data-container/hooks/usePatterns';
-import { __ } from '@/utils/i18n';
+import { usePatterns } from '@/blocks/remote-data-container/hooks/usePatterns';
interface ItemListProps {
blockName: string;
loading: boolean;
- noResultsText: string;
onSelect: ( data: RemoteDataQueryInput ) => void;
- placeholderText: string;
results?: RemoteData[ 'results' ];
+ searchTerms: string;
+ setSearchTerms: ( newValue: string ) => void;
}
export function ItemList( props: ItemListProps ) {
- const { defaultPattern: pattern } = usePatterns( props.blockName );
+ const { blockName, loading, onSelect, results, searchTerms, setSearchTerms } = props;
+ const { defaultPattern: pattern } = usePatterns( blockName );
- if ( props.loading || ! pattern ) {
- return ;
- }
+ const instanceId = useInstanceId( ItemList, blockName );
- if ( ! props.results ) {
- return
{ __( props.placeholderText ) }
;
- }
+ // ensure each result has an 'id' key
+ const data = useMemo( () => {
+ return ( results ?? [] ).map( ( item: Record< string, unknown > ) =>
+ item.id
+ ? item
+ : {
+ ...item,
+ id: Object.keys( item ).find( key => /(^|_)(id)$/i.test( key ) ) // Regex to match 'id' or part of '_id'
+ ? item[ Object.keys( item ).find( key => /(^|_)(id)$/i.test( key ) ) as string ]
+ : instanceId,
+ }
+ ) as RemoteData[ 'results' ];
+ }, [ results ] );
- if ( props.results.length === 0 ) {
- return { __( props.noResultsText ) }
;
- }
+ // get fields from results data to use as columns
+ const { fields, mediaField, tableFields, titleField } = useMemo( () => {
+ const getFields: string[] = Array.from(
+ new Set(
+ data
+ ?.flatMap( item => Object.keys( item ) )
+ .filter( ( key: string ) => ! /(^|_)(id)$/i.test( key ) ) // Filters out keys containing 'id' or similar patterns
+ )
+ );
+
+ // generic search for title
+ const title: string =
+ getFields.find(
+ ( field: string ) =>
+ field.toLowerCase().includes( 'title' ) || field.toLowerCase().includes( 'name' )
+ ) || '';
+
+ // generic search for media
+ const media: string =
+ getFields.find(
+ ( field: string ) =>
+ field.toLowerCase().includes( 'url' ) || field.toLowerCase().includes( 'image' )
+ ) || '';
+
+ const fieldObject: {
+ id: string;
+ label: string;
+ enableGlobalSearch: boolean;
+ render?: ( { item }: { item: RemoteData[ 'results' ][ 0 ] } ) => JSX.Element;
+ enableSorting: boolean;
+ }[] = getFields.map( field => {
+ return {
+ id: field,
+ label: field ?? '',
+ enableGlobalSearch: true,
+ render:
+ field === media
+ ? ( { item }: { item: RemoteData[ 'results' ][ 0 ] } ) => {
+ return (
+
+ );
+ }
+ : undefined,
+ enableSorting: field !== media,
+ };
+ } );
+
+ return { fields: fieldObject, tableFields: getFields, titleField: title, mediaField: media };
+ }, [ data ] );
+
+ const [ view, setView ] = useState< View >( {
+ type: 'table' as const,
+ perPage: 8,
+ page: 1,
+ search: '',
+ fields: [],
+ filters: [],
+ layout: {},
+ titleField,
+ mediaField,
+ } );
+
+ const defaultLayouts = mediaField
+ ? {
+ table: {},
+ grid: {},
+ }
+ : { table: {} };
+
+ // this prevents just an empty table rendering
+ useEffect( () => {
+ if ( tableFields.length > 0 ) {
+ setView( prevView => ( {
+ ...prevView,
+ fields: tableFields.filter( field => field !== mediaField ),
+ } ) );
+ }
+ }, [ mediaField, tableFields ] );
+
+ useEffect( () => {
+ if ( view.search !== searchTerms ) {
+ setSearchTerms( view.search ?? '' );
+ }
+ }, [ view, searchTerms ] );
+
+ // filter, sort and paginate data
+ const { data: filteredData, paginationInfo } = useMemo( () => {
+ return filterSortAndPaginate( data ?? [], view, fields );
+ }, [ data, view ] );
+
+ const actions = [
+ {
+ id: 'choose',
+ icon: <>{ __( 'Choose' ) }>,
+ isPrimary: true,
+ label: '',
+ callback: ( items: RemoteData[ 'results' ] ) => {
+ items.map( item => onSelect( item ) );
+ },
+ },
+ ];
return (
-
- { props.results.map( ( result, index ) => {
- const blocks =
- pattern?.blocks.map( block =>
- cloneBlockWithAttributes( block, result, props.blockName )
- ) ?? [];
-
- return (
- props.onSelect( result ) }
- />
- );
- } ) }
-
+ item.id || '' }
+ isLoading={ loading || ! pattern || ! results || results.length === 0 }
+ isItemClickable={ () => true }
+ onClickItem={ item => onSelect( item ) }
+ onChangeView={ setView }
+ paginationInfo={ paginationInfo }
+ view={ view }
+ />
);
}
diff --git a/src/blocks/remote-data-container/components/modals/BaseModal.tsx b/src/blocks/remote-data-container/components/modals/BaseModal.tsx
index 02fb2a22..0ec73b13 100644
--- a/src/blocks/remote-data-container/components/modals/BaseModal.tsx
+++ b/src/blocks/remote-data-container/components/modals/BaseModal.tsx
@@ -4,6 +4,7 @@ import { __ } from '@/utils/i18n';
export interface BaseModalProps {
children: JSX.Element;
+ className?: string;
headerActions?: JSX.Element;
headerImage?: string;
onClose: () => void;
@@ -14,14 +15,14 @@ export interface BaseModalProps {
export function BaseModal( props: BaseModalProps ) {
return (
{ props.headerImage && (
) }
{ props.headerActions }
diff --git a/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx b/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx
new file mode 100644
index 00000000..b7db3646
--- /dev/null
+++ b/src/blocks/remote-data-container/components/modals/DataViewsModal.tsx
@@ -0,0 +1,57 @@
+import { __ } from '@wordpress/i18n';
+
+import { ModalWithButtonTrigger } from './BaseModal';
+import { useModalState } from '../../hooks/useModalState';
+import { ItemList } from '../item-list/ItemList';
+import { useSearchResults } from '@/blocks/remote-data-container/hooks/useSearchResults';
+import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks';
+import { getBlockDataSourceType } from '@/utils/localized-block-data';
+
+interface DataViewsModalProps {
+ blockName: string;
+ headerImage?: string;
+ onSelect: ( data: RemoteDataQueryInput ) => void;
+ queryKey: string;
+ title: string;
+}
+
+export const DataViewsModal: React.FC< DataViewsModalProps > = props => {
+ const { blockName, onSelect, queryKey, title } = props;
+
+ const { loading, results, searchTerms, setSearchTerms } = useSearchResults( {
+ blockName,
+ queryKey,
+ } );
+
+ const { close, isOpen, open } = useModalState();
+
+ function onSelectItem( data: RemoteDataQueryInput ): void {
+ onSelect( data );
+ sendTracksEvent( 'remotedatablocks_add_block', {
+ action: 'select_item',
+ selected_option: 'search_from_list',
+ data_source_type: getBlockDataSourceType( blockName ),
+ } );
+ close();
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/src/blocks/remote-data-container/components/modals/ItemListModal.tsx b/src/blocks/remote-data-container/components/modals/ItemListModal.tsx
deleted file mode 100644
index 1179e22d..00000000
--- a/src/blocks/remote-data-container/components/modals/ItemListModal.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { ItemList } from '@/blocks/remote-data-container/components/item-list/ItemList';
-import { ModalWithButtonTrigger } from '@/blocks/remote-data-container/components/modals/BaseModal';
-import { useModalState } from '@/blocks/remote-data-container/hooks/useModalState';
-import { __ } from '@/utils/i18n';
-
-export interface ItemListModalProps {
- blockName: string;
- buttonText: string;
- headerActions?: JSX.Element;
- headerImage?: string;
- loading: boolean;
- onOpen?: () => void;
- onSelect: ( data: RemoteDataQueryInput ) => void;
- results?: RemoteData[ 'results' ];
- title: string;
-}
-
-export function ItemListModal( props: ItemListModalProps ) {
- const { close, isOpen, open } = useModalState( props.onOpen );
-
- function wrappedOnSelect( data: RemoteDataQueryInput ): void {
- props.onSelect( data );
- close();
- }
-
- return (
-
-
-
- );
-}
diff --git a/src/blocks/remote-data-container/components/modals/ListModal.tsx b/src/blocks/remote-data-container/components/modals/ListModal.tsx
deleted file mode 100644
index 2c0461f9..00000000
--- a/src/blocks/remote-data-container/components/modals/ListModal.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { ItemListModal } from '@/blocks/remote-data-container/components/modals/ItemListModal';
-import { useRemoteData } from '@/blocks/remote-data-container/hooks/useRemoteData';
-
-interface ListModalProps {
- blockName: string;
- headerImage?: string;
- onSelect: ( data: RemoteDataQueryInput ) => void;
- queryKey: string;
- title: string;
-}
-
-export function ListModal( props: ListModalProps ) {
- const { blockName, onSelect, queryKey, title } = props;
-
- const { data, execute, loading } = useRemoteData( blockName, queryKey );
-
- return (
- void execute( {} ) }
- onSelect={ onSelect }
- results={ data?.results }
- title={ title }
- />
- );
-}
diff --git a/src/blocks/remote-data-container/components/modals/SearchModal.tsx b/src/blocks/remote-data-container/components/modals/SearchModal.tsx
deleted file mode 100644
index 66d4485f..00000000
--- a/src/blocks/remote-data-container/components/modals/SearchModal.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { SearchControl } from '@wordpress/components';
-
-import { ItemListModal } from '@/blocks/remote-data-container/components/modals/ItemListModal';
-import { useSearchResults } from '@/blocks/remote-data-container/hooks/useSearchResults';
-import { sendTracksEvent } from '@/blocks/remote-data-container/utils/tracks';
-import { getBlockDataSourceType } from '@/utils/localized-block-data';
-
-interface SearchModalProps {
- blockName: string;
- headerImage?: string;
- onSelect: ( data: RemoteDataQueryInput ) => void;
- queryKey: string;
- title: string;
-}
-
-export function SearchModal( props: SearchModalProps ) {
- const { blockName, onSelect, queryKey, title } = props;
-
- const { loading, onChange, onKeyDown, results, searchTerms } = useSearchResults( {
- blockName,
- queryKey,
- } );
-
- function onSelectItem( data: RemoteDataQueryInput ): void {
- onSelect( data );
- sendTracksEvent( 'remotedatablocks_add_block', {
- action: 'select_item',
- selected_option: 'search_from_list',
- data_source_type: getBlockDataSourceType( blockName ),
- } );
- }
-
- return (
-
- }
- headerImage={ props.headerImage }
- loading={ loading }
- onSelect={ onSelectItem }
- results={ results }
- title={ title }
- />
- );
-}
diff --git a/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx
index 39e9073d..ae31a187 100644
--- a/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx
+++ b/src/blocks/remote-data-container/components/placeholders/ItemSelectQueryType.tsx
@@ -1,8 +1,7 @@
import { ButtonGroup } from '@wordpress/components';
+import { DataViewsModal } from '@/blocks/remote-data-container/components/modals/DataViewsModal';
import { InputModal } from '@/blocks/remote-data-container/components/modals/InputModal';
-import { ListModal } from '@/blocks/remote-data-container/components/modals/ListModal';
-import { SearchModal } from '@/blocks/remote-data-container/components/modals/SearchModal';
interface ItemSelectQueryTypeProps {
blockConfig: BlockConfig;
@@ -29,9 +28,8 @@ export function ItemSelectQueryType( props: ItemSelectQueryTypeProps ) {
switch ( selector.type ) {
case 'search':
- return ;
case 'list':
- return ;
+ return ;
case 'input':
return ;
}
diff --git a/src/blocks/remote-data-container/editor.scss b/src/blocks/remote-data-container/editor.scss
index 39003b55..1a5b7503 100644
--- a/src/blocks/remote-data-container/editor.scss
+++ b/src/blocks/remote-data-container/editor.scss
@@ -2,6 +2,8 @@
* The following styles get applied inside the editor only.
*/
+@import url(@wordpress/dataviews/build-style/style.css);
+
.remote-data-blocks-loading-overlay {
align-items: center;
background-color: rgba(255, 255, 255, 0.25);
@@ -70,3 +72,56 @@ remote-data-blocks-inline-field {
vertical-align: bottom;
width: 20px;
}
+
+.dataviews-column-primary__media img {
+ width: 52px;
+ height: 52px;
+ object-fit: cover;
+ border-radius: 4px;
+ background-color: #f0f0f0;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
+}
+
+.dataviews-view-table tbody .dataviews-view-table__cell-content-wrapper {
+ max-width: 250px;
+ text-wrap: auto;
+}
+
+.dataviews-view-table__row .dataviews-view-table__actions-column .components-button.is-compact.has-icon:not(.has-text) {
+ opacity: 0;
+ display: inline-flex;
+ align-items: center;
+ background: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9));
+ color: var(--wp-components-color-accent-inverted, #fff);
+ height: 32px;
+ padding: 6px 12px;
+ border-radius: 2px;
+ width: auto;
+}
+
+.dataviews-view-table__row:hover .dataviews-view-table__actions-column .components-button.is-compact.has-icon:not(.has-text),
+.dataviews-view-table__row:focus-within .dataviews-view-table__actions-column .components-button.is-compact.has-icon:not(.has-text) {
+ opacity: 1;
+
+}
+
+.dataviews-view-table tbody td {
+ vertical-align: middle;
+}
+
+.dataviews-view-table tr td:last-child,
+.dataviews-view-table tr th:last-child {
+ padding-left: 48px;
+}
+
+.rdb-editor_data-views-modal .components-modal__content {
+ padding: 0;
+
+ > :nth-child(2) {
+ height: 100%;
+ }
+
+ .dataviews-view-table {
+ padding: 4px 32px 32px;
+ }
+}
diff --git a/src/blocks/remote-data-container/hooks/useSearchResults.ts b/src/blocks/remote-data-container/hooks/useSearchResults.ts
index ca7dabdd..707da7e0 100644
--- a/src/blocks/remote-data-container/hooks/useSearchResults.ts
+++ b/src/blocks/remote-data-container/hooks/useSearchResults.ts
@@ -18,10 +18,6 @@ export function useSearchResults( {
const { data, execute, loading } = useRemoteData( blockName, queryKey );
const timer = useRef< NodeJS.Timeout >();
- function onChange( newValue: string ): void {
- setSearchTerms( newValue );
- }
-
function onSubmit(): void {
void execute( { search_terms: searchTerms } );
}
@@ -48,9 +44,9 @@ export function useSearchResults( {
return {
loading,
- onChange,
onKeyDown,
results: data?.results,
searchTerms,
+ setSearchTerms,
};
}