Fase 3 (isla): recuperar ítems (restore_items) en React/TSX, flujo de 2 pasos
restore_items_view ahora responde JSON cuando la petición es AJAX (X-Requested-With):
GET con character_name -> {items}; POST recover_id -> {success, message}. Se conserva
el flujo legacy (redirect+messages) para no-AJAX. RestoreItemsForm.tsx: paso 1 elegir
personaje y buscar ítems borrados (SOAP), paso 2 elegir ítem y recuperarlo; refresca la
lista tras recuperar. La vista pasa character_names por json_script.
Verificado: check OK, render de la isla, redirige a login sin sesión.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
interface DeletedItem {
|
||||
recover_id: string
|
||||
item_name: string
|
||||
item_id: string
|
||||
count: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
csrfToken: string
|
||||
characters: string[]
|
||||
}
|
||||
|
||||
export function RestoreItemsForm({ url, csrfToken, characters }: Props) {
|
||||
const [character, setCharacter] = useState('')
|
||||
const [items, setItems] = useState<DeletedItem[] | null>(null)
|
||||
const [recoverId, setRecoverId] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null)
|
||||
|
||||
async function doSearch(char: string, keepMessage = false) {
|
||||
if (!char) return
|
||||
setBusy(true)
|
||||
if (!keepMessage) setMessage(null)
|
||||
setItems(null)
|
||||
setRecoverId('')
|
||||
try {
|
||||
const r = await fetch(`${url}?character_name=${encodeURIComponent(char)}`, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
const d: { items?: DeletedItem[]; error?: string } = await r.json()
|
||||
if (d.error) setMessage({ ok: false, html: `<span class="red-form-response">${d.error}</span>` })
|
||||
else setItems(d.items || [])
|
||||
} catch {
|
||||
setMessage({ ok: false, html: '<span class="red-form-response">Error al buscar los ítems.</span>' })
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
function search(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (busy) return
|
||||
void doSearch(character)
|
||||
}
|
||||
|
||||
async function recover(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!recoverId || busy) return
|
||||
setBusy(true)
|
||||
setMessage(null)
|
||||
const body = new URLSearchParams()
|
||||
body.set('recover_id', recoverId)
|
||||
body.set('character_name', character)
|
||||
body.set('csrfmiddlewaretoken', csrfToken)
|
||||
try {
|
||||
const r = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': csrfToken,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: body.toString(),
|
||||
})
|
||||
const d: { success?: boolean; message?: string } = await r.json()
|
||||
setMessage({ ok: !!d.success, html: d.message || '' })
|
||||
if (d.success) {
|
||||
// refrescar la lista de ítems tras recuperar uno (conservando el mensaje)
|
||||
void doSearch(character, true)
|
||||
}
|
||||
} catch {
|
||||
setMessage({ ok: false, html: '<span class="red-form-response">Error al recuperar el ítem.</span>' })
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={search}>
|
||||
<select value={character} onChange={(e) => setCharacter(e.target.value)} required>
|
||||
<option value="" disabled>Selecciona un personaje</option>
|
||||
{characters.map((c) => (
|
||||
<option value={c} key={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="submit" disabled={busy || !character}>
|
||||
{busy ? 'Buscando…' : 'Buscar Ítems Eliminados'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{items !== null && (
|
||||
items.length > 0 ? (
|
||||
<form onSubmit={recover}>
|
||||
<input type="hidden" name="character_name" value={character} />
|
||||
<select value={recoverId} onChange={(e) => setRecoverId(e.target.value)} required>
|
||||
<option value="" disabled>Selecciona un ítem</option>
|
||||
{items.map((it) => (
|
||||
<option value={it.recover_id} key={it.recover_id}>
|
||||
{it.item_name} (x{it.count})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="submit" disabled={busy || !recoverId}>
|
||||
{busy ? 'Recuperando…' : 'Recuperar Ítem'}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<p>No hay ítems eliminados para este personaje.</p>
|
||||
)
|
||||
)}
|
||||
|
||||
<div className="alert-message" style={{ display: message ? 'block' : 'none' }}>
|
||||
{message && <span dangerouslySetInnerHTML={{ __html: message.html }} />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { RestoreItemsForm } from '../components/RestoreItemsForm'
|
||||
|
||||
const el = document.getElementById('restore-items-app')
|
||||
if (el) {
|
||||
const dEl = document.getElementById('restore-items-data')
|
||||
const characters: string[] = dEl ? JSON.parse(dEl.textContent || '[]') : []
|
||||
createRoot(el).render(
|
||||
<StrictMode>
|
||||
<RestoreItemsForm url={el.dataset.url || ''} csrfToken={el.dataset.csrf || ''} characters={characters} />
|
||||
</StrictMode>,
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export default defineConfig({
|
||||
vote_points: resolve(__dirname, 'src/entries/vote_points.tsx'),
|
||||
history_table: resolve(__dirname, 'src/entries/history_table.tsx'),
|
||||
restore_character: resolve(__dirname, 'src/entries/restore_character.tsx'),
|
||||
restore_items: resolve(__dirname, 'src/entries/restore_items.tsx'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,35 +17,14 @@
|
||||
<p>Se pueden consultar ítems borrados en un mismo personaje cada 8 horas.</p>
|
||||
</fieldset>
|
||||
|
||||
<!-- Selección de personaje -->
|
||||
<form method="GET" action="{% url 'restore-items' %}">
|
||||
<label for="character_name">Selecciona un personaje:</label>
|
||||
<select name="character_name" id="character_name" required>
|
||||
<option value="" disabled selected>Selecciona un personaje</option>
|
||||
{% for character in characters %}
|
||||
<option value="{{ character }}" {% if character == selected_character %}selected{% endif %}>{{ character }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit">Buscar Ítems Eliminados</button>
|
||||
</form>
|
||||
|
||||
{% if deleted_items %}
|
||||
<form method="POST" action="{% url 'restore-items' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="character_name" value="{{ selected_character }}">
|
||||
<label for="recover_id">Selecciona un ítem para restaurar:</label>
|
||||
<select name="recover_id" id="recover_id" required>
|
||||
{% for item in deleted_items %}
|
||||
<option value="{{ item.recover_id }}">
|
||||
Item: {{ item.item_name }} ({{ item.item_id }}):{{ item.count }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit">Recuperar Ítem</button>
|
||||
</form>
|
||||
{% elif selected_character %}
|
||||
<p>No hay ítems eliminados disponibles para restaurar.</p>
|
||||
{% endif %}
|
||||
{% load django_vite %}
|
||||
<!-- Isla React (Vite): recuperar ítems borrados (2 pasos vía AJAX:
|
||||
buscar ítems del personaje -> recuperar el elegido). -->
|
||||
<div id="restore-items-app"
|
||||
data-csrf="{{ csrf_token }}"
|
||||
data-url="{% url 'restore-items' %}"></div>
|
||||
{{ character_names|json_script:"restore-items-data" }}
|
||||
{% vite_asset 'src/entries/restore_items.tsx' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -204,6 +204,9 @@ def restore_items_view(request):
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error al obtener los personajes: {str(e)}")
|
||||
|
||||
# La isla React usa AJAX (X-Requested-With); en ese caso respondemos JSON.
|
||||
is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||
|
||||
if request.method == "GET" and "character_name" in request.GET:
|
||||
selected_character = request.GET.get("character_name")
|
||||
|
||||
@@ -229,13 +232,20 @@ def restore_items_view(request):
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if is_ajax:
|
||||
return JsonResponse({'error': f'Error al obtener los ítems eliminados: {str(e)}'})
|
||||
messages.error(request, f"Error al obtener los ítems eliminados: {str(e)}")
|
||||
|
||||
if is_ajax:
|
||||
return JsonResponse({'items': deleted_items})
|
||||
|
||||
if request.method == "POST":
|
||||
recover_id = request.POST.get("recover_id")
|
||||
selected_character = request.POST.get("character_name")
|
||||
|
||||
if not recover_id or not selected_character:
|
||||
if is_ajax:
|
||||
return JsonResponse({'success': False, 'message': '<span class="red-form-response">Selecciona un personaje y un ítem para restaurar.</span>'})
|
||||
messages.error(request, "Selecciona un personaje y un ítem para restaurar.")
|
||||
return redirect('restore-items')
|
||||
|
||||
@@ -243,20 +253,26 @@ def restore_items_view(request):
|
||||
# Ejecutar comando SOAP para restaurar el ítem
|
||||
command = f".item restore {recover_id} {selected_character}"
|
||||
response = execute_soap_command(command)
|
||||
|
||||
if "success" in response.lower():
|
||||
messages.success(request, "Ítem restaurado exitosamente.")
|
||||
else:
|
||||
messages.error(request, "No se pudo restaurar el ítem.")
|
||||
ok = "success" in response.lower()
|
||||
except Exception as e:
|
||||
if is_ajax:
|
||||
return JsonResponse({'success': False, 'message': f'<span class="red-form-response">Error al restaurar el ítem: {str(e)}</span>'})
|
||||
messages.error(request, f"Error al restaurar el ítem: {str(e)}")
|
||||
return redirect('restore-items')
|
||||
|
||||
if is_ajax:
|
||||
msg = ('<span class="ok-form-response">Ítem restaurado exitosamente.</span>' if ok
|
||||
else '<span class="red-form-response">No se pudo restaurar el ítem.</span>')
|
||||
return JsonResponse({'success': ok, 'message': msg})
|
||||
|
||||
messages.success(request, "Ítem restaurado exitosamente.") if ok else messages.error(request, "No se pudo restaurar el ítem.")
|
||||
return redirect('restore-items')
|
||||
|
||||
return render(request, 'account/restore_items.html', {
|
||||
'characters': characters,
|
||||
'deleted_items': deleted_items,
|
||||
'selected_character': selected_character
|
||||
'selected_character': selected_character,
|
||||
'character_names': characters,
|
||||
})
|
||||
def quest_character_view(request):
|
||||
username = request.session.get('username')
|
||||
|
||||
Reference in New Issue
Block a user