Ajout : - e2e/csp_preview.spec.js — apercu CSP strict SANS deployer : le build officiel @alpinejs/csp (e2e/fixtures/alpine.csp.js, 0 eval) est servi a la place de alpine.min.js par interception Playwright ; toute expression que le parseur maison ne digere pas = pageerror (filet). Premiere surface VERTE : library (composant lie, icones SVG via Alpine.effect, recherche ouverte + focalisee, 0 erreur). Changed : - 16 composants x-data="fn()" enregistres via Alpine.data (registre = seule resolution du build CSP, probe « Undefined variable » ; scripts classiques executes pendant le parsing => alpine:init toujours joint) : appState, libraryPage, workspacesPage, editorState, board x4, settings/import/table_view/team_load/trash/workspace/welcome/accounts/ card_detail. - base.html (shell) migre : x-effect document.* -> syncSidebarClass(), $nextTick(arrow) -> initSidebarSort(), window.FlowDeck.* -> fdCreatePage/fdCreateFolder/fdGwRefresh, Object.keys/Math.min/ window.innerWidth dans x-for et :style -> sidebarSections()/ sectionMenuPos() — tout = simple appel de methode. - x-html restants du shell -> x-init + Alpine.effect : icone agent, carte projet, library x3 ; recherche library -> toggleSearch() (Alpine.nextTick) ; openMoveSelected() pour Object.keys en expression. - eslint : 70 warnings -> 0/0 (globals getCsrf depuis A38 ph1, /* exported openCardDetail */ + /* global owner, repo */, 3 ;; residuels). suite **1093/1093** · ruff OK · eslint 0/0 · E2E 3/3 (csp_preview + smoke x2) · docs a jour (ROADMAP ph3 LOT 1, CHANGELOG, WORKLOAD, VERSION)
165 lines
6.9 KiB
JavaScript
165 lines
6.9 KiB
JavaScript
/* exported importWizard -- appeles depuis les attributs HTML des templates */
|
|
|
|
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
|
|
|
// globales window — probe « Undefined variable: importWizard »).
|
|
|
|
document.addEventListener('alpine:init', function () { Alpine.data('importWizard', importWizard); });
|
|
function importWizard() {
|
|
return {
|
|
sources: [],
|
|
sourceId: '',
|
|
files: [],
|
|
dragOver: false,
|
|
mode: 'skip',
|
|
busy: false,
|
|
progress: 0,
|
|
error: '',
|
|
preview: null,
|
|
report: null,
|
|
mapping: {},
|
|
csrf: '',
|
|
urlValue: '',
|
|
urlError: '',
|
|
forgeProvider: 'gitea',
|
|
forgeOwner: '',
|
|
forgeRepo: '',
|
|
forgeState: 'all',
|
|
forgeError: '',
|
|
types: ['text','number','date','checkbox','email','url','phone','select','multi_select','status'],
|
|
async init() {
|
|
try {
|
|
const r = await fetch('/api/import/sources');
|
|
this.sources = (await r.json()).sources || [];
|
|
} catch { /* volontaire */ }
|
|
try {
|
|
const c = await fetch('/api/csrf-token');
|
|
this.csrf = (await c.json()).csrf_token;
|
|
} catch { /* volontaire */ }
|
|
},
|
|
humanSize(n) { return n > 1048576 ? (n/1048576).toFixed(1)+' Mo' : Math.max(1, Math.round(n/1024))+' Ko'; },
|
|
addFiles(list) {
|
|
for (const f of list) this.files.push({file:f, name:f.name, size:f.size, status:'queued'});
|
|
this.preview = null; this.report = null; this.error = '';
|
|
},
|
|
onFiles(e) { this.addFiles(e.target.files); e.target.value=''; },
|
|
onDrop(e) { this.dragOver=false; this.addFiles(e.dataTransfer.files); },
|
|
buildForm(f) {
|
|
const fd = new FormData();
|
|
fd.append('file', f.file);
|
|
if (this.sourceId) fd.append('source', this.sourceId);
|
|
fd.append('mode', this.mode);
|
|
return fd;
|
|
},
|
|
async doPreview() {
|
|
this.busy = true; this.error=''; this.report=null; this.progress=10;
|
|
try {
|
|
let merged = null;
|
|
for (let i=0;i<this.files.length;i++) {
|
|
const r = await fetch('/api/import/preview', {method:'POST', headers:{'X-CSRF-Token':this.csrf}, body:this.buildForm(this.files[i])});
|
|
const d = await r.json();
|
|
if (!r.ok) { this.error = (this.files[i].name+': '+(d.detail||'Erreur')); continue; }
|
|
if (!merged) merged = {source_label:d.source_label, pages:[], warnings:[], stats:{}};
|
|
merged.pages.push(...(d.pages||[]));
|
|
merged.warnings.push(...(d.warnings||[]));
|
|
this.progress = Math.round(((i+1)/this.files.length)*100);
|
|
}
|
|
this.preview = merged;
|
|
this.mapping = {};
|
|
(merged ? merged.pages : []).forEach(p => (p.schema||[]).forEach(c => { this.mapping[c.name]=c.type; }));
|
|
} catch { this.error = 'Erreur réseau'; }
|
|
finally { this.busy = false; setTimeout(()=>{this.progress=0;},800); }
|
|
},
|
|
async importOne(f) {
|
|
f.status = 'running';
|
|
const fd = this.buildForm(f);
|
|
if (Object.keys(this.mapping).length) fd.append('mapping', JSON.stringify(this.mapping));
|
|
const big = f.size > 5*1024*1024;
|
|
try {
|
|
if (big) {
|
|
fd.append('async','true');
|
|
const r = await fetch('/api/import/run', {method:'POST', headers:{'X-CSRF-Token':this.csrf}, body:fd});
|
|
const d = await r.json();
|
|
if (!r.ok) { f.status='error'; f.error=d.detail||'Erreur'; return null; }
|
|
return await this.pollJob(d.job_id, f);
|
|
}
|
|
const r = await fetch('/api/import/run', {method:'POST', headers:{'X-CSRF-Token':this.csrf}, body:fd});
|
|
const d = await r.json();
|
|
if (!r.ok) { f.status='error'; f.error=d.detail||'Erreur'; return null; }
|
|
f.status = (d.status==='partial' ? 'partial' : 'done');
|
|
return d;
|
|
} catch { f.status='error'; f.error='Erreur réseau'; return null; }
|
|
},
|
|
async doImport() {
|
|
this.busy = true; this.error=''; this.report=null; this.progress=0;
|
|
const aggregate = {pages_created:0,pages_updated:0,collections_created:0,rows_created:0,
|
|
attachments:0,skipped:0,warnings:[],errors:[],per_file:[]};
|
|
for (let i=0;i<this.files.length;i++) {
|
|
const d = await this.importOne(this.files[i]);
|
|
if (d) {
|
|
for (const k of ['pages_created','pages_updated','collections_created','rows_created','attachments','skipped'])
|
|
aggregate[k] += (d[k]||0);
|
|
aggregate.warnings.push(...(d.warnings||[]));
|
|
aggregate.errors.push(...(d.errors||[]));
|
|
aggregate.per_file.push({filename:this.files[i].name, report:d});
|
|
} else {
|
|
aggregate.errors.push({title:this.files[i].name, error:this.files[i].error||'Erreur'});
|
|
aggregate.per_file.push({filename:this.files[i].name, report:{status:'error'}});
|
|
}
|
|
this.progress = Math.round(((i+1)/this.files.length)*100);
|
|
}
|
|
this.report = aggregate;
|
|
this.busy = false;
|
|
},
|
|
async doUrl() {
|
|
this.busy = true; this.urlError = ''; this.report = null; this.progress = 40;
|
|
try {
|
|
const r = await fetch('/api/import/url', {
|
|
method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrf},
|
|
body: JSON.stringify({url:this.urlValue.trim()})
|
|
});
|
|
const d = await r.json();
|
|
if (!r.ok) { this.urlError = d.detail || 'Erreur'; return; }
|
|
this.report = d; this.progress = 100;
|
|
} catch { this.urlError = 'Erreur réseau'; }
|
|
finally { this.busy = false; }
|
|
},
|
|
async doForge(kind) {
|
|
this.busy = true; this.forgeError = ''; this.report = null; this.progress = 30;
|
|
const endpoint = kind === 'repo' ? '/api/import/forge-repo' : '/api/import/forge';
|
|
try {
|
|
const r = await fetch(endpoint, {
|
|
method:'POST', headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrf},
|
|
body: JSON.stringify({
|
|
provider:this.forgeProvider, owner:this.forgeOwner.trim(),
|
|
repo:this.forgeRepo.trim(), state:this.forgeState
|
|
})
|
|
});
|
|
const d = await r.json();
|
|
if (!r.ok) { this.forgeError = d.detail || 'Erreur'; return; }
|
|
this.report = d; this.progress = 100;
|
|
} catch { this.forgeError = 'Erreur réseau'; }
|
|
finally { this.busy = false; }
|
|
},
|
|
async pollJob(jobId, f) {
|
|
for (let i=0;i<600;i++) {
|
|
await new Promise(res => setTimeout(res, 700));
|
|
const r = await fetch('/api/import/jobs/'+jobId);
|
|
const j = await r.json();
|
|
if (j.status === 'done') { if (f) f.status='done'; return j.report; }
|
|
if (j.status === 'error') { if (f) { f.status='error'; f.error=j.error; } return null; }
|
|
}
|
|
if (f) { f.status='error'; f.error='Délai dépassé'; }
|
|
return null;
|
|
},
|
|
downloadReport() {
|
|
const blob = new Blob([JSON.stringify(this.report, null, 2)], {type:'application/json'});
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = 'flowdeck-import-report.json';
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
}
|
|
};
|
|
}
|