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 ( - + 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, }; }