fix: resolve new session creation state race condition and backdrop closure in AppWPP
This commit is contained in:
+101
-62
@@ -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) => {
|
||||||
let updated;
|
updateServersState((prevServers) => {
|
||||||
const exists = servers.some((s) => s.id === serverData.id);
|
const exists = prevServers.some((s) => s.id === serverData.id);
|
||||||
if (exists) {
|
let updated;
|
||||||
updated = servers.map((s) => (s.id === serverData.id ? { ...s, ...serverData } : s));
|
if (exists) {
|
||||||
showToast(`Servidor "${serverData.name}" atualizado!`, 'success');
|
updated = prevServers.map((s) => (s.id === serverData.id ? { ...s, ...serverData } : s));
|
||||||
} else {
|
} else {
|
||||||
updated = [...servers, serverData];
|
updated = [...prevServers, serverData];
|
||||||
setActiveServerId(serverData.id);
|
setActiveServerId(serverData.id);
|
||||||
showToast(`Servidor "${serverData.name}" adicionado com sucesso!`, 'success');
|
}
|
||||||
}
|
return updated;
|
||||||
updateServersState(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 updatedSessions = [...(activeServer.sessions || [])];
|
let isDuplicate = false;
|
||||||
if (originalName) {
|
|
||||||
updatedSessions = updatedSessions.map((s) => (s.name === originalName ? sessionData : s));
|
updateServersState((prevServers) => {
|
||||||
showToast(`Sessão "${sessionData.name}" atualizada!`, 'success');
|
const srv = prevServers.find((s) => s.id === activeServerId);
|
||||||
} else {
|
if (!srv) return prevServers;
|
||||||
if (updatedSessions.some((s) => s.name === sessionData.name)) {
|
|
||||||
showToast('Já existe uma sessão com este nome.', 'error');
|
const currentSessions = [...(srv.sessions || [])];
|
||||||
return;
|
let updatedSessions;
|
||||||
|
|
||||||
|
if (originalName) {
|
||||||
|
updatedSessions = currentSessions.map((s) => (s.name === originalName ? sessionData : s));
|
||||||
|
} else {
|
||||||
|
if (currentSessions.some((s) => s.name === sessionData.name)) {
|
||||||
|
isDuplicate = true;
|
||||||
|
return prevServers;
|
||||||
|
}
|
||||||
|
updatedSessions = [...currentSessions, sessionData];
|
||||||
}
|
}
|
||||||
updatedSessions.push(sessionData);
|
|
||||||
showToast(`Sessão "${sessionData.name}" criada!`, 'success');
|
return prevServers.map((s) => (s.id === activeServerId ? { ...s, sessions: updatedSessions } : s));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
showToast('Já existe uma sessão com este nome.', 'error');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
try {
|
if (currentServer) {
|
||||||
const token = await generateToken(activeServer, sessionData.name);
|
if (!sessionData.token) {
|
||||||
if (token) {
|
try {
|
||||||
sessionData.token = token;
|
const token = await generateToken(currentServer, sessionData.name);
|
||||||
updateServersState(
|
if (token) {
|
||||||
servers.map((s) =>
|
updateSessionProperty(sessionData.name, { token });
|
||||||
s.id === activeServer.id
|
}
|
||||||
? { ...s, sessions: updatedSessions.map((x) => (x.name === sessionData.name ? { ...x, token } : x)) }
|
} catch (e) {
|
||||||
: s
|
console.warn('Erro ao gerar token automático:', e);
|
||||||
)
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} 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>
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user