@unbrained/pm-web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +107 -0
- package/dist/auth.js +20 -0
- package/dist/auth.js.map +1 -0
- package/dist/crypto.js +42 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.js +111 -0
- package/dist/db.js.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.js +16 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/routes/admin.js +207 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.js +163 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/github.js +354 -0
- package/dist/routes/github.js.map +1 -0
- package/dist/routes/groups.js +180 -0
- package/dist/routes/groups.js.map +1 -0
- package/dist/routes/pm.js +2446 -0
- package/dist/routes/pm.js.map +1 -0
- package/dist/routes/projects.js +151 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/sharing.js +155 -0
- package/dist/routes/sharing.js.map +1 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/services/pm-runner.js +190 -0
- package/dist/services/pm-runner.js.map +1 -0
- package/dist/services/sse.js +111 -0
- package/dist/services/sse.js.map +1 -0
- package/manifest.json +15 -0
- package/package.json +111 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/index.html +265 -0
- package/public/manifest.json +66 -0
- package/public/src/api.js +28 -0
- package/public/src/api.js.map +1 -0
- package/public/src/api.ts +29 -0
- package/public/src/app.js +926 -0
- package/public/src/app.js.map +1 -0
- package/public/src/app.ts +929 -0
- package/public/src/components/modals.js +62 -0
- package/public/src/components/modals.js.map +1 -0
- package/public/src/components/modals.ts +73 -0
- package/public/src/components/toast.js +10 -0
- package/public/src/components/toast.js.map +1 -0
- package/public/src/components/toast.ts +13 -0
- package/public/src/constants.js +30 -0
- package/public/src/constants.js.map +1 -0
- package/public/src/constants.ts +41 -0
- package/public/src/state.js +15 -0
- package/public/src/state.js.map +1 -0
- package/public/src/state.ts +19 -0
- package/public/src/types.js +5 -0
- package/public/src/types.js.map +1 -0
- package/public/src/types.ts +253 -0
- package/public/src/utils.js +57 -0
- package/public/src/utils.js.map +1 -0
- package/public/src/utils.ts +56 -0
- package/public/src/views/activity.js +47 -0
- package/public/src/views/activity.js.map +1 -0
- package/public/src/views/activity.ts +41 -0
- package/public/src/views/admin.js +435 -0
- package/public/src/views/admin.js.map +1 -0
- package/public/src/views/admin.ts +504 -0
- package/public/src/views/auth.js +81 -0
- package/public/src/views/auth.js.map +1 -0
- package/public/src/views/auth.ts +74 -0
- package/public/src/views/calendar.js +133 -0
- package/public/src/views/calendar.js.map +1 -0
- package/public/src/views/calendar.ts +129 -0
- package/public/src/views/comments-audit.js +109 -0
- package/public/src/views/comments-audit.js.map +1 -0
- package/public/src/views/comments-audit.ts +108 -0
- package/public/src/views/config.js +322 -0
- package/public/src/views/config.js.map +1 -0
- package/public/src/views/config.ts +344 -0
- package/public/src/views/context.js +98 -0
- package/public/src/views/context.js.map +1 -0
- package/public/src/views/context.ts +100 -0
- package/public/src/views/create.js +293 -0
- package/public/src/views/create.js.map +1 -0
- package/public/src/views/create.ts +246 -0
- package/public/src/views/dedupe.js +51 -0
- package/public/src/views/dedupe.js.map +1 -0
- package/public/src/views/dedupe.ts +43 -0
- package/public/src/views/export.js +300 -0
- package/public/src/views/export.js.map +1 -0
- package/public/src/views/export.ts +274 -0
- package/public/src/views/github.js +360 -0
- package/public/src/views/github.js.map +1 -0
- package/public/src/views/github.ts +308 -0
- package/public/src/views/graph-canvas.js +1986 -0
- package/public/src/views/graph-canvas.js.map +1 -0
- package/public/src/views/graph-canvas.ts +2218 -0
- package/public/src/views/graph.js +1824 -0
- package/public/src/views/graph.js.map +1 -0
- package/public/src/views/graph.ts +1891 -0
- package/public/src/views/groups.js +186 -0
- package/public/src/views/groups.js.map +1 -0
- package/public/src/views/groups.ts +172 -0
- package/public/src/views/guide.js +151 -0
- package/public/src/views/guide.js.map +1 -0
- package/public/src/views/guide.ts +162 -0
- package/public/src/views/health.js +105 -0
- package/public/src/views/health.js.map +1 -0
- package/public/src/views/health.ts +102 -0
- package/public/src/views/items.js +1306 -0
- package/public/src/views/items.js.map +1 -0
- package/public/src/views/items.ts +1196 -0
- package/public/src/views/normalize.js +67 -0
- package/public/src/views/normalize.js.map +1 -0
- package/public/src/views/normalize.ts +58 -0
- package/public/src/views/plan.js +454 -0
- package/public/src/views/plan.js.map +1 -0
- package/public/src/views/plan.ts +496 -0
- package/public/src/views/projects.js +204 -0
- package/public/src/views/projects.js.map +1 -0
- package/public/src/views/projects.ts +196 -0
- package/public/src/views/router.js +227 -0
- package/public/src/views/router.js.map +1 -0
- package/public/src/views/router.ts +188 -0
- package/public/src/views/search.js +103 -0
- package/public/src/views/search.js.map +1 -0
- package/public/src/views/search.ts +94 -0
- package/public/src/views/settings.js +272 -0
- package/public/src/views/settings.js.map +1 -0
- package/public/src/views/settings.ts +190 -0
- package/public/src/views/shared.js +49 -0
- package/public/src/views/shared.js.map +1 -0
- package/public/src/views/shared.ts +49 -0
- package/public/src/views/sharing.js +152 -0
- package/public/src/views/sharing.js.map +1 -0
- package/public/src/views/sharing.ts +139 -0
- package/public/src/views/stats.js +92 -0
- package/public/src/views/stats.js.map +1 -0
- package/public/src/views/stats.ts +88 -0
- package/public/src/views/templates.js +117 -0
- package/public/src/views/templates.js.map +1 -0
- package/public/src/views/templates.ts +113 -0
- package/public/src/views/validate.js +54 -0
- package/public/src/views/validate.js.map +1 -0
- package/public/src/views/validate.ts +48 -0
- package/public/styles.css +2231 -0
- package/public/sw.js +318 -0
- package/public/tsconfig.json +20 -0
- package/sql/schema.sql +105 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// DEDUPE AUDIT VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml, typeIcon, statusBadge } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
export async function renderDedupeAuditView(): Promise<void> {
|
|
9
|
+
const el = document.getElementById('content-dedupe');
|
|
10
|
+
if (!el) return;
|
|
11
|
+
if (!state.currentProject) { el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>'; return; }
|
|
12
|
+
el.innerHTML = `
|
|
13
|
+
<div class="page-header">
|
|
14
|
+
<div><div class="page-title">Dedupe Audit</div><div class="page-subtitle">Find potential duplicate items in ${escHtml(state.currentProject.name)}</div></div>
|
|
15
|
+
<div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderDedupeAuditView()">↺ Refresh</button></div>
|
|
16
|
+
</div>
|
|
17
|
+
<div id="dedupe-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
|
|
18
|
+
try {
|
|
19
|
+
const data = await api('GET', `/projects/${state.currentProject.id}/pm/dedupe-audit`);
|
|
20
|
+
const groups = (data as any).groups || (data as any).duplicates || [];
|
|
21
|
+
const el2 = document.getElementById('dedupe-content');
|
|
22
|
+
if (!el2) return;
|
|
23
|
+
if (groups.length === 0) {
|
|
24
|
+
el2.innerHTML = `<div class="card"><div class="card-body"><div style="color:var(--status-closed);font-size:13px">✓ No potential duplicates found — project looks clean!</div></div></div>`;
|
|
25
|
+
} else {
|
|
26
|
+
el2.innerHTML = groups.map((g: any, i: number) => `
|
|
27
|
+
<div class="card" style="margin-bottom:12px">
|
|
28
|
+
<div class="card-header"><div class="card-title">Potential Duplicate Group ${i+1} ${g.score!==undefined?`<span style="font-size:11px;color:var(--text-muted)">· ${Math.round((g.score||0)*100)}% similarity</span>`:''}</div></div>
|
|
29
|
+
<div class="card-body">
|
|
30
|
+
${(g.items||[]).map((item: any)=>`
|
|
31
|
+
<div class="item-row" onclick="window.__app.openItemDetail('${escHtml(item.id||item)}')" style="cursor:pointer">
|
|
32
|
+
${typeIcon(item.type||'')} <span class="item-id">${escHtml(item.id||item)}</span>
|
|
33
|
+
<span class="item-title">${escHtml(item.title||'')}</span>
|
|
34
|
+
<div class="item-meta">${statusBadge(item.status||'draft')}</div>
|
|
35
|
+
</div>`).join('')}
|
|
36
|
+
</div>
|
|
37
|
+
</div>`).join('') || `<div class="card"><div class="card-body"><pre style="font-size:12px;color:var(--text-secondary);white-space:pre-wrap">${escHtml(JSON.stringify(data,null,2))}</pre></div></div>`;
|
|
38
|
+
}
|
|
39
|
+
} catch(err: unknown) {
|
|
40
|
+
const el2 = document.getElementById('dedupe-content');
|
|
41
|
+
if (el2) el2.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// EXPORT / IMPORT VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml } from '../utils.js';
|
|
7
|
+
import { toast } from '../components/toast.js';
|
|
8
|
+
export async function renderExportView() {
|
|
9
|
+
const el = document.getElementById('content-export');
|
|
10
|
+
if (!el)
|
|
11
|
+
return;
|
|
12
|
+
if (!state.currentProject) {
|
|
13
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
el.innerHTML = `
|
|
17
|
+
<div class="page-header">
|
|
18
|
+
<div><div class="page-title">Export & Import</div><div class="page-subtitle">${escHtml(state.currentProject.name)}</div></div>
|
|
19
|
+
</div>
|
|
20
|
+
<div style="max-width:600px;display:flex;flex-direction:column;gap:16px">
|
|
21
|
+
<div class="export-card">
|
|
22
|
+
<div class="export-card-icon">📥</div>
|
|
23
|
+
<div class="export-card-info">
|
|
24
|
+
<div class="export-card-title">Export as JSON</div>
|
|
25
|
+
<div class="export-card-desc">Download all items with full metadata, body, tags, and all available fields</div>
|
|
26
|
+
</div>
|
|
27
|
+
<button class="btn btn-primary btn-sm" onclick="window.__app.exportData('json')"><span>Export JSON</span></button>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="export-card">
|
|
30
|
+
<div class="export-card-icon">📊</div>
|
|
31
|
+
<div class="export-card-info">
|
|
32
|
+
<div class="export-card-title">Export as CSV</div>
|
|
33
|
+
<div class="export-card-desc">Download items as a spreadsheet-compatible CSV file (includes body, parent, estimate)</div>
|
|
34
|
+
</div>
|
|
35
|
+
<button class="btn btn-secondary btn-sm" onclick="window.__app.exportData('csv')"><span>Export CSV</span></button>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="export-card">
|
|
38
|
+
<div class="export-card-icon">📋</div>
|
|
39
|
+
<div class="export-card-info">
|
|
40
|
+
<div class="export-card-title">Export as YAML</div>
|
|
41
|
+
<div class="export-card-desc">Download items as a human-readable YAML file with all available fields</div>
|
|
42
|
+
</div>
|
|
43
|
+
<button class="btn btn-secondary btn-sm" onclick="window.__app.exportData('yaml')"><span>Export YAML</span></button>
|
|
44
|
+
</div>
|
|
45
|
+
<hr class="section-divider">
|
|
46
|
+
<div class="export-card">
|
|
47
|
+
<div class="export-card-icon">📤</div>
|
|
48
|
+
<div class="export-card-info">
|
|
49
|
+
<div class="export-card-title">Import from JSON or YAML</div>
|
|
50
|
+
<div class="export-card-desc">Upload a previously exported JSON or YAML file to add items to this project (max 500 items)</div>
|
|
51
|
+
</div>
|
|
52
|
+
<label class="btn btn-secondary btn-sm" style="cursor:pointer">
|
|
53
|
+
<span>Choose File</span>
|
|
54
|
+
<input type="file" accept=".json,.yaml,.yml" style="display:none" onchange="window.__app.importData(this.files[0])">
|
|
55
|
+
</label>
|
|
56
|
+
</div>
|
|
57
|
+
<div id="export-status" style="display:none"></div>
|
|
58
|
+
</div>`;
|
|
59
|
+
}
|
|
60
|
+
export async function exportData(format) {
|
|
61
|
+
const statusEl = document.getElementById('export-status');
|
|
62
|
+
if (!state.currentProject || !statusEl)
|
|
63
|
+
return;
|
|
64
|
+
statusEl.style.display = '';
|
|
65
|
+
statusEl.innerHTML = '<div class="loading-state" style="padding:16px"><div class="loading-spinner"></div></div>';
|
|
66
|
+
try {
|
|
67
|
+
// Use server-side export endpoint which fetches --full --include-body data
|
|
68
|
+
const projectId = state.currentProject.id;
|
|
69
|
+
const slug = state.currentProject.slug;
|
|
70
|
+
const exportedAt = new Date().toISOString();
|
|
71
|
+
if (format === 'csv') {
|
|
72
|
+
// Fetch via the dedicated server-side export endpoint for CSV
|
|
73
|
+
const resp = await fetch(`/api/projects/${encodeURIComponent(projectId)}/pm/export?format=csv`, {
|
|
74
|
+
credentials: 'include',
|
|
75
|
+
});
|
|
76
|
+
if (!resp.ok) {
|
|
77
|
+
const err = await resp.json().catch(() => ({ error: 'Export failed' }));
|
|
78
|
+
throw new Error(err.error || `Export failed (${resp.status})`);
|
|
79
|
+
}
|
|
80
|
+
const content = await resp.text();
|
|
81
|
+
const lineCount = content.split('\n').length - 1; // subtract header
|
|
82
|
+
downloadFile(content, `${slug}-items.csv`, 'text/csv');
|
|
83
|
+
statusEl.innerHTML = `<div style="color:var(--status-closed);font-size:13px;padding:12px">✓ Exported ${lineCount} items as CSV</div>`;
|
|
84
|
+
toast(`Exported ${lineCount} items as CSV`, 'success');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// For JSON/YAML: fetch from list-all with all fields, then format client-side
|
|
88
|
+
const data = await api('GET', `/projects/${projectId}/pm/list-all?limit=9999`);
|
|
89
|
+
const items = data.items || [];
|
|
90
|
+
if (items.length === 0) {
|
|
91
|
+
statusEl.innerHTML = '<div style="color:var(--text-muted);font-size:13px;padding:12px">No items to export</div>';
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let content, filename, mime;
|
|
95
|
+
if (format === 'yaml') {
|
|
96
|
+
// YAML serializer — no external dependency needed for this structure
|
|
97
|
+
const yamlVal = (v, indent) => {
|
|
98
|
+
const pad = ' '.repeat(indent);
|
|
99
|
+
if (v === null || v === undefined)
|
|
100
|
+
return 'null';
|
|
101
|
+
if (typeof v === 'boolean')
|
|
102
|
+
return v ? 'true' : 'false';
|
|
103
|
+
if (typeof v === 'number')
|
|
104
|
+
return String(v);
|
|
105
|
+
if (Array.isArray(v)) {
|
|
106
|
+
if (v.length === 0)
|
|
107
|
+
return '[]';
|
|
108
|
+
return '\n' + v.map((item) => `${pad}- ${yamlVal(item, indent + 1)}`).join('\n');
|
|
109
|
+
}
|
|
110
|
+
if (typeof v === 'object') {
|
|
111
|
+
const entries = Object.entries(v).filter(([, val]) => val !== null && val !== undefined);
|
|
112
|
+
if (entries.length === 0)
|
|
113
|
+
return '{}';
|
|
114
|
+
return '\n' + entries.map(([k, val]) => {
|
|
115
|
+
const valStr = yamlVal(val, indent + 1);
|
|
116
|
+
return valStr.startsWith('\n') ? `${pad}${k}:${valStr}` : `${pad}${k}: ${valStr}`;
|
|
117
|
+
}).join('\n');
|
|
118
|
+
}
|
|
119
|
+
// String: quote if contains special chars
|
|
120
|
+
const str = String(v);
|
|
121
|
+
if (str.includes('\n') || str.includes(':') || str.includes('#') || str.includes('"') || str.startsWith(' ') || str.endsWith(' ')) {
|
|
122
|
+
return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
123
|
+
}
|
|
124
|
+
return str || '""';
|
|
125
|
+
};
|
|
126
|
+
const yamlItems = items.map((item) => {
|
|
127
|
+
const itemObj = item;
|
|
128
|
+
const fields = Object.keys(itemObj).filter(f => itemObj[f] !== null && itemObj[f] !== undefined && itemObj[f] !== '');
|
|
129
|
+
const lines = fields.map(f => {
|
|
130
|
+
const valStr = yamlVal(itemObj[f], 1);
|
|
131
|
+
return valStr.startsWith('\n') ? ` ${f}:${valStr}` : ` ${f}: ${valStr}`;
|
|
132
|
+
});
|
|
133
|
+
return '- ' + lines.join('\n').trimStart();
|
|
134
|
+
});
|
|
135
|
+
const header = `# pm-web export\n# project: ${state.currentProject.name}\n# exported_at: ${exportedAt}\n# version: "2.0"\nitems:\n`;
|
|
136
|
+
content = header + yamlItems.join('\n');
|
|
137
|
+
filename = `${slug}-items.yaml`;
|
|
138
|
+
mime = 'text/yaml';
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
content = JSON.stringify({ exportedAt, project: state.currentProject.name, version: '2.0', items }, null, 2);
|
|
142
|
+
filename = `${slug}-items.json`;
|
|
143
|
+
mime = 'application/json';
|
|
144
|
+
}
|
|
145
|
+
downloadFile(content, filename, mime);
|
|
146
|
+
statusEl.innerHTML = `<div style="color:var(--status-closed);font-size:13px;padding:12px">✓ Exported ${items.length} items as ${format.toUpperCase()}</div>`;
|
|
147
|
+
toast(`Exported ${items.length} items`, 'success');
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
statusEl.innerHTML = `<div style="color:var(--status-blocked);font-size:13px;padding:12px">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div>`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function downloadFile(content, filename, mime) {
|
|
154
|
+
const blob = new Blob([content], { type: mime });
|
|
155
|
+
const url = URL.createObjectURL(blob);
|
|
156
|
+
const a = document.createElement('a');
|
|
157
|
+
a.href = url;
|
|
158
|
+
a.download = filename;
|
|
159
|
+
a.click();
|
|
160
|
+
URL.revokeObjectURL(url);
|
|
161
|
+
}
|
|
162
|
+
// Parse a minimal YAML items list into an array of objects
|
|
163
|
+
// Handles the pm-web export YAML format (flat key: value under "- " list items)
|
|
164
|
+
function parseYamlItems(text) {
|
|
165
|
+
const items = [];
|
|
166
|
+
// Strip comment lines and find items block
|
|
167
|
+
const lines = text.split('\n');
|
|
168
|
+
let inItems = false;
|
|
169
|
+
let current = null;
|
|
170
|
+
for (let i = 0; i < lines.length; i++) {
|
|
171
|
+
const line = lines[i];
|
|
172
|
+
const stripped = line.trimEnd();
|
|
173
|
+
// Skip comments and empty lines before items block
|
|
174
|
+
if (!inItems) {
|
|
175
|
+
if (stripped.match(/^items\s*:/)) {
|
|
176
|
+
inItems = true;
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// List item start
|
|
181
|
+
if (stripped.match(/^- /)) {
|
|
182
|
+
if (current)
|
|
183
|
+
items.push(current);
|
|
184
|
+
current = {};
|
|
185
|
+
const rest = stripped.slice(2).trim();
|
|
186
|
+
if (rest) {
|
|
187
|
+
const colonIdx = rest.indexOf(':');
|
|
188
|
+
if (colonIdx > 0) {
|
|
189
|
+
const k = rest.slice(0, colonIdx).trim();
|
|
190
|
+
const v = rest.slice(colonIdx + 1).trim();
|
|
191
|
+
current[k] = parseYamlValue(v);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (current && stripped.match(/^ [a-zA-Z_]/)) {
|
|
196
|
+
// Continuation key under current item
|
|
197
|
+
const inner = stripped.slice(2);
|
|
198
|
+
const colonIdx = inner.indexOf(':');
|
|
199
|
+
if (colonIdx > 0) {
|
|
200
|
+
const k = inner.slice(0, colonIdx).trim();
|
|
201
|
+
const v = inner.slice(colonIdx + 1).trim();
|
|
202
|
+
if (v) {
|
|
203
|
+
current[k] = parseYamlValue(v);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (current)
|
|
209
|
+
items.push(current);
|
|
210
|
+
return items;
|
|
211
|
+
}
|
|
212
|
+
function parseYamlValue(v) {
|
|
213
|
+
if (!v || v === 'null')
|
|
214
|
+
return null;
|
|
215
|
+
if (v === 'true')
|
|
216
|
+
return true;
|
|
217
|
+
if (v === 'false')
|
|
218
|
+
return false;
|
|
219
|
+
if (/^\d+(\.\d+)?$/.test(v))
|
|
220
|
+
return Number(v);
|
|
221
|
+
// Quoted string
|
|
222
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
223
|
+
return v.slice(1, -1).replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
224
|
+
}
|
|
225
|
+
// Inline list
|
|
226
|
+
if (v.startsWith('[') && v.endsWith(']')) {
|
|
227
|
+
return v.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
|
|
228
|
+
}
|
|
229
|
+
return v;
|
|
230
|
+
}
|
|
231
|
+
export async function importData(file) {
|
|
232
|
+
if (!file || !state.currentProject)
|
|
233
|
+
return;
|
|
234
|
+
const statusEl = document.getElementById('export-status');
|
|
235
|
+
if (!statusEl)
|
|
236
|
+
return;
|
|
237
|
+
statusEl.style.display = '';
|
|
238
|
+
statusEl.innerHTML = '<div class="loading-state" style="padding:16px"><div class="loading-spinner"></div></div>';
|
|
239
|
+
try {
|
|
240
|
+
const text = await file.text();
|
|
241
|
+
const name = file.name.toLowerCase();
|
|
242
|
+
let rawItems = [];
|
|
243
|
+
if (name.endsWith('.yaml') || name.endsWith('.yml')) {
|
|
244
|
+
rawItems = parseYamlItems(text);
|
|
245
|
+
if (rawItems.length === 0)
|
|
246
|
+
throw new Error('No items found in YAML file. Make sure the file has an "items:" list.');
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// JSON
|
|
250
|
+
const data = JSON.parse(text);
|
|
251
|
+
rawItems = Array.isArray(data) ? data : (Array.isArray(data.items) ? data.items : []);
|
|
252
|
+
if (rawItems.length === 0)
|
|
253
|
+
throw new Error('No items found in JSON file');
|
|
254
|
+
}
|
|
255
|
+
if (rawItems.length > 500) {
|
|
256
|
+
throw new Error(`File contains ${rawItems.length} items — maximum is 500 per import`);
|
|
257
|
+
}
|
|
258
|
+
const items = rawItems.map((item) => {
|
|
259
|
+
const i = item;
|
|
260
|
+
const mapped = {
|
|
261
|
+
title: String(i['title'] || 'Imported item'),
|
|
262
|
+
type: String(i['type'] || 'Task'),
|
|
263
|
+
priority: String(i['priority'] || 3),
|
|
264
|
+
};
|
|
265
|
+
if (i['description'])
|
|
266
|
+
mapped['description'] = String(i['description']);
|
|
267
|
+
if (i['status'])
|
|
268
|
+
mapped['status'] = String(i['status']);
|
|
269
|
+
if (i['tags'])
|
|
270
|
+
mapped['tags'] = Array.isArray(i['tags']) ? i['tags'].join(',') : String(i['tags']);
|
|
271
|
+
if (i['deadline'])
|
|
272
|
+
mapped['deadline'] = String(i['deadline']);
|
|
273
|
+
if (i['assignee'])
|
|
274
|
+
mapped['assignee'] = String(i['assignee']);
|
|
275
|
+
if (i['sprint'])
|
|
276
|
+
mapped['sprint'] = String(i['sprint']);
|
|
277
|
+
if (i['release'])
|
|
278
|
+
mapped['release'] = String(i['release']);
|
|
279
|
+
if (i['body'])
|
|
280
|
+
mapped['body'] = String(i['body']);
|
|
281
|
+
if (i['parent'])
|
|
282
|
+
mapped['parent'] = String(i['parent']);
|
|
283
|
+
if (i['estimate'])
|
|
284
|
+
mapped['estimate'] = String(i['estimate']);
|
|
285
|
+
return mapped;
|
|
286
|
+
});
|
|
287
|
+
const result = await api('POST', `/projects/${state.currentProject.id}/pm/import`, { items });
|
|
288
|
+
const created = result.created?.length ?? 0;
|
|
289
|
+
const failed = result.errors?.length ?? 0;
|
|
290
|
+
statusEl.innerHTML = `<div style="padding:12px;font-size:13px"><span style="color:var(--status-closed)">✓ Imported ${created} items</span>${failed ? `<span style="color:var(--status-blocked);margin-left:12px">✗ ${failed} failed</span>` : ''}</div>`;
|
|
291
|
+
toast(`Imported ${created} items`, 'success');
|
|
292
|
+
if (window.__app?.loadItemsBadge) {
|
|
293
|
+
window.__app.loadItemsBadge();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
statusEl.innerHTML = `<div style="color:var(--status-blocked);font-size:13px;padding:12px">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div>`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.js","sourceRoot":"","sources":["export.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// EXPORT / IMPORT VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml } from '../utils.js';
|
|
7
|
+
import { toast } from '../components/toast.js';
|
|
8
|
+
|
|
9
|
+
export async function renderExportView(): Promise<void> {
|
|
10
|
+
const el = document.getElementById('content-export');
|
|
11
|
+
if (!el) return;
|
|
12
|
+
if (!state.currentProject) { el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>'; return; }
|
|
13
|
+
el.innerHTML = `
|
|
14
|
+
<div class="page-header">
|
|
15
|
+
<div><div class="page-title">Export & Import</div><div class="page-subtitle">${escHtml(state.currentProject.name)}</div></div>
|
|
16
|
+
</div>
|
|
17
|
+
<div style="max-width:600px;display:flex;flex-direction:column;gap:16px">
|
|
18
|
+
<div class="export-card">
|
|
19
|
+
<div class="export-card-icon">📥</div>
|
|
20
|
+
<div class="export-card-info">
|
|
21
|
+
<div class="export-card-title">Export as JSON</div>
|
|
22
|
+
<div class="export-card-desc">Download all items with full metadata, body, tags, and all available fields</div>
|
|
23
|
+
</div>
|
|
24
|
+
<button class="btn btn-primary btn-sm" onclick="window.__app.exportData('json')"><span>Export JSON</span></button>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="export-card">
|
|
27
|
+
<div class="export-card-icon">📊</div>
|
|
28
|
+
<div class="export-card-info">
|
|
29
|
+
<div class="export-card-title">Export as CSV</div>
|
|
30
|
+
<div class="export-card-desc">Download items as a spreadsheet-compatible CSV file (includes body, parent, estimate)</div>
|
|
31
|
+
</div>
|
|
32
|
+
<button class="btn btn-secondary btn-sm" onclick="window.__app.exportData('csv')"><span>Export CSV</span></button>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="export-card">
|
|
35
|
+
<div class="export-card-icon">📋</div>
|
|
36
|
+
<div class="export-card-info">
|
|
37
|
+
<div class="export-card-title">Export as YAML</div>
|
|
38
|
+
<div class="export-card-desc">Download items as a human-readable YAML file with all available fields</div>
|
|
39
|
+
</div>
|
|
40
|
+
<button class="btn btn-secondary btn-sm" onclick="window.__app.exportData('yaml')"><span>Export YAML</span></button>
|
|
41
|
+
</div>
|
|
42
|
+
<hr class="section-divider">
|
|
43
|
+
<div class="export-card">
|
|
44
|
+
<div class="export-card-icon">📤</div>
|
|
45
|
+
<div class="export-card-info">
|
|
46
|
+
<div class="export-card-title">Import from JSON or YAML</div>
|
|
47
|
+
<div class="export-card-desc">Upload a previously exported JSON or YAML file to add items to this project (max 500 items)</div>
|
|
48
|
+
</div>
|
|
49
|
+
<label class="btn btn-secondary btn-sm" style="cursor:pointer">
|
|
50
|
+
<span>Choose File</span>
|
|
51
|
+
<input type="file" accept=".json,.yaml,.yml" style="display:none" onchange="window.__app.importData(this.files[0])">
|
|
52
|
+
</label>
|
|
53
|
+
</div>
|
|
54
|
+
<div id="export-status" style="display:none"></div>
|
|
55
|
+
</div>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function exportData(format: string): Promise<void> {
|
|
59
|
+
const statusEl = document.getElementById('export-status');
|
|
60
|
+
if (!state.currentProject || !statusEl) return;
|
|
61
|
+
statusEl.style.display = '';
|
|
62
|
+
statusEl.innerHTML = '<div class="loading-state" style="padding:16px"><div class="loading-spinner"></div></div>';
|
|
63
|
+
try {
|
|
64
|
+
// Use server-side export endpoint which fetches --full --include-body data
|
|
65
|
+
const projectId = state.currentProject.id;
|
|
66
|
+
const slug = state.currentProject.slug;
|
|
67
|
+
const exportedAt = new Date().toISOString();
|
|
68
|
+
|
|
69
|
+
if (format === 'csv') {
|
|
70
|
+
// Fetch via the dedicated server-side export endpoint for CSV
|
|
71
|
+
const resp = await fetch(`/api/projects/${encodeURIComponent(projectId)}/pm/export?format=csv`, {
|
|
72
|
+
credentials: 'include',
|
|
73
|
+
});
|
|
74
|
+
if (!resp.ok) {
|
|
75
|
+
const err = await resp.json().catch(() => ({ error: 'Export failed' })) as { error?: string };
|
|
76
|
+
throw new Error(err.error || `Export failed (${resp.status})`);
|
|
77
|
+
}
|
|
78
|
+
const content = await resp.text();
|
|
79
|
+
const lineCount = content.split('\n').length - 1; // subtract header
|
|
80
|
+
downloadFile(content, `${slug}-items.csv`, 'text/csv');
|
|
81
|
+
statusEl.innerHTML = `<div style="color:var(--status-closed);font-size:13px;padding:12px">✓ Exported ${lineCount} items as CSV</div>`;
|
|
82
|
+
toast(`Exported ${lineCount} items as CSV`, 'success');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// For JSON/YAML: fetch from list-all with all fields, then format client-side
|
|
87
|
+
const data = await api('GET', `/projects/${projectId}/pm/list-all?limit=9999`);
|
|
88
|
+
const items = (data as { items?: unknown[] }).items || [];
|
|
89
|
+
if (items.length === 0) { statusEl.innerHTML = '<div style="color:var(--text-muted);font-size:13px;padding:12px">No items to export</div>'; return; }
|
|
90
|
+
|
|
91
|
+
let content: string, filename: string, mime: string;
|
|
92
|
+
|
|
93
|
+
if (format === 'yaml') {
|
|
94
|
+
// YAML serializer — no external dependency needed for this structure
|
|
95
|
+
const yamlVal = (v: unknown, indent: number): string => {
|
|
96
|
+
const pad = ' '.repeat(indent);
|
|
97
|
+
if (v === null || v === undefined) return 'null';
|
|
98
|
+
if (typeof v === 'boolean') return v ? 'true' : 'false';
|
|
99
|
+
if (typeof v === 'number') return String(v);
|
|
100
|
+
if (Array.isArray(v)) {
|
|
101
|
+
if (v.length === 0) return '[]';
|
|
102
|
+
return '\n' + v.map((item) => `${pad}- ${yamlVal(item, indent + 1)}`).join('\n');
|
|
103
|
+
}
|
|
104
|
+
if (typeof v === 'object') {
|
|
105
|
+
const entries = Object.entries(v as Record<string, unknown>).filter(([, val]) => val !== null && val !== undefined);
|
|
106
|
+
if (entries.length === 0) return '{}';
|
|
107
|
+
return '\n' + entries.map(([k, val]) => {
|
|
108
|
+
const valStr = yamlVal(val, indent + 1);
|
|
109
|
+
return valStr.startsWith('\n') ? `${pad}${k}:${valStr}` : `${pad}${k}: ${valStr}`;
|
|
110
|
+
}).join('\n');
|
|
111
|
+
}
|
|
112
|
+
// String: quote if contains special chars
|
|
113
|
+
const str = String(v);
|
|
114
|
+
if (str.includes('\n') || str.includes(':') || str.includes('#') || str.includes('"') || str.startsWith(' ') || str.endsWith(' ')) {
|
|
115
|
+
return `"${str.replace(/\\/g,'\\\\').replace(/"/g,'\\"').replace(/\n/g,'\\n')}"`;
|
|
116
|
+
}
|
|
117
|
+
return str || '""';
|
|
118
|
+
};
|
|
119
|
+
const yamlItems = items.map((item) => {
|
|
120
|
+
const itemObj = item as Record<string, unknown>;
|
|
121
|
+
const fields = Object.keys(itemObj).filter(f => itemObj[f] !== null && itemObj[f] !== undefined && itemObj[f] !== '');
|
|
122
|
+
const lines: string[] = fields.map(f => {
|
|
123
|
+
const valStr = yamlVal(itemObj[f], 1);
|
|
124
|
+
return valStr.startsWith('\n') ? ` ${f}:${valStr}` : ` ${f}: ${valStr}`;
|
|
125
|
+
});
|
|
126
|
+
return '- ' + lines.join('\n').trimStart();
|
|
127
|
+
});
|
|
128
|
+
const header = `# pm-web export\n# project: ${state.currentProject.name}\n# exported_at: ${exportedAt}\n# version: "2.0"\nitems:\n`;
|
|
129
|
+
content = header + yamlItems.join('\n');
|
|
130
|
+
filename = `${slug}-items.yaml`;
|
|
131
|
+
mime = 'text/yaml';
|
|
132
|
+
} else {
|
|
133
|
+
content = JSON.stringify({ exportedAt, project: state.currentProject.name, version: '2.0', items }, null, 2);
|
|
134
|
+
filename = `${slug}-items.json`;
|
|
135
|
+
mime = 'application/json';
|
|
136
|
+
}
|
|
137
|
+
downloadFile(content, filename, mime);
|
|
138
|
+
statusEl.innerHTML = `<div style="color:var(--status-closed);font-size:13px;padding:12px">✓ Exported ${items.length} items as ${format.toUpperCase()}</div>`;
|
|
139
|
+
toast(`Exported ${items.length} items`, 'success');
|
|
140
|
+
} catch(err: unknown) {
|
|
141
|
+
statusEl.innerHTML = `<div style="color:var(--status-blocked);font-size:13px;padding:12px">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div>`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function downloadFile(content: string, filename: string, mime: string): void {
|
|
146
|
+
const blob = new Blob([content], { type: mime });
|
|
147
|
+
const url = URL.createObjectURL(blob);
|
|
148
|
+
const a = document.createElement('a');
|
|
149
|
+
a.href = url;
|
|
150
|
+
a.download = filename;
|
|
151
|
+
a.click();
|
|
152
|
+
URL.revokeObjectURL(url);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Parse a minimal YAML items list into an array of objects
|
|
156
|
+
// Handles the pm-web export YAML format (flat key: value under "- " list items)
|
|
157
|
+
function parseYamlItems(text: string): Record<string, unknown>[] {
|
|
158
|
+
const items: Record<string, unknown>[] = [];
|
|
159
|
+
// Strip comment lines and find items block
|
|
160
|
+
const lines = text.split('\n');
|
|
161
|
+
let inItems = false;
|
|
162
|
+
let current: Record<string, unknown> | null = null;
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < lines.length; i++) {
|
|
165
|
+
const line = lines[i]!;
|
|
166
|
+
const stripped = line.trimEnd();
|
|
167
|
+
|
|
168
|
+
// Skip comments and empty lines before items block
|
|
169
|
+
if (!inItems) {
|
|
170
|
+
if (stripped.match(/^items\s*:/)) { inItems = true; }
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// List item start
|
|
175
|
+
if (stripped.match(/^- /)) {
|
|
176
|
+
if (current) items.push(current);
|
|
177
|
+
current = {};
|
|
178
|
+
const rest = stripped.slice(2).trim();
|
|
179
|
+
if (rest) {
|
|
180
|
+
const colonIdx = rest.indexOf(':');
|
|
181
|
+
if (colonIdx > 0) {
|
|
182
|
+
const k = rest.slice(0, colonIdx).trim();
|
|
183
|
+
const v = rest.slice(colonIdx + 1).trim();
|
|
184
|
+
current[k] = parseYamlValue(v);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else if (current && stripped.match(/^ [a-zA-Z_]/)) {
|
|
188
|
+
// Continuation key under current item
|
|
189
|
+
const inner = stripped.slice(2);
|
|
190
|
+
const colonIdx = inner.indexOf(':');
|
|
191
|
+
if (colonIdx > 0) {
|
|
192
|
+
const k = inner.slice(0, colonIdx).trim();
|
|
193
|
+
const v = inner.slice(colonIdx + 1).trim();
|
|
194
|
+
if (v) {
|
|
195
|
+
current[k] = parseYamlValue(v);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (current) items.push(current);
|
|
201
|
+
return items;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseYamlValue(v: string): unknown {
|
|
205
|
+
if (!v || v === 'null') return null;
|
|
206
|
+
if (v === 'true') return true;
|
|
207
|
+
if (v === 'false') return false;
|
|
208
|
+
if (/^\d+(\.\d+)?$/.test(v)) return Number(v);
|
|
209
|
+
// Quoted string
|
|
210
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
211
|
+
return v.slice(1, -1).replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
212
|
+
}
|
|
213
|
+
// Inline list
|
|
214
|
+
if (v.startsWith('[') && v.endsWith(']')) {
|
|
215
|
+
return v.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
|
|
216
|
+
}
|
|
217
|
+
return v;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function importData(file: File): Promise<void> {
|
|
221
|
+
if (!file || !state.currentProject) return;
|
|
222
|
+
const statusEl = document.getElementById('export-status');
|
|
223
|
+
if (!statusEl) return;
|
|
224
|
+
statusEl.style.display = '';
|
|
225
|
+
statusEl.innerHTML = '<div class="loading-state" style="padding:16px"><div class="loading-spinner"></div></div>';
|
|
226
|
+
try {
|
|
227
|
+
const text = await file.text();
|
|
228
|
+
const name = file.name.toLowerCase();
|
|
229
|
+
let rawItems: unknown[] = [];
|
|
230
|
+
|
|
231
|
+
if (name.endsWith('.yaml') || name.endsWith('.yml')) {
|
|
232
|
+
rawItems = parseYamlItems(text);
|
|
233
|
+
if (rawItems.length === 0) throw new Error('No items found in YAML file. Make sure the file has an "items:" list.');
|
|
234
|
+
} else {
|
|
235
|
+
// JSON
|
|
236
|
+
const data = JSON.parse(text) as unknown;
|
|
237
|
+
rawItems = Array.isArray(data) ? data : (Array.isArray((data as { items?: unknown[] }).items) ? (data as { items: unknown[] }).items : []);
|
|
238
|
+
if (rawItems.length === 0) throw new Error('No items found in JSON file');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (rawItems.length > 500) { throw new Error(`File contains ${rawItems.length} items — maximum is 500 per import`); }
|
|
242
|
+
|
|
243
|
+
const items = rawItems.map((item: unknown) => {
|
|
244
|
+
const i = item as Record<string, unknown>;
|
|
245
|
+
const mapped: Record<string, string> = {
|
|
246
|
+
title: String(i['title'] || 'Imported item'),
|
|
247
|
+
type: String(i['type'] || 'Task'),
|
|
248
|
+
priority: String(i['priority'] || 3),
|
|
249
|
+
};
|
|
250
|
+
if (i['description']) mapped['description'] = String(i['description']);
|
|
251
|
+
if (i['status']) mapped['status'] = String(i['status']);
|
|
252
|
+
if (i['tags']) mapped['tags'] = Array.isArray(i['tags']) ? (i['tags'] as string[]).join(',') : String(i['tags']);
|
|
253
|
+
if (i['deadline']) mapped['deadline'] = String(i['deadline']);
|
|
254
|
+
if (i['assignee']) mapped['assignee'] = String(i['assignee']);
|
|
255
|
+
if (i['sprint']) mapped['sprint'] = String(i['sprint']);
|
|
256
|
+
if (i['release']) mapped['release'] = String(i['release']);
|
|
257
|
+
if (i['body']) mapped['body'] = String(i['body']);
|
|
258
|
+
if (i['parent']) mapped['parent'] = String(i['parent']);
|
|
259
|
+
if (i['estimate']) mapped['estimate'] = String(i['estimate']);
|
|
260
|
+
return mapped;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const result = await api('POST', `/projects/${state.currentProject.id}/pm/import`, { items }) as { created?: string[]; errors?: string[]; total?: number };
|
|
264
|
+
const created = result.created?.length ?? 0;
|
|
265
|
+
const failed = result.errors?.length ?? 0;
|
|
266
|
+
statusEl.innerHTML = `<div style="padding:12px;font-size:13px"><span style="color:var(--status-closed)">✓ Imported ${created} items</span>${failed ? `<span style="color:var(--status-blocked);margin-left:12px">✗ ${failed} failed</span>` : ''}</div>`;
|
|
267
|
+
toast(`Imported ${created} items`, 'success');
|
|
268
|
+
if ((window as unknown as { __app?: { loadItemsBadge?: () => void } }).__app?.loadItemsBadge) {
|
|
269
|
+
(window as unknown as { __app: { loadItemsBadge: () => void } }).__app.loadItemsBadge();
|
|
270
|
+
}
|
|
271
|
+
} catch(err: unknown) {
|
|
272
|
+
statusEl.innerHTML = `<div style="color:var(--status-blocked);font-size:13px;padding:12px">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div>`;
|
|
273
|
+
}
|
|
274
|
+
}
|