feat: add import error tracking and translation tabs
Import Error Tracking:
- Add MarketplaceImportError model to store detailed error information
- Store row number, identifier, error type, message, and row data for each error
- Add API endpoint GET /admin/marketplace-import-jobs/{job_id}/errors
- Add UI to view and browse import errors in job details modal
- Support pagination and error type filtering
Translation Tabs:
- Replace flat translation list with tabbed interface on product detail page
- Add language tabs with full language names
- Add copy-to-clipboard functionality for translation content
- Improved UX with better visual separation of translations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -221,34 +221,93 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Translations Card -->
|
||||
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="product?.translations && Object.keys(product.translations).length > 0">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Translations
|
||||
</h3>
|
||||
<div class="space-y-6">
|
||||
<template x-for="(trans, lang) in (product?.translations || {})" :key="lang">
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 pb-4 last:border-b-0 last:pb-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="px-2 py-0.5 text-xs font-semibold uppercase bg-gray-100 dark:bg-gray-700 rounded" x-text="lang"></span>
|
||||
<!-- Translations Card with Tabs -->
|
||||
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800" x-show="product?.translations && Object.keys(product.translations).length > 0" x-data="{ activeTab: Object.keys(product?.translations || {})[0] || '' }">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Translations
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<span x-text="Object.keys(product?.translations || {}).length"></span> language(s)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Language Tabs -->
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 mb-4">
|
||||
<nav class="-mb-px flex space-x-4 overflow-x-auto" aria-label="Translation tabs">
|
||||
<template x-for="(trans, lang) in (product?.translations || {})" :key="lang">
|
||||
<button
|
||||
@click="activeTab = lang"
|
||||
:class="{
|
||||
'border-purple-500 text-purple-600 dark:text-purple-400': activeTab === lang,
|
||||
'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300': activeTab !== lang
|
||||
}"
|
||||
class="whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors"
|
||||
>
|
||||
<span class="uppercase" x-text="lang"></span>
|
||||
<span class="ml-1 text-xs text-gray-400" x-text="getLanguageName(lang)"></span>
|
||||
</button>
|
||||
</template>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<template x-for="(trans, lang) in (product?.translations || {})" :key="'content-' + lang">
|
||||
<div x-show="activeTab === lang" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
|
||||
<div class="space-y-4">
|
||||
<!-- Title -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 p-4 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Title</p>
|
||||
<button
|
||||
@click="copyToClipboard(trans?.title)"
|
||||
class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<span x-html="$icon('clipboard', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-base font-medium text-gray-900 dark:text-gray-100" x-text="trans?.title || 'No title'"></p>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400">Title</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="trans?.title || '-'">-</p>
|
||||
|
||||
<!-- Short Description -->
|
||||
<div x-show="trans?.short_description" class="bg-gray-50 dark:bg-gray-700/50 p-4 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Short Description</p>
|
||||
<button
|
||||
@click="copyToClipboard(trans?.short_description)"
|
||||
class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<span x-html="$icon('clipboard', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div x-show="trans?.short_description">
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400">Short Description</p>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="trans?.short_description">-</p>
|
||||
</div>
|
||||
<div x-show="trans?.description">
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400">Description</p>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300 prose prose-sm dark:prose-invert max-w-none" x-html="trans?.description || '-'"></div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300" x-text="trans?.short_description"></p>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div x-show="trans?.description" class="bg-gray-50 dark:bg-gray-700/50 p-4 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Description</p>
|
||||
<button
|
||||
@click="copyToClipboard(trans?.description)"
|
||||
class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<span x-html="$icon('clipboard', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300 prose prose-sm dark:prose-invert max-w-none max-h-96 overflow-y-auto" x-html="trans?.description || 'No description'"></div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state if no content -->
|
||||
<div x-show="!trans?.title && !trans?.short_description && !trans?.description" class="text-center py-8">
|
||||
<span x-html="$icon('document-text', 'w-12 h-12 mx-auto text-gray-300 dark:text-gray-600')"></span>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">No translation content for this language</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Timestamps -->
|
||||
|
||||
@@ -625,11 +625,64 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Error Details #}
|
||||
<div x-show="{{ job_var }}?.error_details?.length > 0">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Error Details</p>
|
||||
<div class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg max-h-48 overflow-y-auto">
|
||||
<pre class="text-xs text-red-700 dark:text-red-300 whitespace-pre-wrap font-mono" x-text="JSON.stringify({{ job_var }}?.error_details, null, 2)"></pre>
|
||||
{# Detailed Import Errors #}
|
||||
<div x-show="{{ job_var }}?.error_count > 0">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Import Errors</p>
|
||||
<button
|
||||
x-show="!jobErrors || jobErrors.length === 0"
|
||||
@click="loadJobErrors({{ job_var }}?.id)"
|
||||
:disabled="loadingErrors"
|
||||
class="px-3 py-1 text-xs font-medium text-purple-600 dark:text-purple-400 border border-purple-300 dark:border-purple-600 rounded-lg hover:bg-purple-50 dark:hover:bg-purple-900/20 disabled:opacity-50"
|
||||
>
|
||||
<span x-show="loadingErrors" x-html="$icon('spinner', 'w-3 h-3 mr-1 inline')"></span>
|
||||
<span x-text="loadingErrors ? 'Loading...' : 'View Errors'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Errors List #}
|
||||
<div x-show="jobErrors && jobErrors.length > 0" class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<template x-for="error in jobErrors" :key="error.id">
|
||||
<div class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="px-2 py-0.5 text-xs font-medium bg-red-100 dark:bg-red-800 text-red-700 dark:text-red-300 rounded" x-text="error.error_type"></span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">Row <span x-text="error.row_number"></span></span>
|
||||
</div>
|
||||
<p class="text-sm text-red-700 dark:text-red-300" x-text="error.error_message"></p>
|
||||
<p x-show="error.identifier" class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
ID: <span class="font-mono" x-text="error.identifier"></span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
x-show="error.row_data"
|
||||
@click="error._expanded = !error._expanded"
|
||||
class="ml-2 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
:title="error._expanded ? 'Hide row data' : 'Show row data'"
|
||||
>
|
||||
<span x-html="$icon(error._expanded ? 'chevron-up' : 'chevron-down', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
{# Expandable row data #}
|
||||
<div x-show="error._expanded && error.row_data" x-collapse class="mt-2 pt-2 border-t border-red-200 dark:border-red-700">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">Row Data:</p>
|
||||
<pre class="text-xs text-gray-600 dark:text-gray-300 font-mono whitespace-pre-wrap bg-white dark:bg-gray-800 p-2 rounded" x-text="JSON.stringify(error.row_data, null, 2)"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{# Pagination for errors #}
|
||||
<div x-show="jobErrorsTotal > jobErrors?.length" class="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>Showing <span x-text="jobErrors?.length"></span> of <span x-text="jobErrorsTotal"></span> errors</span>
|
||||
<button
|
||||
@click="loadMoreJobErrors({{ job_var }}?.id)"
|
||||
:disabled="loadingErrors"
|
||||
class="text-purple-600 dark:text-purple-400 hover:underline disabled:opacity-50"
|
||||
>
|
||||
Load more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user