始める前に
この記事では、前の記事で始めたプロジェクトを基に、前回の続きから進めていきます。
この記事の出発点となるプロジェクトを取得するには、次の手順を実行できます:
git clone git@github.com:wptrainingteam/devblog-dataviews-plugin.git
git checkout part1
この記事で説明されているプロジェクトの最終コードは、GitHubで入手可能です。
記事全体を通して、説明されている変更に対応する特定のコミットへのリンクが見つかります。これにより、プロジェクトの進捗を追跡するのに役立ちます。
メディアライブラリに画像を追加するアクション
現在の状態では、プロジェクトは画像をフルサイズで表示するアクションのみを持っています。次に、メディアライブラリに画像をアップロードする新しいアクションを作成しましょう。
次のコードをactions
配列に追加し、actions
プロパティに渡します。Dataviews
コンポーネント:
src/App.jsconst actions = [
{
id: 'upload-media',
label: __( 'Upload Media' ),
isPrimary: true,
icon: 'upload',
supportsBulk: true,
callback: ( images ) => {
images.forEach( ( image ) => {
console.log( `Image to upload: ${ image.slug }` );
});
},
},
...
]
この新しいアクションは、データビューUIでのマルチ選択を可能にします。現時点では、トリガーされると、選択された各画像に対してコンソールにメッセージを記録します。
ファイルが変更されたときにプロジェクトのビルドバージョンを自動的に生成するために、npm start
(npm run start
のエイリアス)が実行されていることを確認してください。
‘see-original’アクションは、id
、label
、callback
プロパティを使用して定義されました。上記のように、この新しい‘upload-media’アクションの定義では、次のような追加のプロパティが使用されています:
isPrimary
– このプロパティをtrue
に設定することで、このアクションは各アイテムのデフォルトアクションになります。supportsBulk
– このプロパティをtrue
に設定することで、アイテムのマルチ選択を有効にできます。この設定を有効にすると、複数のアイテムに対して同時にアクションを実行できます。icon
– プライマリアクション用のアイコン。
このアクションのコールバック関数が、現在1つ以上のアイテムの配列を受け取っていることに注意してください。forEach
を使用することで、選択された各アイテムに対していくつかのロジックを実行できます。
データビューアイテムのアクションを定義するために使用できるプロパティの完全なリストは、こちらで確認してください。
‘upload-media’アクションが作成されたので、URLを指定してメディアライブラリに画像をアップロードするために必要な手順を考えてみましょう:
- 1. 提供されたURLから画像をダウンロードし、blobに変換します。
- 2. 画像blobを含むFormDataオブジェクトを作成し、POSTリクエストのボディとして送信します。
- 3.
apiFetch
を使用して、WordPress REST APIの適切なエンドポイントに画像をメディアライブラリに追加するためのPOSTリクエストを送信します。
基本的に、特定のREST API URLにPOSTリクエストを行い、バイナリ形式の画像データを含める必要があります。これを行うには、REST APIリクエストのデータを準備するためにいくつかの前提条件が必要です。
これらの手順をコメントとして追加し、各ステップのロジックを追加する際の参照として使用します:
src/App.jsconst actions = [
{
id: 'upload-media',
...
callback: ( images ) => {
images.forEach( ( image ) => {
// 1- Download the image and convert it to a blob
// 2- Create FormData with the image blob
// 3- Send the request to the WP REST API
});
},
},
...
]
画像をダウンロードしてblobに変換する
各アイテムの画像URLはimage.urls.raw
で利用可能ですので、ブラウザのネイティブfetch
メソッドを使用して各画像をダウンロードし、レスポンスをblobに変換できます。
POSTリクエストで画像を送信するには、ネットワークを介して送信できる形式に変換する必要があります。一般的な形式はバイナリ大オブジェクト(Blob)です。
このコードを追加して、forEach
のコールバック関数に:
src/App.jsimages.forEach( async ( image ) => {
// 1- Download the image and convert it to a blob
const responseRequestImage = await fetch( image.urls.raw );
const blobImage = await responseRequestImage.blob();
...
})
`````fetch`````からの成功したレスポンスは、[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)インスタンスにカプセル化されており、[`````blob()`````メソッド](https://developer.mozilla.org/en-US/docs/Web/API/Response/blob)が含まれています。このメソッドを使用すると、レスポンスから直接blobを生成できます。
<a name="create-formdata-with-the-image-blob"></a>
### 画像blobでFormDataを作成する
画像がblob形式になったら、次のステップはREST APIへのHTTPリクエストで送信するデータを準備することです。POSTリクエストでデータを送信する方法の1つは、[`````FormData`````オブジェクト](https://developer.mozilla.org/en-US/docs/Web/API/FormData)をリクエストボディとして使用することです。
`````fetch`````(またはWordPressの`````apiFetch`````)のようなメソッドは、リクエストのボディとして`````FormData`````オブジェクトを使用できます。このデータは、`````Content-Type`````が`````multipart/form-data`````に設定されてエンコードされ、送信されます。
[このコードを追加](https://github.com/wptrainingteam/devblog-dataviews-plugin/commit/273658720dbf832eddcf4a0db8a4542e4a315767)して、`````forEach`````のコールバック関数に:
``````bash
src/App.jsimages.forEach( async ( image ) => {
...
// 2- Create FormData with the image blob
const formDataWithImage = new FormData();
formDataWithImage.append(
'file',
blobImage,
);
...
})
`
上記のコードは、新しいFormData
オブジェクトを作成し、blob形式の画像を含むfile
フィールドを追加します。
<a name="send-the-request-to-the-wp-rest-api"></a>
### WP REST APIにリクエストを送信する
この時点で、すべてが`````apiFetch`````メソッドを使用してREST APIリクエストを行う準備が整いました。
`````apiFetch`````は、`````window.fetch`````のラッパーであり、WP REST APIへのリクエストを行う際に、REST APIエンドポイントのベースURLを自動的に補完し、[`````nonce`````をリクエストヘッダーに含める](/read/wordpress/c78da111cf4e6cb4.md)など、いくつかの利点を提供します。
`````App.js`````の先頭に移動し、`````apiFetch`````メソッドを`````@wordpress/api-fetch`````からインポートします:
``````bash
src/App.jsimport apiFetch from "@wordpress/api-fetch";
`
次に、このコードを追加して、forEach
のコールバック関数に:
src/App.jsimages.forEach( async ( image ) => {
...
// 3- Send the request to the WP REST API with apiFetch
await apiFetch({
path: "/wp/v2/media",
method: "POST",
body: formDataWithImage,
})
.then( console.log )
...
})
上記のコードは、apiFetch
を使用してwp/v2/media
エンドポイントに(POST)リクエストを行います。REST APIハンドブックに記載されているように、/wp/v2/media
エンドポイントへのPOSTリクエストを介して、WordPressインストールにメディアアイテムを作成できます。
パブリックREST APIは、WP 4.7以降コアの一部です。
あなたのデータビューには、表示された写真をメディアライブラリに直接アップロードする新しい機能が含まれています。個別に画像をアップロードすることも、複数の画像を選択して一括アップロードすることもできます。
ただし、ユーザーがアップロードプロセスについて受け取るフィードバックはあまり良くなく、コンソールにメッセージが表示されるだけです。このユーザーエクスペリエンスを改善するために取り組みましょう。
アップロードプロセスの結果を通知するボックス
WordPressは、通知システムを提供しており、通知ストアを介して管理できます。通知UIと対話する良いアプローチは、withNotices
ハイアオーダーコンポーネントでReactコンポーネントをラップすることです。
- `````noticeOperations`````は、特定の通知ボックスを生成するために使用できる[`````createNotice`````](a594ec44f1731fc8.md#createnotice)メソッド(他のメソッドも含む)を含むオブジェクトです。
- `````noticeUI`````は、通知ボックスが作成されたときに表示される通知Reactコンポーネントです。
`````App.js`````の先頭に移動し、[`````withNotices`````メソッドを`````"@wordpress/components"`````からインポート](https://github.com/wptrainingteam/devblog-dataviews-plugin/commit/0e0059f6dbe5361d383a7adfa6243426fd8dabe1#diff-3d74dddefb6e35fbffe3c76ec0712d5c416352d9449e2fcc8210a9dee57dff67R4)します:
``````bash
src/App.jsimport { withNotices } from "@wordpress/components";
`
``````bash
src/App.jsconst App = withNotices(({ noticeOperations, noticeUI }) => {
const { createNotice } = noticeOperations;
...
});
`
createNotice
メソッドをnoticeOperations
から分解して使用して、Reactアプリの画面上にnoticeUI
コンポーネントを配置する場所に通知ボックスを作成できます。これらの通知ボックスは、メディアライブラリにアップロードされた画像の成功または失敗についてユーザーに情報を提供するために使用できます。
次のthen
およびcatch
メソッドをapiFetch
呼び出しにチェーンして、APIリクエストの成功またはエラーを処理します。まだ定義されていないonSuccessMediaUpload
およびonErrorMediaUpload
関数を使用します:
src/App.js// 3- Send the request to the WP REST API with apiFetch
await apiFetch({
path: "/wp/v2/media",
method: "POST",
body: formDataWithImage,
})
.then(onSuccessMediaUpload)
.catch(onErrorMediaUpload);
次に、次のコードをAppコンポーネントに追加します(これらの関数の呼び出しの前に)onSuccessMediaUpload
およびonErrorMediaUpload
関数の定義を含めます:
src/App.jsconst onSuccessMediaUpload = (oImageUploaded) => {
const title = oImageUploaded.title.rendered;
createNotice({
status: "success",
content: __(`${title}.jpg successfully uploaded to Media Library!`),
isDismissible: true,
});
};
const onErrorMediaUpload = (error) => {
console.log(error);
createNotice({
status: "error",
content: __("An error occurred!"),
isDismissible: true,
});
};
アップロード操作が失敗した場合、`````onErrorMediaUpload`````は、アップロード操作の失敗を引き起こした特定のエラーを受け取ります。この関数内でも、何かがうまくいかなかったことをユーザーに通知するために`````error`````ボックスを呼び出すことができます。
`````createNotice`````メソッドによってトリガーされた通知ボックスを表示するには、[`````noticeUI`````](https://github.com/wptrainingteam/devblog-dataviews-plugin/commit/0e0059f6dbe5361d383a7adfa6243426fd8dabe1#diff-3d74dddefb6e35fbffe3c76ec0712d5c416352d9449e2fcc8210a9dee57dff67R186)コンポーネントを`````App`````コンポーネントのreturnに追加します:
``````bash
src/App,jsreturn (
<>
{noticeUI}
<DataViews
...
/>
</>
);
`
Reactコンポーネントは1つの親要素しか返せないため、Fragment
コンポーネントを使用して要素をグループ化できます。
通知ボックスは画面の上部に表示されるため、次のコードを追加して、‘upload-media’コールバックの開始時に毎回このアクションがトリガーされるたびにスクロールして、ユーザーが通知を見逃さないようにします:
src/App.jswindow.scrollTo( 0, 0 );
アップロードプロセスの進行状況を示すスピナー
追加できる良いUI要素は、アップロードプロセスの進行状況を示すインジケーターです。WordPressコンポーネントのSpinner
コンポーネントは、これに最適です。
ただし、Spinner
コンポーネントを使用する前に、アップロードプロセスが実行されている間のみ表示されるようにするためのロジックを追加する必要があります。進行中のアップロードプロセスを監視するには、進行中のすべてのアップロードの配列を保持する状態変数を使用できます。
状態変数を使用してアップロードプロセスを追跡する
Appコンポーネントの先頭に次のコードを追加して、useState
を使用して状態変数を追加します:
src/App.jsconst [isUploadingItems, setIsUploadingItems] = useState([]);
状態変数が更新されるたびに、Reactコンポーネントが再レンダリングされます。
[次のコード](https://github.com/wptrainingteam/devblog-dataviews-plugin/commit/c046c5808e704409f9d717311abed4cac8f0af0b#diff-3d74dddefb6e35fbffe3c76ec0712d5c416352d9449e2fcc8210a9dee57dff67R161)を`````forEach`````コールバックの最初に追加します(‘upload-media’アクションのコールバック内):
``````bash
src/App.jssetIsUploadingItems((prevIsUploadingItems) => [
...prevIsUploadingItems,
image.slug,
]);
`
上記のコードは、アップロードのために選択されている各画像のスラグをisUploadingItems
状態変数配列に追加します。
画像が正常にアップロードされたときにisUploadingItems
状態変数配列から画像のスラグを削除するか、エラーが発生したときに完全に空にするには、https://github.com/wptrainingteam/devblog-dataviews-plugin/commit/c046c5808e704409f9d717311abed4cac8f0af0b#diff-3d74dddefb6e35fbffe3c76ec0712d5c416352d9449e2fcc8210a9dee57dff67R124をonSuccessMediaUpload
およびonErrorMediaUpload
関数に追加します。
src/App.jsconst onSuccessMediaUpload = (oImageUploaded) => {
...
setIsUploadingItems((prevIsUploadingItems) =>
prevIsUploadingItems.filter((slugLoading) => slugLoading !== title)
);
...
};
const onErrorMediaUpload = (error) => {
setIsUploadingItems([]);
...
};
スピナーコンポーネントの表示
App.jsの先頭に移動し、[`````Spinner`````コンポーネント](https://github.com/wptrainingteam/devblog-dataviews-plugin/commit/c046c5808e704409f9d717311abed4cac8f0af0b#diff-3d74dddefb6e35fbffe3c76ec0712d5c416352d9449e2fcc8210a9dee57dff67R4)を`````"@wordpress/components"`````からインポートします:
``````bash
src/App.jsimport { Spinner } from "@wordpress/components";
`
最後に、画像のアップロードがまだ進行中である間にスピナーコンポーネントを条件付きで表示するために、次のコードをAppコンポーネントのreturn文に追加します:
src/Ap.jsreturn (
<>
{!!isUploadingItems.length && <Spinner />}
...
<DataViews
...
/>
</>
);
モーダルウィンドウを使用したアクション
前回の記事で作成したアクションを思い出してください。画像をフルサイズで表示するためのものです。このアクションを改善して、ユーザーが新しいウィンドウで開く画像のサイズを選択できるようにモーダルを表示することはどうでしょうか?
データビューアクションは、トリガーされたときにモーダルウィンドウを表示するメカニズムを提供します。RenderModal
プロパティを介して、アクション呼び出し時にモーダルウィンドウとして表示されるReactコンポーネントを定義できます。RenderModal
プロパティが提供されると、callback
プロパティは無視されます。
‘see-original’アクションの定義を次のコードに置き換えることができます:
``````bash
src/App.js{
id: 'see-original',
label: __( 'See Original' ),
modalHeader: __( 'See Original Image', 'action label' ),
RenderModal: ( { items: [ item ], closeModal } ) => {
return (
<div>
<button
onClick={ () => {
closeModal();
window.open( item.urls.raw, '_blank' );
} }
>
Open original image in new window
</button>
</div>
);
},
}
`
上記のコードは、アクションが画像に対してトリガーされるとモーダルを開き、元の画像を新しいウィンドウで開くボタンを提供します。これは、callback
を介して前のバージョンで定義されたのと同じ動作ですが、その間にモーダルウィンドウがあります。
このモーダルウィンドウをより魅力的にするために、ユーザーが新しいウィンドウで開く画像のサイズを選択できるようにドロップダウンを提供しましょう。
まず、モーダルウィンドウで使用するいくつかのコンポーネントをインポートします:
src/App.jsimport {
SelectControl,
Button,
__experimentalText as Text,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
...
} from '@wordpress/components';
SelectControl、Button、Text、HStack、およびVStackは、Reactを使用したWordPress開発のために利用可能なWordPressコンポーネントです。これらのコンポーネントや他のコンポーネントのライブ例は、Gutenberg Storybookで見つけることができます。
次に、次のコードを使用して‘see-original’アクションを定義します:
src/App.js{
id: 'see-original',
label: __( 'See Original' ),
modalHeader: __( 'See Original Image', 'action label' ),
RenderModal: ( { items: [ item ], closeModal } ) => {
const [ size, setSize ] = useState( 'raw' );
return (
<VStack spacing="5">
<Text>
{ `Select the size you want to open for "${ item.slug }"` }
</Text>
<HStack justify="left">
<SelectControl
__nextHasNoMarginBottom
label="Size"
value={ size }
options={ Object.keys( item.urls )
.filter( ( url ) => url !== 'small_s3' )
.map( ( url ) => ( {
label: url,
value: url,
} ) ) }
onChange={ setSize }
/>
</HStack>
<HStack justify="right">
<Button
__next40pxDefaultSize
variant="primary"
onClick={ () => {
closeModal();
window.open( item.urls[ size ], '_blank' );
} }
>
Open image from original location
</Button>
</HStack>
</VStack>
);
},
},
上記のコードは、SelectControl
コンポーネントを使用して、各画像の情報を含むオブジェクトから利用可能なサイズを表示します。選択された画像サイズは、size
状態変数にsetSize
関数を介して保存されます。
ButtonコンポーネントのonClick
は、closeModal(props経由で利用可能)を呼び出し、次に選択された画像(選択されたsize
)を新しいウィンドウで開きます。
これで、画像の‘see-original’アクションをクリックすると、次のようなモーダルウィンドウが表示されるはずです:
完全な実装と出力
この時点で、プロジェクトの最終src/App.js
ファイルはこのように見えるはずです:
src/App.jsimport { DataViews, filterSortAndPaginate } from '@wordpress/dataviews';
import { getTopicsElementsFormat } from './utils';
import { useState, useMemo } from '@wordpress/element';
import {
SelectControl,
Button,
__experimentalText as Text,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
Spinner,
withNotices,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import './style.scss';
// source "data" definition
import { dataPhotos } from './data';
// "defaultLayouts" definition
const primaryField = 'id';
const mediaField = 'img_src';
const defaultLayouts = {
table: {
layout: {
primaryField,
},
},
grid: {
layout: {
primaryField,
mediaField,
},
},
};
// "fields" definition
const fields = [
{
id: 'img_src',
label: __( 'Image' ),
render: ( { item } ) => (
<img alt={ item.alt_description } src={ item.urls.thumb } />
),
enableSorting: false,
},
{
id: 'id',
label: __( 'ID' ),
enableGlobalSearch: true,
},
{
id: 'author',
label: __( 'Author' ),
getValue: ( { item } ) =>
render: ( { item } ) => (
<a target="_blank" href={ item.user.url } rel="noreferrer">
{ item.user.first_name } { item.user.last_name }
</a>
),
enableGlobalSearch: true,
},
{
id: 'alt_description',
label: __( 'Description' ),
enableGlobalSearch: true,
},
{
id: 'topics',
label: __( 'Topics' ),
elements: getTopicsElementsFormat( dataPhotos ),
render: ( { item } ) => {
return (
<div className="topic_photos">
{ item.topics.map( ( topic ) => (
<span key={ topic } className="topic_photo_item">
{ topic.toUpperCase() }
</span>
) ) }
</div>
);
},
filterBy: {
operators: [ 'isAny', 'isNone', 'isAll', 'isNotAll' ],
},
enableSorting: false,
},
{
id: 'width',
label: __( 'Width' ),
getValue: ( { item } ) => parseInt( item.width ),
enableSorting: true,
},
{
id: 'height',
label: __( 'Height' ),
getValue: ( { item } ) => parseInt( item.height ),
enableSorting: true,
},
];
const App = withNotices( ( { noticeOperations, noticeUI } ) => {
const { createNotice } = noticeOperations;
const [ isUploadingItems, setIsUploadingItems ] = useState( [] );
// "view" and "setView" definition
const [ view, setView ] = useState( {
type: 'table',
perPage: 10,
layout: defaultLayouts.table.layout,
fields: [
'img_src',
'id',
'alt_description',
'author',
'topics',
'width',
'height',
],
} );
// "processedData" and "paginationInfo" definition
const { data: processedData, paginationInfo } = useMemo( () => {
return filterSortAndPaginate( dataPhotos, view, fields );
}, [ view ] );
const onSuccessMediaUpload = ( oImageUploaded ) => {
const title = oImageUploaded.title.rendered;
setIsUploadingItems( ( prevIsUploadingItems ) =>
prevIsUploadingItems.filter(
( slugLoading ) => slugLoading !== title
)
);
createNotice( {
status: 'success',
// translators: %s is the image title
content:
__( 'successfully uploaded to Media Library' ),
isDismissible: true,
} );
};
const onErrorMediaUpload = ( error ) => {
setIsUploadingItems( [] );
console.log( error );
createNotice( {
status: 'error',
content: __( 'An error occurred!' ),
isDismissible: true,
} );
};
// "actions" definition
const actions = [
{
id: 'upload-media',
label: __( 'Upload Media' ),
isPrimary: true,
icon: 'upload',
supportsBulk: true,
callback: ( images ) => {
images.forEach( async ( image ) => {
// 1- Download the image and convert it to a blob
const responseRequestImage = await fetch( image.urls.raw );
const blobImage = await responseRequestImage.blob();
// 2- Create FormData with the image blob
const formDataWithImage = new FormData();
formDataWithImage.append(
'file',
blobImage,
);
// 3- Send the request to the WP REST API with apiFetch
await apiFetch( {
path: '/wp/v2/media',
method: 'POST',
body: formDataWithImage,
} ).then( console.log );
} );
},
},
{
id: 'see-original',
label: __( 'See Original' ),
modalHeader: __( 'See Original Image', 'action label' ),
RenderModal: ( { items: [ item ], closeModal } ) => {
const [ size, setSize ] = useState( 'raw' );
return (
<VStack spacing="5">
<Text>
{ `Select the size you want to open for "${ item.slug }"` }
</Text>
<HStack justify="left">
<SelectControl
__nextHasNoMarginBottom
label="Size"
value={ size }
options={ Object.keys( item.urls )
.filter( ( url ) => url !== 'small_s3' )
.map( ( url ) => ( {
label: url,
value: url,
} ) ) }
onChange={ setSize }
/>
</HStack>
<HStack justify="right">
<Button
__next40pxDefaultSize
variant="primary"
onClick={ () => {
closeModal();
window.open( item.urls[ size ], '_blank' );
} }
>
Open image from original location
</Button>
</HStack>
</VStack>
);
},
},
];
return (
<>
{ !! isUploadingItems.length && <Spinner /> }
{ noticeUI }
<DataViews
data={ processedData }
fields={ fields }
view={ view }
onChangeView={ setView }
defaultLayouts={ defaultLayouts }
actions={ actions }
paginationInfo={ paginationInfo }
/>
</>
);
} );
export default App;
データビューのプロジェクトはこれで完了です!
管理パネルに移動し、‘メディアからサードパーティサービスを追加’サブページを‘メディア’設定の下に開きます。次の動作が観察されるはずです:
- テーブルモードでは、各画像にプライマリの‘Upload Media’アクションがあり、ホバー時にアイコンとして表示されます。
- 画像の三点ボタンをクリックすると、‘Upload Media’と‘See Original’の2つのアクションが表示されます。
- 複数の画像を選択し、すべてに対して‘Upload Media’アクションを同時に実行できます。
- 画像のアップロードプロセスが完了すると、通知ボックスが表示されます。
- 画像がアップロードされている間、スピナーアイコンが表示されます。
- “See Original”アクションは、ユーザーが新しいウィンドウで開く画像のサイズを選択できるモーダルウィンドウを表示します。
このプロジェクトの完全なコードは、https://github.com/wptrainingteam/devblog-dataviews-pluginで入手可能です。
プロジェクトのライブデモも、Playgroundによって提供されています。
まとめ
この記事は、データビューコンポーネントの可能性を探る2部構成のシリーズを締めくくります:
- プラグインでデータを表示し、対話するためのデータビューの使用は、データビューを始めるための基本的な概念をカバーしました。
- データビューからのアクション:メディアライブラリに画像を追加する(この記事)は、データビューアクションとそれを通じて実行できるタスクの種類について詳しく説明しました。
この機能の進捗を追跡したい場合は、gutenbergリポジトリの「[Feature] Data Views」ラベルの問題を確認できます。このコンポーネントには、隔週の更新もあり、https://make.wordpress.org/design/tag/dataviews/で共有されています。
@wordpress/dataviewsパッケージは、プラグイン開発者に新しい可能性を開くツールであり、この機能に取り組んでいるWordPressコア開発者は、あなたの意見を聞きたいと思っています。これを使用して興味深いと感じましたか?それとも、何かできなかったことに遭遇しましたか?コメントやGutenbergリポジトリの問題としてあなたの考えを共有してください。
@bph、@greenshady、@oandregal、@milana_capに感謝します。フィードバックとこの投稿のレビューを提供してくれました。