refactor: PairForm — single type dropdown, 4 variants, carry-over behavior
- answer_type is now a single value (text/yes_no/question/word) - Dropdown replaces checkboxes, per-type field sections - Text: only positive statement - Yes/No: optional question + answer picker - Question: question* + positive* + optional negative - Word: optional question + word pickers - After save: texts/words carry over, type resets for next pair - Fix: closed missing div in PairsPanel scroll container Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -156,16 +156,22 @@ function WordTag({ word, onRemove }) {
|
||||
}
|
||||
|
||||
// ─── PairForm ─────────────────────────────────────────────────────────────────
|
||||
const PAIR_TYPES = [
|
||||
{ value: 'text', label: 'Text', hint: 'Nur positives Statement' },
|
||||
{ value: 'yes_no', label: 'Ja / Nein', hint: 'Frage + Ja/Nein Antwort' },
|
||||
{ value: 'question', label: 'Frage', hint: 'Frage + Positiv + Negativ' },
|
||||
{ value: 'word', label: 'Wort', hint: 'Frage + Positive/Negative Wörter' },
|
||||
];
|
||||
|
||||
function PairForm({ objectId, onPairSaved }) {
|
||||
const lang = getUserLang();
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [savedFlash, setSavedFlash] = useState(false);
|
||||
|
||||
// Type checkboxes
|
||||
const [typeText, setTypeText] = useState(false);
|
||||
const [typeYesNo, setTypeYesNo] = useState(false);
|
||||
const [typeWord, setTypeWord] = useState(false);
|
||||
// Single type dropdown
|
||||
const [type, setType] = useState('');
|
||||
|
||||
// Text fields
|
||||
// Text fields — carried over between saves
|
||||
const [question, setQuestion] = useState('');
|
||||
const [positive, setPositive] = useState('');
|
||||
const [negative, setNegative] = useState('');
|
||||
@@ -174,16 +180,17 @@ function PairForm({ objectId, onPairSaved }) {
|
||||
const [creatingWord, setCreatingWord] = useState(false);
|
||||
|
||||
// Yes/No answer
|
||||
const [yesNoAnswer, setYesNoAnswer] = useState(null); // null | true | false
|
||||
const [yesNoAnswer, setYesNoAnswer] = useState(null);
|
||||
|
||||
// Word pickers
|
||||
// Word pickers — carried over between saves
|
||||
const [positiveWords, setPositiveWords] = useState([]);
|
||||
const [negativeWords, setNegativeWords] = useState([]);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Auto-detect words from text fields (only when Text type active)
|
||||
const allText = typeText ? `${question} ${positive} ${negative}` : '';
|
||||
// Auto-detect words in sentence fields
|
||||
const needsWordDetection = type === 'text' || type === 'question';
|
||||
const allText = needsWordDetection ? `${question} ${positive} ${negative}` : '';
|
||||
useEffect(() => {
|
||||
if (!allText.trim()) { setWordMap({}); return; }
|
||||
const t = setTimeout(async () => {
|
||||
@@ -212,40 +219,32 @@ function PairForm({ objectId, onPairSaved }) {
|
||||
finally { setCreatingWord(false); }
|
||||
}
|
||||
|
||||
// Validation
|
||||
const atLeastOne = typeText || typeYesNo || typeWord;
|
||||
const textOk = !typeText || positive.trim().length > 0;
|
||||
const negativeOk = !negative.trim() || (question.trim().length > 0 && positive.trim().length > 0);
|
||||
const canSave = atLeastOne && textOk && negativeOk;
|
||||
function canSave() {
|
||||
if (!type) return false;
|
||||
if (type === 'text') return positive.trim().length > 0;
|
||||
if (type === 'yes_no') return true;
|
||||
if (type === 'question') return question.trim().length > 0 && positive.trim().length > 0;
|
||||
if (type === 'word') return positiveWords.length > 0 || negativeWords.length > 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
function validationHint() {
|
||||
if (!atLeastOne) return 'Wähle mindestens einen Typ (Text / Ja·Nein / Wort).';
|
||||
if (!textOk) return 'Text: Positive Aussage ist Pflicht.';
|
||||
if (!negativeOk) return 'Negative Aussage erfordert Frage + Positive Aussage.';
|
||||
if (!type) return 'Bitte einen Typ wählen.';
|
||||
if (type === 'text' && !positive.trim()) return 'Statement ist Pflicht.';
|
||||
if (type === 'question' && !question.trim()) return 'Frage ist Pflicht.';
|
||||
if (type === 'question' && !positive.trim()) return 'Positive Aussage ist Pflicht.';
|
||||
if (type === 'word' && !positiveWords.length && !negativeWords.length)
|
||||
return 'Mindestens ein Wort erforderlich.';
|
||||
return '';
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setTypeText(false); setTypeYesNo(false); setTypeWord(false);
|
||||
setQuestion(''); setPositive(''); setNegative('');
|
||||
setWordMap({}); setYesNoAnswer(null);
|
||||
setPositiveWords([]); setNegativeWords([]);
|
||||
setShowForm(false);
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!canSave) return;
|
||||
if (!canSave()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const answerTypes = [
|
||||
...(typeText ? ['text'] : []),
|
||||
...(typeYesNo ? ['yes_no'] : []),
|
||||
...(typeWord ? ['word'] : []),
|
||||
];
|
||||
|
||||
// Question (only if Text + question text filled)
|
||||
// Question record — for yes_no, question, word (if text filled)
|
||||
let questionId = null;
|
||||
if (typeText && question.trim()) {
|
||||
if (type !== 'text' && question.trim()) {
|
||||
const q = await apiPost('/questions', {
|
||||
[`sentence_${lang}`]: withPlaceholders(question, wordMap),
|
||||
status: 'draft',
|
||||
@@ -254,87 +253,97 @@ function PairForm({ objectId, onPairSaved }) {
|
||||
}
|
||||
|
||||
// Positive statement
|
||||
const posBody = { status: 'draft' };
|
||||
if (typeText && positive.trim())
|
||||
posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap);
|
||||
if (typeYesNo && yesNoAnswer !== null)
|
||||
posBody.answer = yesNoAnswer;
|
||||
|
||||
const posStmt = await apiPost('/statements', posBody);
|
||||
|
||||
// Link positive words to positive statement
|
||||
if (typeWord && positiveWords.length)
|
||||
await Promise.all(positiveWords.map(w => apiLink(`/statements/${posStmt.id}/positive-words/${w.id}`)));
|
||||
|
||||
// Negative statement (Text + negative text, OR Word + negativeWords)
|
||||
let negStmtId = null;
|
||||
const hasNegText = typeText && negative.trim();
|
||||
const hasNegWords = typeWord && negativeWords.length > 0;
|
||||
|
||||
if (hasNegText || hasNegWords) {
|
||||
const negBody = { status: 'draft' };
|
||||
if (hasNegText)
|
||||
negBody[`negative_sentence_${lang}`] = withPlaceholders(negative, wordMap);
|
||||
const negStmt = await apiPost('/statements', negBody);
|
||||
negStmtId = negStmt.id;
|
||||
if (hasNegWords)
|
||||
await Promise.all(negativeWords.map(w => apiLink(`/statements/${negStmt.id}/negative-words/${w.id}`)));
|
||||
let posStmtId = null;
|
||||
if (type === 'text' || type === 'question') {
|
||||
const posBody = { status: 'draft' };
|
||||
if (positive.trim())
|
||||
posBody[`positive_sentence_${lang}`] = withPlaceholders(positive, wordMap);
|
||||
const s = await apiPost('/statements', posBody);
|
||||
posStmtId = s.id;
|
||||
} else if (type === 'yes_no') {
|
||||
const posBody = { status: 'draft' };
|
||||
if (yesNoAnswer !== null) posBody.answer = yesNoAnswer;
|
||||
const s = await apiPost('/statements', posBody);
|
||||
posStmtId = s.id;
|
||||
} else if (type === 'word' && positiveWords.length) {
|
||||
const s = await apiPost('/statements', { status: 'draft' });
|
||||
posStmtId = s.id;
|
||||
await Promise.all(positiveWords.map(w => apiLink(`/statements/${posStmtId}/positive-words/${w.id}`)));
|
||||
}
|
||||
|
||||
// Pair
|
||||
// Negative statement
|
||||
let negStmtId = null;
|
||||
if (type === 'question' && negative.trim()) {
|
||||
const negBody = { status: 'draft', [`negative_sentence_${lang}`]: withPlaceholders(negative, wordMap) };
|
||||
const s = await apiPost('/statements', negBody);
|
||||
negStmtId = s.id;
|
||||
} else if (type === 'word' && negativeWords.length) {
|
||||
const s = await apiPost('/statements', { status: 'draft' });
|
||||
negStmtId = s.id;
|
||||
await Promise.all(negativeWords.map(w => apiLink(`/statements/${negStmtId}/negative-words/${w.id}`)));
|
||||
}
|
||||
|
||||
// Create pair
|
||||
const pair = await apiPost('/pairs', {
|
||||
answer_type: answerTypes,
|
||||
answer_type: type,
|
||||
question_id: questionId,
|
||||
positive_statement_id: posStmt.id,
|
||||
positive_statement_id: posStmtId,
|
||||
negative_statement_id: negStmtId,
|
||||
status: 'draft',
|
||||
});
|
||||
|
||||
await apiLink(`/objects/${objectId}/pairs/${pair.id}`);
|
||||
|
||||
reset();
|
||||
onPairSaved({ ...pair, question: questionId ? { [`sentence_${lang}`]: question } : null,
|
||||
positive_statement: posStmt, negative_statement: null });
|
||||
// Carry-over: keep all text/word values, only reset type to prompt re-selection
|
||||
setType('');
|
||||
setYesNoAnswer(null);
|
||||
setSavedFlash(true);
|
||||
setTimeout(() => setSavedFlash(false), 2000);
|
||||
onPairSaved(pair);
|
||||
} catch (e) { alert('Fehler: ' + e.message); }
|
||||
finally { setSaving(false); }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Toggle button */}
|
||||
<button
|
||||
onClick={() => setShowForm(s => !s)}
|
||||
className="w-full py-1.5 text-xs font-medium rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white transition-colors"
|
||||
>
|
||||
{showForm ? '✕ Abbrechen' : '+ Add new pair'}
|
||||
{showForm ? '✕ Abbrechen' : '+ Pair hinzufügen'}
|
||||
</button>
|
||||
|
||||
{showForm && (
|
||||
<div className="border border-indigo-100 rounded-xl p-3 bg-indigo-50/30 space-y-3">
|
||||
|
||||
{/* ── Type checkboxes ── */}
|
||||
<div className="flex items-center gap-4">
|
||||
{[['typeText', typeText, setTypeText, 'Text'],
|
||||
['typeYesNo', typeYesNo, setTypeYesNo, 'Ja / Nein'],
|
||||
['typeWord', typeWord, setTypeWord, 'Wort'],
|
||||
].map(([key, val, setter, label]) => (
|
||||
<label key={key} className="flex items-center gap-1.5 text-xs font-medium text-slate-700 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={val}
|
||||
onChange={e => setter(e.target.checked)}
|
||||
className="w-3.5 h-3.5 accent-indigo-600"
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
{/* ── Saved flash ── */}
|
||||
{savedFlash && (
|
||||
<div className="text-xs text-green-700 bg-green-50 border border-green-200 rounded-lg px-3 py-1.5 font-medium">
|
||||
✓ Pair gespeichert — Texte übernommen
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Type dropdown ── */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">Typ</label>
|
||||
<select
|
||||
value={type}
|
||||
onChange={e => setType(e.target.value)}
|
||||
className="w-full border border-slate-300 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-400 bg-white"
|
||||
>
|
||||
<option value="">— Typ wählen —</option>
|
||||
{PAIR_TYPES.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label} — {opt.hint}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* ── Text section ── */}
|
||||
{typeText && (
|
||||
{/* ── Per-type fields ── */}
|
||||
{type && (
|
||||
<div className="space-y-2.5 pt-1 border-t border-indigo-100">
|
||||
{/* "Als Wort erstellen" banner */}
|
||||
{selection && (
|
||||
|
||||
{/* Word creation banner (text/question) */}
|
||||
{(type === 'text' || type === 'question') && selection && (
|
||||
<div className="flex items-center gap-2 bg-white border border-indigo-200 rounded-lg px-2.5 py-1.5">
|
||||
<span className="text-xs text-slate-600 flex-1 truncate">
|
||||
„<strong className="text-indigo-700">{selection}</strong>"
|
||||
@@ -346,35 +355,115 @@ function PairForm({ objectId, onPairSaved }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||
Positive Aussage <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<HighlightedTextarea value={positive} onChange={setPositive} wordMap={wordMap}
|
||||
rows={2} placeholder="Das ist ein Hund." onSelectionChange={setSelection} />
|
||||
</div>
|
||||
{/* TEXT — only positive statement */}
|
||||
{type === 'text' && (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||
Statement <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<HighlightedTextarea value={positive} onChange={setPositive} wordMap={wordMap}
|
||||
rows={2} placeholder="Das ist ein Hund." onSelectionChange={setSelection} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||
Frage <span className="text-slate-300">(optional)</span>
|
||||
</label>
|
||||
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap}
|
||||
rows={2} placeholder="Was ist das?" onSelectionChange={setSelection} />
|
||||
</div>
|
||||
{/* YES_NO — question + answer picker */}
|
||||
{type === 'yes_no' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||
Frage <span className="text-slate-400 font-normal normal-case">(optional)</span>
|
||||
</label>
|
||||
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap}
|
||||
rows={2} placeholder="Ist das ein Hund?" onSelectionChange={setSelection} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Antwort</p>
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
[null, '— Offen', 'bg-slate-100 text-slate-600 border-slate-200'],
|
||||
[true, '✓ Ja', 'bg-green-50 text-green-700 border-green-300'],
|
||||
[false, '✗ Nein', 'bg-red-50 text-red-600 border-red-300'],
|
||||
].map(([val, label, cls]) => (
|
||||
<button key={String(val)} onClick={() => setYesNoAnswer(val)}
|
||||
className={`flex-1 py-1.5 text-xs font-medium rounded-lg border transition-all
|
||||
${yesNoAnswer === val
|
||||
? `${cls} ring-2 ring-offset-1 ring-indigo-400`
|
||||
: 'bg-white text-slate-500 border-slate-200 hover:border-slate-300'}`}
|
||||
>{label}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||
Negative Aussage
|
||||
<span className="text-slate-300 ml-1">(optional — braucht Frage + Positiv)</span>
|
||||
</label>
|
||||
<HighlightedTextarea value={negative} onChange={setNegative} wordMap={wordMap}
|
||||
rows={2} placeholder="Das ist keine Katze." onSelectionChange={setSelection} />
|
||||
{negative.trim() && !negativeOk && (
|
||||
<p className="text-xs text-red-500 mt-0.5">Frage + Positive Aussage erforderlich.</p>
|
||||
)}
|
||||
</div>
|
||||
{/* QUESTION — question + positive + negative */}
|
||||
{type === 'question' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||
Frage <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap}
|
||||
rows={2} placeholder="Was ist das?" onSelectionChange={setSelection} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||
Positive Aussage <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<HighlightedTextarea value={positive} onChange={setPositive} wordMap={wordMap}
|
||||
rows={2} placeholder="Das ist ein Hund." onSelectionChange={setSelection} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||
Negative Aussage <span className="text-slate-400 font-normal normal-case">(optional)</span>
|
||||
</label>
|
||||
<HighlightedTextarea value={negative} onChange={setNegative} wordMap={wordMap}
|
||||
rows={2} placeholder="Das ist keine Katze." onSelectionChange={setSelection} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{Object.keys(wordMap).length > 0 && (
|
||||
{/* WORD — question + positive/negative word pickers */}
|
||||
{type === 'word' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
|
||||
Frage <span className="text-slate-400 font-normal normal-case">(optional)</span>
|
||||
</label>
|
||||
<HighlightedTextarea value={question} onChange={setQuestion} wordMap={wordMap}
|
||||
rows={2} placeholder="Was ist das?" onSelectionChange={setSelection} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Positive Wörter</p>
|
||||
<div className="flex flex-wrap gap-1 mb-1.5">
|
||||
{positiveWords.map(w => (
|
||||
<WordTag key={w.id} word={w} onRemove={id => setPositiveWords(prev => prev.filter(x => x.id !== id))} />
|
||||
))}
|
||||
</div>
|
||||
<WordSearch
|
||||
existingIds={positiveWords.map(w => w.id)}
|
||||
onAdd={w => setPositiveWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])}
|
||||
placeholder="Positives Wort suchen…"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Negative Wörter</p>
|
||||
<div className="flex flex-wrap gap-1 mb-1.5">
|
||||
{negativeWords.map(w => (
|
||||
<WordTag key={w.id} word={w} onRemove={id => setNegativeWords(prev => prev.filter(x => x.id !== id))} />
|
||||
))}
|
||||
</div>
|
||||
<WordSearch
|
||||
existingIds={negativeWords.map(w => w.id)}
|
||||
onAdd={w => setNegativeWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])}
|
||||
placeholder="Negatives Wort suchen…"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Detected words indicator */}
|
||||
{(type === 'text' || type === 'question') && Object.keys(wordMap).length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<span className="text-xs text-slate-400 w-full">Erkannte Wörter:</span>
|
||||
{Object.entries(wordMap).map(([title, w]) => (
|
||||
@@ -385,67 +474,14 @@ function PairForm({ objectId, onPairSaved }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Ja/Nein section ── */}
|
||||
{typeYesNo && (
|
||||
<div className="pt-2 border-t border-indigo-100">
|
||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">Antwort</p>
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
[null, '— Offen', 'bg-slate-100 text-slate-600 border-slate-200'],
|
||||
[true, '✓ Ja', 'bg-green-50 text-green-700 border-green-300'],
|
||||
[false, '✗ Nein', 'bg-red-50 text-red-600 border-red-300'],
|
||||
].map(([val, label, cls]) => (
|
||||
<button key={String(val)}
|
||||
onClick={() => setYesNoAnswer(val)}
|
||||
className={`flex-1 py-1.5 text-xs font-medium rounded-lg border transition-all
|
||||
${yesNoAnswer === val ? `${cls} ring-2 ring-offset-1 ring-indigo-400` : 'bg-white text-slate-500 border-slate-200 hover:border-slate-300'}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Wort section ── */}
|
||||
{typeWord && (
|
||||
<div className="pt-2 border-t border-indigo-100 space-y-2.5">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Positive Wörter</p>
|
||||
<div className="flex flex-wrap gap-1 mb-1.5">
|
||||
{positiveWords.map(w => (
|
||||
<WordTag key={w.id} word={w} onRemove={id => setPositiveWords(prev => prev.filter(x => x.id !== id))} />
|
||||
))}
|
||||
</div>
|
||||
<WordSearch
|
||||
existingIds={positiveWords.map(w => w.id)}
|
||||
onAdd={w => setPositiveWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])}
|
||||
placeholder="Positives Wort suchen…"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1.5">Negative Wörter</p>
|
||||
<div className="flex flex-wrap gap-1 mb-1.5">
|
||||
{negativeWords.map(w => (
|
||||
<WordTag key={w.id} word={w} onRemove={id => setNegativeWords(prev => prev.filter(x => x.id !== id))} />
|
||||
))}
|
||||
</div>
|
||||
<WordSearch
|
||||
existingIds={negativeWords.map(w => w.id)}
|
||||
onAdd={w => setNegativeWords(prev => prev.some(x => x.id === w.id) ? prev : [...prev, w])}
|
||||
placeholder="Negatives Wort suchen…"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Save ── */}
|
||||
<div className="pt-2 border-t border-indigo-100 space-y-1.5">
|
||||
{!canSave && <p className="text-xs text-amber-600">{validationHint()}</p>}
|
||||
{type && !canSave() && (
|
||||
<p className="text-xs text-amber-600">{validationHint()}</p>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving || !canSave}
|
||||
disabled={saving || !canSave()}
|
||||
className="w-full py-2 text-xs font-medium rounded-lg bg-green-600 hover:bg-green-700 disabled:opacity-40 text-white transition-colors"
|
||||
>
|
||||
{saving ? 'Speichern…' : '✓ Pair speichern'}
|
||||
@@ -527,6 +563,7 @@ function PairsPanel({ selectedObject, objectPairs, loadingPairs, onPairSaved })
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user