Spaces:
Running
Running
| import React, { useEffect, useState } from 'react' | |
| import { api, downloadBlob } from '../api' | |
| import DataTable from './DataTable' | |
| import EquationFormatsPanel from './EquationFormatsPanel' | |
| import LoadingOverlay from './LoadingOverlay' | |
| import MapFrame from './MapFrame' | |
| import PlotFigure from './PlotFigure' | |
| const REPO_INNER_TABS = [ | |
| { key: 'mapa', label: 'Mapa' }, | |
| { key: 'dados_mercado', label: 'Dados de Mercado' }, | |
| { key: 'metricas', label: 'Métricas' }, | |
| { key: 'transformacoes', label: 'Transformações' }, | |
| { key: 'resumo', label: 'Resumo' }, | |
| { key: 'coeficientes', label: 'Coeficientes' }, | |
| { key: 'obs_calc', label: 'Obs x Calc' }, | |
| { key: 'graficos', label: 'Gráficos' }, | |
| ] | |
| function formatarFonte(fonte) { | |
| if (!fonte || typeof fonte !== 'object') return 'Fonte não informada' | |
| const provider = String(fonte.provider || '').toLowerCase() | |
| if (provider === 'hf_dataset') { | |
| const repo = String(fonte.repo_id || '').trim() | |
| const revision = String(fonte.revision || '').trim() | |
| const suffix = fonte.degraded ? ' (modo contingência)' : '' | |
| return `HF Dataset${repo ? ` (${repo})` : ''}${revision ? ` | revisão ${revision.slice(0, 8)}` : ''}${suffix}` | |
| } | |
| return 'Pasta local' | |
| } | |
| export default function RepositorioTab({ authUser, sessionId }) { | |
| const [modelos, setModelos] = useState([]) | |
| const [fonte, setFonte] = useState(null) | |
| const [loading, setLoading] = useState(false) | |
| const [uploading, setUploading] = useState(false) | |
| const [deleting, setDeleting] = useState(false) | |
| const [error, setError] = useState('') | |
| const [status, setStatus] = useState('') | |
| const [arquivoUpload, setArquivoUpload] = useState(null) | |
| const [confirmDeleteId, setConfirmDeleteId] = useState('') | |
| const [confirmarSubstituicao, setConfirmarSubstituicao] = useState({ open: false, substituidos: [] }) | |
| const [confirmarExclusaoModal, setConfirmarExclusaoModal] = useState({ | |
| open: false, | |
| modeloId: '', | |
| nomeModelo: '', | |
| digitado: '', | |
| }) | |
| const [modeloAbertoMeta, setModeloAbertoMeta] = useState(null) | |
| const [modeloAbertoLoading, setModeloAbertoLoading] = useState(false) | |
| const [modeloAbertoError, setModeloAbertoError] = useState('') | |
| const [modeloAbertoActiveTab, setModeloAbertoActiveTab] = useState('mapa') | |
| const [modeloAbertoDados, setModeloAbertoDados] = useState(null) | |
| const [modeloAbertoEstatisticas, setModeloAbertoEstatisticas] = useState(null) | |
| const [modeloAbertoEscalasHtml, setModeloAbertoEscalasHtml] = useState('') | |
| const [modeloAbertoDadosTransformados, setModeloAbertoDadosTransformados] = useState(null) | |
| const [modeloAbertoResumoHtml, setModeloAbertoResumoHtml] = useState('') | |
| const [modeloAbertoEquacoes, setModeloAbertoEquacoes] = useState(null) | |
| const [modeloAbertoCoeficientes, setModeloAbertoCoeficientes] = useState(null) | |
| const [modeloAbertoObsCalc, setModeloAbertoObsCalc] = useState(null) | |
| const [modeloAbertoMapaHtml, setModeloAbertoMapaHtml] = useState('') | |
| const [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão']) | |
| const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão') | |
| const [modeloAbertoPlotObsCalc, setModeloAbertoPlotObsCalc] = useState(null) | |
| const [modeloAbertoPlotResiduos, setModeloAbertoPlotResiduos] = useState(null) | |
| const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null) | |
| const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null) | |
| const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null) | |
| const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin' | |
| const totalModelos = modelos.length | |
| const modoModeloAberto = Boolean(modeloAbertoMeta) | |
| const nomesSubstituidos = Array.isArray(confirmarSubstituicao.substituidos) | |
| ? confirmarSubstituicao.substituidos.filter(Boolean) | |
| : [] | |
| const exclusaoDigitadaCorreta = confirmarExclusaoModal.digitado === confirmarExclusaoModal.nomeModelo | |
| useEffect(() => { | |
| void carregarModelos() | |
| }, []) | |
| async function carregarModelos() { | |
| setLoading(true) | |
| setError('') | |
| try { | |
| const resp = await api.repositorioListar() | |
| setModelos(Array.isArray(resp?.modelos) ? resp.modelos : []) | |
| setFonte(resp?.fonte || null) | |
| setStatus('') | |
| } catch (err) { | |
| setError(err.message || 'Falha ao carregar repositório.') | |
| setModelos([]) | |
| setFonte(null) | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| function parseSubstituidosErro(err) { | |
| if (!err || typeof err !== 'object') return [] | |
| const detail = err.detail | |
| if (!detail || typeof detail !== 'object') return [] | |
| const lista = detail.substituidos | |
| if (!Array.isArray(lista)) return [] | |
| return lista.map((item) => String(item || '').trim()).filter(Boolean) | |
| } | |
| async function onUploadArquivo(confirmar = false) { | |
| if (!isAdmin || !arquivoUpload) return | |
| setUploading(true) | |
| setError('') | |
| setStatus('') | |
| try { | |
| const resp = await api.repositorioUpload([arquivoUpload], { confirmarSubstituicao: confirmar }) | |
| setModelos(Array.isArray(resp?.modelos) ? resp.modelos : []) | |
| setFonte(resp?.fonte || null) | |
| setStatus(resp?.status || 'Modelo incluído no repositório.') | |
| setArquivoUpload(null) | |
| setConfirmarSubstituicao({ open: false, substituidos: [] }) | |
| } catch (err) { | |
| const substituidos = parseSubstituidosErro(err) | |
| const erroDuplicado = Number(err?.status) === 409 && substituidos.length > 0 | |
| if (!confirmar && erroDuplicado) { | |
| setConfirmarSubstituicao({ open: true, substituidos }) | |
| setError('') | |
| return | |
| } | |
| setError(err.message || 'Falha ao incluir modelo no repositório.') | |
| } finally { | |
| setUploading(false) | |
| } | |
| } | |
| async function onExcluirModelo(modeloId) { | |
| if (!isAdmin || !modeloId) return | |
| setDeleting(true) | |
| setError('') | |
| setStatus('') | |
| try { | |
| const resp = await api.repositorioDelete([String(modeloId)]) | |
| setModelos(Array.isArray(resp?.modelos) ? resp.modelos : []) | |
| setFonte(resp?.fonte || null) | |
| setStatus(resp?.status || 'Modelo removido do repositório.') | |
| setConfirmDeleteId('') | |
| setConfirmarExclusaoModal({ open: false, modeloId: '', nomeModelo: '', digitado: '' }) | |
| } catch (err) { | |
| setError(err.message || 'Falha na exclusão do modelo.') | |
| } finally { | |
| setDeleting(false) | |
| } | |
| } | |
| function onAbrirModalExclusao(item) { | |
| const id = String(item?.id || '').trim() | |
| if (!id) return | |
| const nomeModelo = String(item?.nome_modelo || item?.arquivo || id).trim() | |
| setConfirmDeleteId('') | |
| setConfirmarExclusaoModal({ | |
| open: true, | |
| modeloId: id, | |
| nomeModelo, | |
| digitado: '', | |
| }) | |
| } | |
| function onCancelarModalExclusao() { | |
| if (deleting) return | |
| setConfirmarExclusaoModal({ open: false, modeloId: '', nomeModelo: '', digitado: '' }) | |
| } | |
| async function onConfirmarExclusaoModal() { | |
| const modeloId = String(confirmarExclusaoModal.modeloId || '').trim() | |
| if (!modeloId) return | |
| if (confirmarExclusaoModal.digitado !== confirmarExclusaoModal.nomeModelo) return | |
| await onExcluirModelo(modeloId) | |
| } | |
| function preencherModeloAberto(resp) { | |
| setModeloAbertoDados(resp?.dados || null) | |
| setModeloAbertoEstatisticas(resp?.estatisticas || null) | |
| setModeloAbertoEscalasHtml(resp?.escalas_html || '') | |
| setModeloAbertoDadosTransformados(resp?.dados_transformados || null) | |
| setModeloAbertoResumoHtml(resp?.resumo_html || '') | |
| setModeloAbertoEquacoes(resp?.equacoes || null) | |
| setModeloAbertoCoeficientes(resp?.coeficientes || null) | |
| setModeloAbertoObsCalc(resp?.obs_calc || null) | |
| setModeloAbertoMapaHtml(resp?.mapa_html || '') | |
| setModeloAbertoMapaChoices(resp?.mapa_choices || ['Visualização Padrão']) | |
| setModeloAbertoMapaVar('Visualização Padrão') | |
| setModeloAbertoPlotObsCalc(resp?.grafico_obs_calc || null) | |
| setModeloAbertoPlotResiduos(resp?.grafico_residuos || null) | |
| setModeloAbertoPlotHistograma(resp?.grafico_histograma || null) | |
| setModeloAbertoPlotCook(resp?.grafico_cook || null) | |
| setModeloAbertoPlotCorr(resp?.grafico_correlacao || null) | |
| } | |
| async function onAbrirModelo(item) { | |
| if (!sessionId) { | |
| setError('Sessão indisponível no momento. Aguarde e tente novamente.') | |
| return | |
| } | |
| setModeloAbertoLoading(true) | |
| setModeloAbertoError('') | |
| try { | |
| await api.visualizacaoRepositorioCarregar(sessionId, String(item?.id || '')) | |
| const resp = await api.exibirVisualizacao(sessionId) | |
| preencherModeloAberto(resp) | |
| setModeloAbertoActiveTab('mapa') | |
| setModeloAbertoMeta({ | |
| id: String(item?.id || ''), | |
| nome: item?.nome_modelo || item?.arquivo || String(item?.id || ''), | |
| }) | |
| } catch (err) { | |
| setModeloAbertoError(err.message || 'Falha ao abrir modelo.') | |
| } finally { | |
| setModeloAbertoLoading(false) | |
| } | |
| } | |
| function onVoltarRepositorio() { | |
| setModeloAbertoMeta(null) | |
| setModeloAbertoError('') | |
| setModeloAbertoActiveTab('mapa') | |
| } | |
| async function onModeloAbertoMapChange(nextVar) { | |
| setModeloAbertoMapaVar(nextVar) | |
| if (!sessionId) return | |
| try { | |
| const resp = await api.updateVisualizacaoMap(sessionId, nextVar) | |
| setModeloAbertoMapaHtml(resp?.mapa_html || '') | |
| } catch (err) { | |
| setModeloAbertoError(err.message || 'Falha ao atualizar mapa do modelo.') | |
| } | |
| } | |
| async function onDownloadEquacaoModeloAberto(mode) { | |
| if (!sessionId || !mode) return | |
| setModeloAbertoLoading(true) | |
| try { | |
| const blob = await api.exportEquationViz(sessionId, mode) | |
| const sufixo = String(mode) === 'excel_sab' ? 'estilo_sab' : 'excel' | |
| downloadBlob(blob, `equacao_modelo_${sufixo}.xlsx`) | |
| } catch (err) { | |
| setModeloAbertoError(err.message || 'Falha ao exportar equação.') | |
| } finally { | |
| setModeloAbertoLoading(false) | |
| } | |
| } | |
| if (modoModeloAberto) { | |
| return ( | |
| <div className="tab-content"> | |
| <div className="pesquisa-opened-model-view"> | |
| <div className="pesquisa-opened-model-head"> | |
| <div className="pesquisa-opened-model-title-wrap"> | |
| <h3>{modeloAbertoMeta?.nome || 'Modelo'}</h3> | |
| <p>Visualização do modelo do repositório</p> | |
| </div> | |
| <button type="button" className="model-source-back-btn model-source-back-btn-danger" onClick={onVoltarRepositorio} disabled={modeloAbertoLoading}> | |
| Voltar ao repositório | |
| </button> | |
| </div> | |
| <div className="inner-tabs" role="tablist" aria-label="Abas internas do modelo aberto no repositório"> | |
| {REPO_INNER_TABS.map((tab) => ( | |
| <button | |
| key={tab.key} | |
| type="button" | |
| className={modeloAbertoActiveTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'} | |
| onClick={() => setModeloAbertoActiveTab(tab.key)} | |
| > | |
| {tab.label} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="inner-tab-panel"> | |
| {modeloAbertoActiveTab === 'mapa' ? ( | |
| <> | |
| <div className="row compact visualizacao-mapa-controls"> | |
| <label>Variável no mapa</label> | |
| <select value={modeloAbertoMapaVar} onChange={(event) => void onModeloAbertoMapChange(event.target.value)}> | |
| {modeloAbertoMapaChoices.map((choice) => ( | |
| <option key={`repo-modelo-aberto-mapa-${choice}`} value={choice}>{choice}</option> | |
| ))} | |
| </select> | |
| </div> | |
| <MapFrame html={modeloAbertoMapaHtml} /> | |
| </> | |
| ) : null} | |
| {modeloAbertoActiveTab === 'dados_mercado' ? <DataTable table={modeloAbertoDados} maxHeight={620} /> : null} | |
| {modeloAbertoActiveTab === 'metricas' ? <DataTable table={modeloAbertoEstatisticas} maxHeight={620} /> : null} | |
| {modeloAbertoActiveTab === 'transformacoes' ? ( | |
| <> | |
| <div dangerouslySetInnerHTML={{ __html: modeloAbertoEscalasHtml }} /> | |
| <h4 className="visualizacao-table-title">Dados com variáveis transformadas</h4> | |
| <DataTable table={modeloAbertoDadosTransformados} /> | |
| </> | |
| ) : null} | |
| {modeloAbertoActiveTab === 'resumo' ? ( | |
| <> | |
| <div className="equation-formats-section"> | |
| <h4>Equações do Modelo</h4> | |
| <EquationFormatsPanel | |
| equacoes={modeloAbertoEquacoes} | |
| onDownload={(mode) => void onDownloadEquacaoModeloAberto(mode)} | |
| disabled={modeloAbertoLoading} | |
| /> | |
| </div> | |
| <div dangerouslySetInnerHTML={{ __html: modeloAbertoResumoHtml }} /> | |
| </> | |
| ) : null} | |
| {modeloAbertoActiveTab === 'coeficientes' ? <DataTable table={modeloAbertoCoeficientes} maxHeight={620} /> : null} | |
| {modeloAbertoActiveTab === 'obs_calc' ? <DataTable table={modeloAbertoObsCalc} maxHeight={620} /> : null} | |
| {modeloAbertoActiveTab === 'graficos' ? ( | |
| <> | |
| <div className="plot-grid-2-fixed"> | |
| <PlotFigure figure={modeloAbertoPlotObsCalc} title="Obs x Calc" /> | |
| <PlotFigure figure={modeloAbertoPlotResiduos} title="Resíduos" /> | |
| <PlotFigure figure={modeloAbertoPlotHistograma} title="Histograma" /> | |
| <PlotFigure figure={modeloAbertoPlotCook} title="Cook" forceHideLegend /> | |
| </div> | |
| <div className="plot-full-width"> | |
| <PlotFigure figure={modeloAbertoPlotCorr} title="Correlação" className="plot-correlation-card" /> | |
| </div> | |
| </> | |
| ) : null} | |
| </div> | |
| {modeloAbertoError ? <div className="error-line inline-error">{modeloAbertoError}</div> : null} | |
| </div> | |
| <LoadingOverlay show={modeloAbertoLoading} label="Carregando modelo..." /> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className="tab-content"> | |
| <div className="repositorio-standalone-panel"> | |
| <div className="repo-toolbar"> | |
| <div className="repo-summary"> | |
| <div><strong>Total:</strong> {totalModelos}</div> | |
| <div><strong>Fonte:</strong> {formatarFonte(fonte)}</div> | |
| <div><strong>Perfil:</strong> {isAdmin ? 'Administrador' : 'Leitura'}</div> | |
| </div> | |
| <div className="repo-actions"> | |
| <button | |
| type="button" | |
| className="repo-refresh-btn" | |
| onClick={() => void carregarModelos()} | |
| disabled={loading || uploading || deleting} | |
| > | |
| Atualizar lista | |
| </button> | |
| </div> | |
| </div> | |
| {isAdmin ? ( | |
| <div className="repo-admin-controls"> | |
| <div className="repo-upload-row repo-upload-row-single"> | |
| <input | |
| type="file" | |
| accept=".dai" | |
| onChange={(event) => setArquivoUpload(event.target.files?.[0] || null)} | |
| disabled={uploading || deleting} | |
| /> | |
| <button type="button" onClick={() => void onUploadArquivo()} disabled={uploading || deleting || !arquivoUpload}> | |
| Incluir modelo no repositório | |
| </button> | |
| </div> | |
| {arquivoUpload ? <div className="section1-empty-hint">Arquivo selecionado: {arquivoUpload.name}</div> : null} | |
| </div> | |
| ) : ( | |
| <div className="section1-empty-hint">Perfil de leitura: inclusão e exclusão disponíveis apenas para administradores.</div> | |
| )} | |
| {status ? <div className="status-line">{status}</div> : null} | |
| {error ? <div className="error-line">{error}</div> : null} | |
| <div className="table-container repo-table-block"> | |
| <table className="repo-table"> | |
| <thead> | |
| <tr> | |
| <th>Modelo</th> | |
| <th>Tipo</th> | |
| <th>Finalidade</th> | |
| <th>Autor</th> | |
| <th>Período</th> | |
| <th>Dados</th> | |
| <th>APP</th> | |
| <th>Status</th> | |
| <th className="repo-col-open">Abrir</th> | |
| {isAdmin ? <th className="repo-col-delete">Excluir</th> : null} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {modelos.map((item) => { | |
| const key = String(item.id) | |
| const emConfirmacao = confirmDeleteId === key | |
| return ( | |
| <tr key={key}> | |
| <td>{item.nome_modelo || item.arquivo || key}</td> | |
| <td>{item.tipo_imovel || '-'}</td> | |
| <td>{item.finalidade || '-'}</td> | |
| <td>{item.autor || '-'}</td> | |
| <td>{item.periodo_dados?.label || '-'}</td> | |
| <td>{item.total_dados ?? '-'}</td> | |
| <td>{item.tem_app ? 'Sim' : 'Não'}</td> | |
| <td>{item.status || '-'}</td> | |
| <td className="repo-col-open"> | |
| <button | |
| type="button" | |
| className="repo-open-btn" | |
| onClick={() => void onAbrirModelo(item)} | |
| title="Abrir modelo" | |
| aria-label={`Abrir ${item.nome_modelo || item.arquivo || key}`} | |
| > | |
| ↗ | |
| </button> | |
| </td> | |
| {isAdmin ? ( | |
| <td className="repo-col-delete"> | |
| {!emConfirmacao ? ( | |
| <button | |
| type="button" | |
| className="repo-delete-icon-btn" | |
| onClick={() => setConfirmDeleteId(key)} | |
| disabled={deleting} | |
| title="Excluir modelo" | |
| aria-label={`Excluir ${item.nome_modelo || item.arquivo || key}`} | |
| > | |
| 🗑 | |
| </button> | |
| ) : ( | |
| <div className="repo-delete-inline-confirm"> | |
| <button | |
| type="button" | |
| className="btn-danger repo-delete-confirm-btn" | |
| onClick={() => onAbrirModalExclusao(item)} | |
| disabled={deleting} | |
| > | |
| Excluir | |
| </button> | |
| <button | |
| type="button" | |
| className="repo-delete-cancel-btn" | |
| onClick={() => setConfirmDeleteId('')} | |
| disabled={deleting} | |
| > | |
| Cancelar | |
| </button> | |
| </div> | |
| )} | |
| </td> | |
| ) : null} | |
| </tr> | |
| ) | |
| })} | |
| {!modelos.length ? ( | |
| <tr> | |
| <td colSpan={isAdmin ? 10 : 9}> | |
| {loading ? 'Carregando modelos...' : 'Nenhum modelo encontrado no repositório.'} | |
| </td> | |
| </tr> | |
| ) : null} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| {confirmarSubstituicao.open ? ( | |
| <div className="pesquisa-modal-backdrop"> | |
| <div className="pesquisa-modal repo-confirm-modal"> | |
| <div className="pesquisa-modal-head"> | |
| <div> | |
| <h4>Confirmar substituição de modelo</h4> | |
| <p>Já existe modelo com o mesmo nome no repositório.</p> | |
| </div> | |
| <button | |
| type="button" | |
| className="pesquisa-modal-close" | |
| onClick={() => setConfirmarSubstituicao({ open: false, substituidos: [] })} | |
| disabled={uploading} | |
| > | |
| Fechar | |
| </button> | |
| </div> | |
| <div className="repo-confirm-modal-body"> | |
| <div className="repo-confirm-text"> | |
| Se você continuar, o modelo existente será substituído. | |
| </div> | |
| <ul className="repo-replace-list"> | |
| {(nomesSubstituidos.length ? nomesSubstituidos : [String(arquivoUpload?.name || '')]).map((nome) => ( | |
| <li key={`substituir-${nome}`}>{nome}</li> | |
| ))} | |
| </ul> | |
| <div className="repo-confirm-actions"> | |
| <button | |
| type="button" | |
| className="repo-delete-cancel-btn" | |
| onClick={() => setConfirmarSubstituicao({ open: false, substituidos: [] })} | |
| disabled={uploading} | |
| > | |
| Cancelar | |
| </button> | |
| <button | |
| type="button" | |
| className="btn-danger" | |
| onClick={() => void onUploadArquivo(true)} | |
| disabled={uploading} | |
| > | |
| {uploading ? 'Substituindo...' : 'Confirmar substituição'} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ) : null} | |
| {confirmarExclusaoModal.open ? ( | |
| <div className="pesquisa-modal-backdrop"> | |
| <div className="pesquisa-modal repo-confirm-modal"> | |
| <div className="pesquisa-modal-head"> | |
| <div> | |
| <h4>{confirmarExclusaoModal.nomeModelo || 'Confirmar exclusão'}</h4> | |
| <p>Digite o nome completo do modelo para confirmar a exclusão.</p> | |
| </div> | |
| <button | |
| type="button" | |
| className="pesquisa-modal-close" | |
| onClick={onCancelarModalExclusao} | |
| disabled={deleting} | |
| > | |
| Fechar | |
| </button> | |
| </div> | |
| <form | |
| className="repo-confirm-modal-body" | |
| onSubmit={(event) => { | |
| event.preventDefault() | |
| void onConfirmarExclusaoModal() | |
| }} | |
| > | |
| <label className="repo-delete-typing-field"> | |
| Nome do modelo | |
| <input | |
| type="text" | |
| value={confirmarExclusaoModal.digitado} | |
| onChange={(event) => setConfirmarExclusaoModal((prev) => ({ ...prev, digitado: event.target.value }))} | |
| placeholder="Digite exatamente como no título" | |
| autoComplete="off" | |
| disabled={deleting} | |
| /> | |
| </label> | |
| {confirmarExclusaoModal.digitado && !exclusaoDigitadaCorreta ? ( | |
| <div className="repo-delete-typing-hint repo-delete-typing-hint-error"> | |
| O texto digitado não corresponde ao nome do modelo. | |
| </div> | |
| ) : ( | |
| <div className="repo-delete-typing-hint"> | |
| A exclusão será concluída somente quando o nome for digitado exatamente. | |
| </div> | |
| )} | |
| <div className="repo-confirm-actions"> | |
| <button type="button" className="repo-delete-cancel-btn" onClick={onCancelarModalExclusao} disabled={deleting}> | |
| Cancelar | |
| </button> | |
| <button type="submit" className="btn-danger" disabled={deleting || !exclusaoDigitadaCorreta}> | |
| {deleting ? 'Excluindo...' : 'Excluir modelo'} | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| ) : null} | |
| </div> | |
| ) | |
| } | |