*/ private const PROVIDERS = array( 'amazon' => 'Amazon', 'rakuten' => '楽天市場', 'yahoo_shopping' => 'Yahoo!ショッピング', ); /** * Closed set of `affilun_status` query-parameter literals the * page-render closure may forward. * * @var list */ public const VALID_STATUS_FLASHES = array( 'saved', 'error' ); /** * Echo the §7 Add New admin page HTML to stdout. * * @param string|null $flash One of {@see self::VALID_STATUS_FLASHES} * or null when no flash is active. */ public function render( ?string $flash = null ): void { echo '
'; echo '

' . esc_html( '商品を追加' ) . '

'; $this->renderEmbedded( $flash ); echo '
'; } /** * Echo the add-new product UI without an outer admin-page wrapper. * * @param string|null $flash One of {@see self::VALID_STATUS_FLASHES} * or null when no flash is active. */ public function renderEmbedded( ?string $flash = null ): void { echo '

' . esc_html( // phpcs:ignore Generic.Files.LineLength.MaxExceeded -- Long literal accepted as a single visible paragraph. 'プロバイダーで商品を検索し、保存 をクリックします。保存した商品は「商品一覧」に即時反映され、ブロックエディターから選択できます。' ) . '

'; $this->renderFlash( $flash ); $this->renderForm(); $this->renderResultsShell(); $this->renderScript(); } /** * Render the optional admin-notice flash banner. * * @param string|null $flash Whitelisted status literal or null. */ private function renderFlash( ?string $flash ): void { if ( $flash === null ) { return; } if ( ! in_array( $flash, self::VALID_STATUS_FLASHES, true ) ) { return; } if ( $flash === 'saved' ) { echo '

' . esc_html( '商品を保存しました。' ) . '

'; return; } echo '

' . esc_html( '保存に失敗しました。インラインエラーの詳細を確認してください。' ) . '

'; } /** * Render the provider tab strip + keyword input + Search * button. * * The form is intentionally `
` * (interactive submit is handled in JS); a non-JS fallback * is not implemented because the §8.1 search endpoint can * only be exercised through `wp.apiFetch` with the editor * nonce in this iteration. */ private function renderForm(): void { echo ''; echo '
'; echo '' . esc_html( 'プロバイダー' ) . ''; $first = true; foreach ( self::PROVIDERS as $key => $label ) { $inputId = 'affilun-add-new-provider-' . $key; echo ''; $first = false; } echo '
'; echo ''; echo '
'; } /** * Render the empty results-pane shell the inline JS * populates after the §8.1 search round-trip. */ private function renderResultsShell(): void { echo '
'; echo '
'; echo '

' . esc_html( 'キーワードを入力して検索をクリックすると、商品を取得します。' ) . '

'; echo '
'; } /** * Enqueue the inline JS that drives the search + save loop. * * The script: * * - Reads the selected provider radio + the keyword input. * - Calls `wp.apiFetch` against `/affilun/v1/products/search` * to populate the results pane. * - Renders a per-row "Save" button that POSTs the row to * `/affilun/v1/products` and surfaces "Saved (#N)" inline on * success, with a "View in Products" link to the saved * record. * - Falls back to a visible error message when * `wp.apiFetch` is unavailable (very old admin host). * * The script reuses the same buildSavePayload / extractSavedId * helper logic the editor JS uses (commit 7ffa110 + * commit 26aefb7) so the wp-admin and Gutenberg surfaces * stay behaviour-equivalent. * * Attached to a no-source admin handle via `wp_add_inline_script()`. */ private function renderScript(): void { // Resolve the products-list admin URL the per-row "View" // link points at, with a synthetic fallback so the link // still parses through esc_url() in pure-unit harnesses // where WordPress is not loaded. $productsAdminUrl = function_exists( 'admin_url' ) ? (string) admin_url( 'admin.php?page=affilun-products' ) : 'http://example.test/wp-admin/admin.php?page=affilun-products'; $productsAdminUrlJs = wp_json_encode( $productsAdminUrl ); if ( ! is_string( $productsAdminUrlJs ) ) { $productsAdminUrlJs = '""'; } // Server-emitted i18n strings the JS surfaces inline. Emitted // through wp_json_encode() so multi-byte strings round-trip // safely. $strings = array( 'enter_keyword' => '検索キーワードを入力してください。', 'searching' => '検索中…', 'no_results' => '一致する商品が見つかりませんでした。', 'search_failed' => '検索に失敗しました。プロバイダー認証情報が設定済みか、edit_posts 権限があるかを確認してください。', 'save_label' => '保存', 'saving_label' => '保存中…', 'saved_label' => '保存しました (#%d)', 'use_saved_label' => '保存済み (#%d)', 'save_failed' => '保存に失敗しました。', 'invalid_row' => 'この検索結果は必須フィールド (タイトル / 詳細 URL) が欠けているため保存できません。', 'view_link' => '商品一覧で表示', 'no_apifetch' => 'wp.apiFetch が読み込まれていないため、この画面から REST API にアクセスできません。wp-api スクリプトがエンキューされているか確認してください。', ); $stringsJs = wp_json_encode( $strings ); if ( ! is_string( $stringsJs ) ) { $stringsJs = '{}'; } $script = '(function(){' . 'var STRINGS=' . $stringsJs . ';' . 'var PRODUCTS_ADMIN_URL=' . $productsAdminUrlJs . ';' . <<<'JS' function format(tpl, n){return tpl.replace('%d', String(n));} function statusLine(text, isError){ var el=document.getElementById('affilun-add-new-status'); if(!el)return; el.textContent=text||''; el.style.color=isError?'#b32d2e':''; } function selectedProvider(){ var radios=document.querySelectorAll('input[name="affilun_provider"]'); for(var i=0;i0)return d.id; if(d.product&&typeof d.product.id==='number'&&d.product.id>0)return d.product.id; } if(response&&typeof response.id==='number'&&response.id>0)return response.id; return null; } function renderError(container, msg){ container.innerHTML=''; var p=document.createElement('p'); p.className='description'; p.style.color='#b32d2e'; p.style.margin='12px'; p.textContent=msg; container.appendChild(p); } function renderEmpty(container, msg){ container.innerHTML=''; var p=document.createElement('p'); p.className='description'; p.style.margin='12px'; p.textContent=msg; container.appendChild(p); } function renderRow(row){ var li=document.createElement('li'); li.style.display='flex'; li.style.alignItems='flex-start'; li.style.gap='10px'; li.style.padding='10px 12px'; li.style.borderBottom='1px solid #eee'; if(typeof row.image_primary_url==='string'&&row.image_primary_url!==''){ var img=document.createElement('img'); img.src=row.image_primary_url; img.alt=''; img.style.width='56px';img.style.height='56px';img.style.objectFit='cover'; img.style.flex='0 0 auto';img.style.borderRadius='3px'; li.appendChild(img); } else { var ph=document.createElement('div'); ph.style.width='56px';ph.style.height='56px';ph.style.background='#f0f0f0'; ph.style.flex='0 0 auto';ph.style.borderRadius='3px'; li.appendChild(ph); } var body=document.createElement('div'); body.style.flex='1 1 auto';body.style.minWidth='0'; var title=document.createElement('div'); title.style.fontWeight='600'; title.textContent=typeof row.title==='string'?row.title:'(no title)'; body.appendChild(title); if(typeof row.brand==='string'&&row.brand!==''){ var brand=document.createElement('div');brand.className='description'; brand.style.fontSize='12px';brand.textContent=row.brand;body.appendChild(brand); } if(row.price&&typeof row.price.label==='string'&&row.price.label!==''){ var price=document.createElement('div');price.style.fontSize='13px'; price.style.marginTop='2px';price.textContent=row.price.label;body.appendChild(price); } if(row.links&&typeof row.links.detail_url==='string'){ var linkLine=document.createElement('div');linkLine.style.fontSize='11px'; linkLine.style.marginTop='2px'; var a=document.createElement('a');a.href=row.links.detail_url;a.target='_blank';a.rel='noopener noreferrer'; a.textContent=row.links.detail_url;linkLine.appendChild(a);body.appendChild(linkLine); } var rowStatus=document.createElement('div'); rowStatus.style.fontSize='12px';rowStatus.style.marginTop='4px'; body.appendChild(rowStatus); li.appendChild(body); var actions=document.createElement('div'); actions.style.flex='0 0 auto'; var btn=document.createElement('button'); btn.type='button';btn.className='button button-primary'; if(row.is_saved&&typeof row.id==='number'&&row.id>0){ btn.textContent=format(STRINGS.use_saved_label, row.id); btn.disabled=true; actions.appendChild(btn); appendViewLink(actions, row.id); } else { btn.textContent=STRINGS.save_label; btn.addEventListener('click', function(){onSaveClick(row, btn, rowStatus, actions);}); actions.appendChild(btn); } li.appendChild(actions); return li; } function appendViewLink(container, id){ var sep=container.querySelector('.affilun-view-link'); if(sep)return; var a=document.createElement('a'); a.className='affilun-view-link'; a.href=PRODUCTS_ADMIN_URL; a.style.marginLeft='8px'; a.style.display='inline-block'; a.style.lineHeight='28px'; a.textContent=STRINGS.view_link; container.appendChild(a); } function onSaveClick(row, btn, rowStatus, actions){ if(!window.wp||typeof window.wp.apiFetch!=='function'){ rowStatus.style.color='#b32d2e'; rowStatus.textContent=STRINGS.no_apifetch; return; } var payload=buildPayload(row); if(payload===null){ rowStatus.style.color='#b32d2e'; rowStatus.textContent=STRINGS.invalid_row; return; } btn.disabled=true; btn.textContent=STRINGS.saving_label; rowStatus.style.color=''; rowStatus.textContent=''; window.wp.apiFetch({path:'/affilun/v1/products', method:'POST', data:payload}).then(function(response){ var savedId=extractSavedId(response); if(savedId===null){ btn.disabled=false; btn.textContent=STRINGS.save_label; rowStatus.style.color='#b32d2e'; rowStatus.textContent=STRINGS.save_failed; return; } btn.textContent=format(STRINGS.saved_label, savedId); appendViewLink(actions, savedId); }).catch(function(err){ btn.disabled=false; btn.textContent=STRINGS.save_label; rowStatus.style.color='#b32d2e'; var msg=STRINGS.save_failed; if(err&&typeof err.message==='string'&&err.message!=='')msg=err.message; else if(err&&err.error&&typeof err.error.message==='string')msg=err.error.message; rowStatus.textContent=msg; }); } function runSearch(){ if(!window.wp||typeof window.wp.apiFetch!=='function'){ statusLine(STRINGS.no_apifetch, true); return; } var input=document.getElementById('affilun-add-new-keywords'); var container=document.getElementById('affilun-add-new-results'); if(!input||!container)return; var keywords=(input.value||'').trim(); if(keywords===''){ statusLine(STRINGS.enter_keyword, true); return; } var provider=selectedProvider(); if(provider===''){ statusLine(STRINGS.enter_keyword, true); return; } statusLine(STRINGS.searching, false); container.innerHTML=''; var path='/affilun/v1/products/search?provider='+encodeURIComponent(provider)+'&keywords='+encodeURIComponent(keywords)+'&per_page=10'; window.wp.apiFetch({path:path}).then(function(response){ var items=extractItems(response); statusLine('', false); if(items.length===0){renderEmpty(container, STRINGS.no_results);return;} var ul=document.createElement('ul'); ul.style.listStyle='none';ul.style.margin='0';ul.style.padding='0'; items.forEach(function(row){ul.appendChild(renderRow(row));}); container.innerHTML=''; container.appendChild(ul); }).catch(function(err){ var msg=STRINGS.search_failed; if(err&&typeof err.message==='string'&&err.message!=='')msg=err.message; else if(err&&err.error&&typeof err.error.message==='string')msg=err.error.message; statusLine('', false); renderError(container, msg); }); } function init(){ var btn=document.getElementById('affilun-add-new-search'); if(btn)btn.addEventListener('click', runSearch); var kw=document.getElementById('affilun-add-new-keywords'); if(kw)kw.addEventListener('keydown', function(e){if(e.key==='Enter'){e.preventDefault();runSearch();}}); } if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', init); } else { init(); } JS; $script .= '})();'; self::enqueueInlineScript( 'affilun-add-new-product-admin', $script ); } /** * Attach inline admin JavaScript through WordPress enqueue APIs. * * @param string $handle Script handle. * @param string $script JavaScript source. */ private static function enqueueInlineScript( string $handle, string $script ): void { if ( ! function_exists( 'wp_register_script' ) || ! function_exists( 'wp_enqueue_script' ) || ! function_exists( 'wp_add_inline_script' ) ) { return; } wp_register_script( $handle, false, array( 'wp-api-fetch' ), defined( 'AFFILUN_VERSION' ) ? AFFILUN_VERSION : '0.7.3', true ); wp_enqueue_script( $handle ); wp_add_inline_script( $handle, $script ); } }