Skip to content

Commit

Permalink
[UI] Implement DataViews in editor (#251)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
brookewp and maxschmeling authored Dec 20, 2024
1 parent c61e82c commit 0b64945
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 175 deletions.
2 changes: 1 addition & 1 deletion inc/Integrations/Shopify/ShopifyIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => [
[
Expand Down
176 changes: 141 additions & 35 deletions src/blocks/remote-data-container/components/item-list/ItemList.tsx
Original file line number Diff line number Diff line change
@@ -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 <Spinner />;
}
const instanceId = useInstanceId( ItemList, blockName );

if ( ! props.results ) {
return <p>{ __( props.placeholderText ) }</p>;
}
// 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 <p>{ __( props.noResultsText ) }</p>;
}
// 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 (
<img
// temporary until we pull in more data
alt=""
src={ item[ field ] as string }
/>
);
}
: 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 (
<ul>
{ props.results.map( ( result, index ) => {
const blocks =
pattern?.blocks.map( block =>
cloneBlockWithAttributes( block, result, props.blockName )
) ?? [];

return (
<ItemPreview
key={ index }
blocks={ blocks }
onSelect={ () => props.onSelect( result ) }
/>
);
} ) }
</ul>
<DataViews
actions={ actions }
data={ filteredData }
defaultLayouts={ defaultLayouts }
fields={ fields }
getItemId={ ( item: { id?: string } ) => item.id || '' }
isLoading={ loading || ! pattern || ! results || results.length === 0 }
isItemClickable={ () => true }
onClickItem={ item => onSelect( item ) }
onChangeView={ setView }
paginationInfo={ paginationInfo }
view={ view }
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { __ } from '@/utils/i18n';

export interface BaseModalProps {
children: JSX.Element;
className?: string;
headerActions?: JSX.Element;
headerImage?: string;
onClose: () => void;
Expand All @@ -14,14 +15,14 @@ export interface BaseModalProps {
export function BaseModal( props: BaseModalProps ) {
return (
<Modal
className="remote-data-blocks-modal"
className={ `${ props.className } remote-data-blocks-modal` }
headerActions={
<>
{ props.headerImage && (
<img
alt={ props.title }
src={ props.headerImage }
style={ { height: '90%', marginRight: '2em', objectFit: 'contain' } }
style={ { marginRight: '2em', objectFit: 'contain' } }
/>
) }
{ props.headerActions }
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<ModalWithButtonTrigger
buttonText={ __( 'Choose' ) }
className="rdb-editor_data-views-modal"
isOpen={ isOpen }
onClose={ close }
onOpen={ open }
title={ title }
>
<ItemList
blockName={ props.blockName }
loading={ loading }
onSelect={ onSelectItem }
results={ results }
searchTerms={ searchTerms }
setSearchTerms={ setSearchTerms }
/>
</ModalWithButtonTrigger>
);
};

This file was deleted.

29 changes: 0 additions & 29 deletions src/blocks/remote-data-container/components/modals/ListModal.tsx

This file was deleted.

Loading

0 comments on commit 0b64945

Please sign in to comment.