fix: resolve new session creation state race condition and backdrop closure in AppWPP

This commit is contained in:
2026-10-15 14:27:22 -03:00
parent f01101cab0
commit d3b7dcbc1e
2 changed files with 118 additions and 70 deletions
+89 -50
View File
@@ -74,9 +74,12 @@ export default function AppWPP() {
}, []); }, []);
// Salva no localStorage sempre que `servers` mudar // Salva no localStorage sempre que `servers` mudar
const updateServersState = (updatedList) => { const updateServersState = (updater) => {
setServers(updatedList); setServers((prevServers) => {
saveServers(updatedList); const next = typeof updater === 'function' ? updater(prevServers) : updater;
saveServers(next);
return next;
});
}; };
const showToast = (message, type = 'info') => { const showToast = (message, type = 'info') => {
@@ -91,79 +94,97 @@ export default function AppWPP() {
// Servidor CRUD // Servidor CRUD
const handleSaveServer = (serverData) => { const handleSaveServer = (serverData) => {
updateServersState((prevServers) => {
const exists = prevServers.some((s) => s.id === serverData.id);
let updated; let updated;
const exists = servers.some((s) => s.id === serverData.id);
if (exists) { if (exists) {
updated = servers.map((s) => (s.id === serverData.id ? { ...s, ...serverData } : s)); updated = prevServers.map((s) => (s.id === serverData.id ? { ...s, ...serverData } : s));
showToast(`Servidor "${serverData.name}" atualizado!`, 'success');
} else { } else {
updated = [...servers, serverData]; updated = [...prevServers, serverData];
setActiveServerId(serverData.id); setActiveServerId(serverData.id);
showToast(`Servidor "${serverData.name}" adicionado com sucesso!`, 'success');
} }
updateServersState(updated); return updated;
});
showToast(`Servidor "${serverData.name}" salvo com sucesso!`, 'success');
}; };
const handleRemoveServer = () => { const handleRemoveServer = () => {
if (!activeServer) return; if (!activeServerId) return;
const currentName = activeServer?.name || 'Servidor';
setConfirmModal({ setConfirmModal({
isOpen: true, isOpen: true,
title: 'Remover Servidor', title: 'Remover Servidor',
message: `Tem certeza que deseja remover o servidor "${activeServer.name}"?`, message: `Tem certeza que deseja remover o servidor "${currentName}"?`,
description: 'Todas as sessões cadastradas para este servidor deixarão de ser exibidas na listagem.', description: 'Todas as sessões cadastradas para este servidor deixarão de ser exibidas na listagem.',
confirmText: 'Remover Servidor', confirmText: 'Remover Servidor',
type: 'danger', type: 'danger',
onConfirm: () => { onConfirm: () => {
const updated = servers.filter((s) => s.id !== activeServer.id); updateServersState((prevServers) => {
updateServersState(updated); const updated = prevServers.filter((s) => s.id !== activeServerId);
setActiveServerId(updated.length > 0 ? updated[0].id : null); setActiveServerId(updated.length > 0 ? updated[0].id : null);
showToast(`Servidor "${activeServer.name}" removido.`, 'info'); return updated;
});
showToast(`Servidor "${currentName}" removido.`, 'info');
} }
}); });
}; };
// Sessão CRUD // Sessão CRUD
const handleSaveSession = async (sessionData, originalName) => { const handleSaveSession = async (sessionData, originalName) => {
if (!activeServer) return; if (!activeServerId) return;
let isDuplicate = false;
updateServersState((prevServers) => {
const srv = prevServers.find((s) => s.id === activeServerId);
if (!srv) return prevServers;
const currentSessions = [...(srv.sessions || [])];
let updatedSessions;
let updatedSessions = [...(activeServer.sessions || [])];
if (originalName) { if (originalName) {
updatedSessions = updatedSessions.map((s) => (s.name === originalName ? sessionData : s)); updatedSessions = currentSessions.map((s) => (s.name === originalName ? sessionData : s));
showToast(`Sessão "${sessionData.name}" atualizada!`, 'success');
} else { } else {
if (updatedSessions.some((s) => s.name === sessionData.name)) { if (currentSessions.some((s) => s.name === sessionData.name)) {
isDuplicate = true;
return prevServers;
}
updatedSessions = [...currentSessions, sessionData];
}
return prevServers.map((s) => (s.id === activeServerId ? { ...s, sessions: updatedSessions } : s));
});
if (isDuplicate) {
showToast('Já existe uma sessão com este nome.', 'error'); showToast('Já existe uma sessão com este nome.', 'error');
return; return;
} }
updatedSessions.push(sessionData);
showToast(`Sessão "${sessionData.name}" criada!`, 'success');
}
const updatedServer = { ...activeServer, sessions: updatedSessions }; if (originalName) {
const updatedServers = servers.map((s) => (s.id === activeServer.id ? updatedServer : s)); showToast(`Sessão "${sessionData.name}" atualizada!`, 'success');
updateServersState(updatedServers); } else {
showToast(`Sessão "${sessionData.name}" criada com sucesso!`, 'success');
// Se for nova sessão, tenta gerar token e iniciar // Se for nova sessão, tenta gerar token e iniciar no servidor WPP
if (!originalName) { const currentServer = servers.find((s) => s.id === activeServerId) || activeServer;
if (currentServer) {
if (!sessionData.token) {
try { try {
const token = await generateToken(activeServer, sessionData.name); const token = await generateToken(currentServer, sessionData.name);
if (token) { if (token) {
sessionData.token = token; updateSessionProperty(sessionData.name, { token });
updateServersState( }
servers.map((s) => } catch (e) {
s.id === activeServer.id console.warn('Erro ao gerar token automático:', e);
? { ...s, sessions: updatedSessions.map((x) => (x.name === sessionData.name ? { ...x, token } : x)) } }
: s
)
);
} }
} catch (e) {}
handleStartSession(sessionData.name, true); handleStartSession(sessionData.name, true);
} }
}
}; };
const handleRemoveSession = (sessionName) => { const handleRemoveSession = (sessionName) => {
if (!activeServer) return; if (!activeServerId) return;
setConfirmModal({ setConfirmModal({
isOpen: true, isOpen: true,
title: 'Excluir Sessão', title: 'Excluir Sessão',
@@ -172,11 +193,13 @@ export default function AppWPP() {
confirmText: 'Excluir Sessão', confirmText: 'Excluir Sessão',
type: 'danger', type: 'danger',
onConfirm: () => { onConfirm: () => {
const updatedSessions = activeServer.sessions.filter((s) => s.name !== sessionName); updateServersState((prevServers) =>
const updatedServers = servers.map((s) => prevServers.map((s) => {
s.id === activeServer.id ? { ...s, sessions: updatedSessions } : s if (s.id !== activeServerId) return s;
const updatedSessions = (s.sessions || []).filter((sess) => sess.name !== sessionName);
return { ...s, sessions: updatedSessions };
})
); );
updateServersState(updatedServers);
showToast(`Sessão "${sessionName}" removida.`, 'info'); showToast(`Sessão "${sessionName}" removida.`, 'info');
} }
}); });
@@ -284,14 +307,18 @@ export default function AppWPP() {
}; };
const updateSessionProperty = (sessionName, props) => { const updateSessionProperty = (sessionName, props) => {
if (!activeServer) return; if (!activeServerId) return;
const updatedSessions = activeServer.sessions.map((s) => updateServersState((prevServers) =>
s.name === sessionName ? { ...s, ...props } : s prevServers.map((srv) => {
if (srv.id !== activeServerId) return srv;
const currentSessions = srv.sessions || [];
const exists = currentSessions.some((s) => s.name === sessionName);
const updatedSessions = exists
? currentSessions.map((s) => (s.name === sessionName ? { ...s, ...props } : s))
: [...currentSessions, { name: sessionName, ...props }];
return { ...srv, sessions: updatedSessions };
})
); );
const updatedServers = servers.map((srv) =>
srv.id === activeServer.id ? { ...srv, sessions: updatedSessions } : srv
);
updateServersState(updatedServers);
}; };
// Filtro de sessões // Filtro de sessões
@@ -469,8 +496,20 @@ export default function AppWPP() {
<p> <p>
{searchQuery {searchQuery
? 'Nenhuma sessão corresponde à busca informada.' ? 'Nenhuma sessão corresponde à busca informada.'
: 'Clique em "+ Nova Sessão" acima para cadastrar sua primeira sessão do WhatsApp.'} : 'Clique no botão abaixo para cadastrar uma nova sessão do WhatsApp.'}
</p> </p>
<button
type="button"
className="btn-new-session"
style={{ marginTop: '0.75rem' }}
onClick={() => {
setSessionToEdit(null);
setIsSessionModalOpen(true);
}}
>
<Plus size={16} />
<span>Nova Sessão</span>
</button>
</div> </div>
)} )}
</div> </div>
+17 -8
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { X, MessageSquare, Key, Globe, Shield } from 'lucide-react'; import { X, MessageSquare, Key, Globe, Shield } from 'lucide-react';
export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit }) { export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit }) {
@@ -7,6 +7,7 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
const [webhook, setWebhook] = useState(''); const [webhook, setWebhook] = useState('');
const [waitQrCode, setWaitQrCode] = useState(false); const [waitQrCode, setWaitQrCode] = useState(false);
const [proxyUrl, setProxyUrl] = useState(''); const [proxyUrl, setProxyUrl] = useState('');
const inputRef = useRef(null);
useEffect(() => { useEffect(() => {
if (sessionToEdit) { if (sessionToEdit) {
@@ -22,19 +23,23 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
setWaitQrCode(false); setWaitQrCode(false);
setProxyUrl(''); setProxyUrl('');
} }
if (isOpen) {
setTimeout(() => inputRef.current?.focus(), 100);
}
}, [sessionToEdit, isOpen]); }, [sessionToEdit, isOpen]);
if (!isOpen) return null; if (!isOpen) return null;
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
if (!name.trim()) { const cleanName = name.trim().replace(/\s+/g, '_');
if (!cleanName) {
alert('Informe o nome da sessão.'); alert('Informe o nome da sessão.');
return; return;
} }
onSave({ onSave({
name: name.trim(), name: cleanName,
token: token.trim(), token: token.trim(),
webhook: webhook.trim(), webhook: webhook.trim(),
waitQrCode, waitQrCode,
@@ -47,14 +52,14 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
}; };
return ( return (
<div className="modal-backdrop" onClick={onClose}> <div className="modal-backdrop">
<div className="modal-dialog" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '520px' }}> <div className="modal-dialog" style={{ maxWidth: '520px' }}>
<div className="modal-header"> <div className="modal-header">
<div className="modal-title-box"> <div className="modal-title-box">
<MessageSquare size={20} className="text-emerald" /> <MessageSquare size={20} className="text-emerald" />
<h3 className="modal-title">{sessionToEdit ? 'Editar Sessão' : 'Nova Sessão WhatsApp'}</h3> <h3 className="modal-title">{sessionToEdit ? 'Editar Sessão' : 'Nova Sessão WhatsApp'}</h3>
</div> </div>
<button type="button" className="modal-close-btn" onClick={onClose}> <button type="button" className="modal-close-btn" onClick={onClose} title="Fechar">
<X size={20} /> <X size={20} />
</button> </button>
</div> </div>
@@ -64,13 +69,17 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
<div className="form-group"> <div className="form-group">
<label className="wpp-form-label">Nome da Sessão (Identificador)</label> <label className="wpp-form-label">Nome da Sessão (Identificador)</label>
<input <input
ref={inputRef}
type="text" type="text"
className="password-input" className="password-input"
placeholder="Ex: atendimento_01, admin, clinica" placeholder="Ex: atendimento_01, admin, clinica (sem espaços)"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
required required
/> />
<span style={{ fontSize: '0.75rem', color: '#94a3b8' }}>
Nome de identificação único para a rota da API (espaços serão convertidos em "_").
</span>
</div> </div>
<div className="form-group"> <div className="form-group">
@@ -78,7 +87,7 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
<input <input
type="text" type="text"
className="password-input" className="password-input"
placeholder="Token JWT gerado pelo servidor" placeholder="Token JWT (gerado automaticamente se vazio)"
value={token} value={token}
onChange={(e) => setToken(e.target.value)} onChange={(e) => setToken(e.target.value)}
/> />