fix: resolve new session creation state race condition and backdrop closure in AppWPP
This commit is contained in:
@@ -74,9 +74,12 @@ export default function AppWPP() {
|
||||
}, []);
|
||||
|
||||
// Salva no localStorage sempre que `servers` mudar
|
||||
const updateServersState = (updatedList) => {
|
||||
setServers(updatedList);
|
||||
saveServers(updatedList);
|
||||
const updateServersState = (updater) => {
|
||||
setServers((prevServers) => {
|
||||
const next = typeof updater === 'function' ? updater(prevServers) : updater;
|
||||
saveServers(next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const showToast = (message, type = 'info') => {
|
||||
@@ -91,79 +94,97 @@ export default function AppWPP() {
|
||||
|
||||
// Servidor CRUD
|
||||
const handleSaveServer = (serverData) => {
|
||||
updateServersState((prevServers) => {
|
||||
const exists = prevServers.some((s) => s.id === serverData.id);
|
||||
let updated;
|
||||
const exists = servers.some((s) => s.id === serverData.id);
|
||||
if (exists) {
|
||||
updated = servers.map((s) => (s.id === serverData.id ? { ...s, ...serverData } : s));
|
||||
showToast(`Servidor "${serverData.name}" atualizado!`, 'success');
|
||||
updated = prevServers.map((s) => (s.id === serverData.id ? { ...s, ...serverData } : s));
|
||||
} else {
|
||||
updated = [...servers, serverData];
|
||||
updated = [...prevServers, serverData];
|
||||
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 = () => {
|
||||
if (!activeServer) return;
|
||||
if (!activeServerId) return;
|
||||
const currentName = activeServer?.name || 'Servidor';
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
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.',
|
||||
confirmText: 'Remover Servidor',
|
||||
type: 'danger',
|
||||
onConfirm: () => {
|
||||
const updated = servers.filter((s) => s.id !== activeServer.id);
|
||||
updateServersState(updated);
|
||||
updateServersState((prevServers) => {
|
||||
const updated = prevServers.filter((s) => s.id !== activeServerId);
|
||||
setActiveServerId(updated.length > 0 ? updated[0].id : null);
|
||||
showToast(`Servidor "${activeServer.name}" removido.`, 'info');
|
||||
return updated;
|
||||
});
|
||||
showToast(`Servidor "${currentName}" removido.`, 'info');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Sessão CRUD
|
||||
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) {
|
||||
updatedSessions = updatedSessions.map((s) => (s.name === originalName ? sessionData : s));
|
||||
showToast(`Sessão "${sessionData.name}" atualizada!`, 'success');
|
||||
updatedSessions = currentSessions.map((s) => (s.name === originalName ? sessionData : s));
|
||||
} 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');
|
||||
return;
|
||||
}
|
||||
updatedSessions.push(sessionData);
|
||||
showToast(`Sessão "${sessionData.name}" criada!`, 'success');
|
||||
}
|
||||
|
||||
const updatedServer = { ...activeServer, sessions: updatedSessions };
|
||||
const updatedServers = servers.map((s) => (s.id === activeServer.id ? updatedServer : s));
|
||||
updateServersState(updatedServers);
|
||||
if (originalName) {
|
||||
showToast(`Sessão "${sessionData.name}" atualizada!`, 'success');
|
||||
} else {
|
||||
showToast(`Sessão "${sessionData.name}" criada com sucesso!`, 'success');
|
||||
|
||||
// Se for nova sessão, tenta gerar token e iniciar
|
||||
if (!originalName) {
|
||||
// Se for nova sessão, tenta gerar token e iniciar no servidor WPP
|
||||
const currentServer = servers.find((s) => s.id === activeServerId) || activeServer;
|
||||
if (currentServer) {
|
||||
if (!sessionData.token) {
|
||||
try {
|
||||
const token = await generateToken(activeServer, sessionData.name);
|
||||
const token = await generateToken(currentServer, sessionData.name);
|
||||
if (token) {
|
||||
sessionData.token = token;
|
||||
updateServersState(
|
||||
servers.map((s) =>
|
||||
s.id === activeServer.id
|
||||
? { ...s, sessions: updatedSessions.map((x) => (x.name === sessionData.name ? { ...x, token } : x)) }
|
||||
: s
|
||||
)
|
||||
);
|
||||
updateSessionProperty(sessionData.name, { token });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Erro ao gerar token automático:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
handleStartSession(sessionData.name, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSession = (sessionName) => {
|
||||
if (!activeServer) return;
|
||||
if (!activeServerId) return;
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
title: 'Excluir Sessão',
|
||||
@@ -172,11 +193,13 @@ export default function AppWPP() {
|
||||
confirmText: 'Excluir Sessão',
|
||||
type: 'danger',
|
||||
onConfirm: () => {
|
||||
const updatedSessions = activeServer.sessions.filter((s) => s.name !== sessionName);
|
||||
const updatedServers = servers.map((s) =>
|
||||
s.id === activeServer.id ? { ...s, sessions: updatedSessions } : s
|
||||
updateServersState((prevServers) =>
|
||||
prevServers.map((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');
|
||||
}
|
||||
});
|
||||
@@ -284,14 +307,18 @@ export default function AppWPP() {
|
||||
};
|
||||
|
||||
const updateSessionProperty = (sessionName, props) => {
|
||||
if (!activeServer) return;
|
||||
const updatedSessions = activeServer.sessions.map((s) =>
|
||||
s.name === sessionName ? { ...s, ...props } : s
|
||||
if (!activeServerId) return;
|
||||
updateServersState((prevServers) =>
|
||||
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
|
||||
@@ -469,8 +496,20 @@ export default function AppWPP() {
|
||||
<p>
|
||||
{searchQuery
|
||||
? '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>
|
||||
<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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
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 [waitQrCode, setWaitQrCode] = useState(false);
|
||||
const [proxyUrl, setProxyUrl] = useState('');
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionToEdit) {
|
||||
@@ -22,19 +23,23 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
|
||||
setWaitQrCode(false);
|
||||
setProxyUrl('');
|
||||
}
|
||||
if (isOpen) {
|
||||
setTimeout(() => inputRef.current?.focus(), 100);
|
||||
}
|
||||
}, [sessionToEdit, isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) {
|
||||
const cleanName = name.trim().replace(/\s+/g, '_');
|
||||
if (!cleanName) {
|
||||
alert('Informe o nome da sessão.');
|
||||
return;
|
||||
}
|
||||
|
||||
onSave({
|
||||
name: name.trim(),
|
||||
name: cleanName,
|
||||
token: token.trim(),
|
||||
webhook: webhook.trim(),
|
||||
waitQrCode,
|
||||
@@ -47,14 +52,14 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal-dialog" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '520px' }}>
|
||||
<div className="modal-backdrop">
|
||||
<div className="modal-dialog" style={{ maxWidth: '520px' }}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title-box">
|
||||
<MessageSquare size={20} className="text-emerald" />
|
||||
<h3 className="modal-title">{sessionToEdit ? 'Editar Sessão' : 'Nova Sessão WhatsApp'}</h3>
|
||||
</div>
|
||||
<button type="button" className="modal-close-btn" onClick={onClose}>
|
||||
<button type="button" className="modal-close-btn" onClick={onClose} title="Fechar">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -64,13 +69,17 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Nome da Sessão (Identificador)</label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="Ex: atendimento_01, admin, clinica"
|
||||
placeholder="Ex: atendimento_01, admin, clinica (sem espaços)"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
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 className="form-group">
|
||||
@@ -78,7 +87,7 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="Token JWT gerado pelo servidor"
|
||||
placeholder="Token JWT (gerado automaticamente se vazio)"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user