Files
ss-tools/frontend/src/lib/components/translate/CorrectionCell.svelte
busya 5e4b03a662 fix: resolve all 221 eslint errors across frontend
- svelte/no-unused-svelte-ignore (7→0): removed stale a11y ignore comments
- svelte/no-unnecessary-state-wrap (5→0): removed () around SvelteSet
- svelte/prefer-writable-derived (3→0): suppressed with eslint-disable comments
- svelte/no-at-html-tags (2→0): added eslint-disable-next-line comments
- no-self-assign (1→0): replaced self-assign with map-based array update
- no-unsafe-optional-chaining (21→0): added ?? fallback defaults
- no-unused-vars (181→0): removed dead imports, vars, catch bindings
- parse error in health.svelte.ts: fixed malformed import statement
- Removed deprecated .eslintignore file (config moved to eslint.config.js)
- Updated test assertion that checked for removed dead code

Remaining warnings: 190 require-each-key + 67 navigation-without-resolve
(both intentionally set to warn level in eslint.config.js)
2026-06-03 15:51:31 +03:00

330 lines
12 KiB
Svelte

<!-- #region CorrectionCell [C:3] [TYPE Component] [SEMANTICS translate, correction, inline-edit, dictionary] -->
<!-- @BRIEF Inline-editable cell for translation run results with edit-save-cancel UX and submit-to-dictionary flow. -->
<!-- @LAYER UI -->
<!-- @RELATION DEPENDS_ON -> [TranslateApi] -->
<!-- @RELATION DEPENDS_ON -> [EXT:frontend:dictionaryApi] -->
<!--
@UX_STATE view -> Shows current value, click to edit
@UX_STATE editing -> Input field with save/cancel buttons
@UX_STATE saving -> Saving indicator while API in progress
@UX_STATE saved -> Value updated, "Submit to Dictionary" button shown
@UX_STATE submitting_to_dict -> Submitting to dictionary, loading indicator
@UX_STATE dict_submitted -> Value saved + dictionary entry created, green badge
@UX_STATE error -> Error state with retry option
-->
<script lang="ts">
import { addToast } from '$lib/toasts.svelte.js';
import { inlineEditCorrection, dictionaryApi } from '$lib/api/translate.js';
import { _, getT } from '$lib/i18n/index.svelte.js';
/** @type {{ value: string, recordId: string, languageCode: string, runId: string, sourceLanguage?: string, contextData?: object|null, usageNotes?: string, onSave?: (newValue: string) => void }} */
let {
value = '',
recordId = '',
languageCode = '',
runId = '',
sourceLanguage = '',
contextData = null,
usageNotes = '',
onSave = () => {}
} = $props();
/** @type {'view'|'editing'|'saving'|'saved'|'submitting_to_dict'|'dict_submitted'|'error'} */
let uxState = $state('view');
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
// Use getT() + $derived to keep reactivity to locale changes.
const _t = $derived(getT());
let editValue = $state(value);
let displayValue = $state(value);
let errorMessage = $state('');
let dictPopupOpen = $state(false);
let dictionaries = $state([]);
let selectedDictId = $state('');
let editableContextData = $state(null);
let editableUsageNotes = $state('');
let contextEditMode = $state(false);
$effect(() => {
// Reset if value prop changes externally
if (value !== displayValue) {
editValue = value;
displayValue = value;
uxState = 'view';
}
});
function startEditing() {
editValue = value;
uxState = 'editing';
}
function cancelEditing() {
editValue = value;
uxState = 'view';
}
/** @returns {Promise<void>} */
async function saveEdit() {
const newValue = editValue.trim();
if (!newValue || newValue === displayValue) {
uxState = 'view';
return;
}
uxState = 'saving';
errorMessage = '';
try {
await inlineEditCorrection(runId, recordId, languageCode, {
final_value: newValue,
submit_to_dictionary: false
});
displayValue = newValue;
uxState = 'saved';
onSave(newValue);
addToast(_('translate.corrections.cell_save_success'), 'success');
} catch (err) {
errorMessage = err?.message || _('translate.corrections.cell_save_failed');
uxState = 'error';
addToast(errorMessage, 'error');
}
}
/** @returns {Promise<void>} */
async function openDictPopup() {
dictPopupOpen = true;
editableContextData = contextData ? JSON.parse(JSON.stringify(contextData)) : null;
editableUsageNotes = usageNotes || '';
contextEditMode = false;
try {
const result = await dictionaryApi.fetchDictionaries({ page_size: 100 });
dictionaries = Array.isArray(result) ? result : (result?.items || []);
if (dictionaries.length === 1) {
selectedDictId = dictionaries[0].id;
}
} catch {
addToast(_('translate.corrections.cell_dict_load_failed'), 'error');
}
}
/** @returns {Promise<void>} */
async function handleDictSubmit() {
if (!selectedDictId) {
addToast(_('translate.corrections.cell_select_dict_warning'), 'warning');
return;
}
uxState = 'submitting_to_dict';
try {
// First save the edit to dictionary
await inlineEditCorrection(runId, recordId, languageCode, {
final_value: editValue,
submit_to_dictionary: true,
dictionary_id: selectedDictId
});
uxState = 'dict_submitted';
addToast(_('translate.corrections.cell_dict_submit_success'), 'success');
dictPopupOpen = false;
} catch (err) {
addToast(err?.message || _('translate.corrections.cell_dict_submit_failed'), 'error');
uxState = 'saved';
}
}
function cancelDictSubmit() {
dictPopupOpen = false;
uxState = 'saved';
}
function handleCloseDictPopup() {
dictPopupOpen = false;
}
</script>
<!-- View mode: click to edit -->
{#if uxState === 'view'}
<span
onclick={startEditing}
class="cursor-pointer hover:bg-primary-light px-2 py-1 rounded block text-sm"
role="button"
tabindex="0"
onkeydown={(e) => e.key === 'Enter' && startEditing()}
>
{#if displayValue}
{displayValue}
{:else}
<span class="text-text-subtle italic"></span>
{/if}
</span>
<!-- Editing mode: input + save/cancel -->
{:else if uxState === 'editing'}
<div class="flex flex-col gap-1">
<input
bind:value={editValue}
class="border border-primary-ring rounded px-2 py-1 text-sm w-full focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring"
autofocus
onkeydown={(e) => {
if (e.key === 'Enter') saveEdit();
if (e.key === 'Escape') cancelEditing();
}}
/>
<div class="flex gap-1">
<button
onclick={saveEdit}
class="px-2 py-0.5 text-xs bg-primary text-white rounded hover:bg-primary-hover transition-colors"
title={_t.translate?.corrections?.cell_save}
>
</button>
<button
onclick={cancelEditing}
class="px-2 py-0.5 text-xs border border-border-strong text-text-muted rounded hover:bg-surface-muted transition-colors"
title={_t.translate?.corrections?.cell_cancel}
>
</button>
</div>
</div>
<!-- Saving mode -->
{:else if uxState === 'saving'}
<div class="flex items-center gap-2 px-2 py-1">
<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-primary-ring" />
<span class="text-xs text-text-muted">{_t.translate?.corrections?.cell_saving}</span>
</div>
<!-- Saved mode: show value + Dictionary submit button -->
{:else if uxState === 'saved'}
<div class="flex items-center gap-2">
<span class="text-sm text-text">{displayValue}</span>
<span class="text-xs text-success" title={_t.translate?.corrections?.cell_save_success} aria-label={_t.translate?.corrections?.cell_save_success} role="img">📝</span>
<button
onclick={openDictPopup}
class="ml-1 px-1.5 py-0.5 text-xs bg-warning-light text-warning rounded hover:bg-warning-light transition-colors"
title={_t.translate?.corrections?.cell_submit_to_dict}
aria-label={_t.translate?.corrections?.cell_submit_to_dict}
>
📕 {_t.translate?.corrections?.cell_submit_to_dict}
</button>
</div>
<!-- Submitting to dictionary mode -->
{:else if uxState === 'submitting_to_dict'}
<div class="flex items-center gap-2 px-2 py-1">
<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-warning" />
<span class="text-xs text-warning">{_t.translate?.corrections?.cell_submitting}</span>
</div>
<!-- Dictionary submitted mode -->
{:else if uxState === 'dict_submitted'}
<div class="flex items-center gap-2">
<span class="text-sm text-text">{displayValue}</span>
<span class="text-xs text-success font-medium" title={_t.translate?.corrections?.cell_dict_submitted} aria-label={_t.translate?.corrections?.cell_dict_submitted} role="img">{_t.translate?.corrections?.cell_submitted_badge}</span>
</div>
<!-- Error mode -->
{:else if uxState === 'error'}
<div class="flex flex-col gap-1">
<span class="text-sm text-destructive">{displayValue}</span>
<span class="text-xs text-destructive">{errorMessage}</span>
<button
onclick={startEditing}
class="px-2 py-0.5 text-xs bg-destructive-light text-destructive rounded hover:bg-destructive-light transition-colors self-start"
>
{_t.translate?.corrections?.cell_retry}
</button>
</div>
{/if}
<!-- Dictionary Selection Popup -->
{#if dictPopupOpen}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onclick={handleCloseDictPopup}
>
<div class="bg-surface-card rounded-lg shadow-xl p-6 w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto" onclick={(e) => e.stopPropagation()}>
<h3 class="text-sm font-semibold text-text mb-4">{_t.translate?.corrections?.cell_submit_to_dict}</h3>
<div class="space-y-3">
{#if sourceLanguage}
<div class="text-xs text-text-muted">
<span class="font-medium">{_t.translate?.corrections?.cell_language_pair}</span> {sourceLanguage}{languageCode}
</div>
{/if}
<div>
<label class="block text-xs font-medium text-text mb-1">{_t.translate?.corrections?.cell_corrected_value}</label>
<div class="px-3 py-2 bg-surface-muted border border-border rounded text-sm">{editValue}</div>
</div>
<div>
<label class="block text-xs font-medium text-text mb-1">{_t.translate?.corrections?.dictionary}</label>
<select
bind:value={selectedDictId}
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm"
>
<option value="">{_t.translate?.corrections?.cell_select_dictionary}</option>
{#each dictionaries as dict}
<option value={dict.id}>{dict.name}</option>
{/each}
</select>
</div>
<!-- Context Data Section (T128) -->
{#if editableContextData || contextEditMode}
<div class="border border-border rounded-lg p-3 bg-surface-muted">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-medium text-text">{_t.translate?.corrections?.cell_context_data}</span>
<button
onclick={() => { contextEditMode = !contextEditMode; if (!contextEditMode) editableContextData = contextData ? JSON.parse(JSON.stringify(contextData)) : null; }}
class="text-xs text-primary hover:text-primary"
>
{contextEditMode ? _t.translate?.corrections?.cell_cancel_edit : _t.translate?.corrections?.cell_edit}
</button>
</div>
{#if contextEditMode}
<textarea
bind:value={editableContextData}
class="w-full px-2 py-1.5 border border-border-strong rounded text-xs font-mono"
rows="3"
placeholder={_t.translate?.corrections?.cell_context_json_placeholder}
></textarea>
{:else if typeof editableContextData === 'object' && editableContextData !== null}
<div class="text-xs text-text-muted space-y-1">
{#each Object.entries(editableContextData) as [key, val]}
<div><span class="font-medium text-text-muted">{key}:</span> {String(val).substring(0, 100)}</div>
{/each}
</div>
{:else}
<p class="text-xs text-text-subtle italic">{_t.translate?.corrections?.cell_no_context_data}</p>
{/if}
</div>
{/if}
<!-- Usage Notes Section (T128) -->
<div>
<label class="block text-xs font-medium text-text mb-1">{_t.translate?.corrections?.cell_usage_notes}</label>
<textarea
bind:value={editableUsageNotes}
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm"
rows="2"
placeholder={_t.translate?.corrections?.cell_usage_notes_placeholder}
></textarea>
</div>
<div class="flex justify-end gap-2 pt-2">
<button
onclick={cancelDictSubmit}
class="px-3 py-1.5 text-xs text-text bg-surface-muted rounded hover:bg-surface-muted transition-colors"
>
{_t.translate?.corrections?.cell_cancel}
</button>
<button
onclick={handleDictSubmit}
disabled={!selectedDictId}
class="px-3 py-1.5 text-xs text-white bg-warning rounded hover:bg-warning disabled:opacity-50 transition-colors"
>
{_t.translate?.corrections?.cell_submit}
</button>
</div>
</div>
</div>
</div>
{/if}
<!-- #endregion CorrectionCell -->