mdboard 1.3.0 → 2.1.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/bin.js +117 -59
- package/index.html +2161 -1579
- package/package.json +7 -5
- package/presets/kanban/api.json +91 -0
- package/presets/kanban/cli.json +69 -0
- package/presets/kanban/docs.json +29 -0
- package/presets/kanban/entities.json +128 -0
- package/presets/kanban/structure.json +15 -0
- package/presets/kanban/ui.json +86 -0
- package/presets/scrum/api.json +98 -0
- package/presets/scrum/cli.json +120 -0
- package/presets/scrum/docs.json +43 -0
- package/presets/scrum/entities.json +268 -0
- package/presets/scrum/structure.json +32 -0
- package/presets/scrum/ui.json +201 -0
- package/presets/shape-up/api.json +40 -0
- package/presets/shape-up/cli.json +44 -0
- package/presets/shape-up/docs.json +32 -0
- package/presets/shape-up/entities.json +140 -0
- package/presets/shape-up/structure.json +28 -0
- package/presets/shape-up/ui.json +114 -0
- package/src/cli/cli.js +186 -210
- package/src/cli/config.js +234 -0
- package/src/cli/init.js +128 -76
- package/src/cli/preset.js +849 -0
- package/src/cli/skill.js +417 -0
- package/src/cli/status.js +126 -96
- package/src/core/config.js +491 -38
- package/src/core/history.js +17 -1
- package/src/core/scanner.js +373 -463
- package/src/core/workspace.js +0 -15
- package/src/server/api.js +464 -741
- package/src/server/server.js +105 -130
- package/build.js +0 -44
- package/defaults.json +0 -43
- package/src/cli/sync.js +0 -194
- package/src/cli/theme.js +0 -142
- package/src/client/app.js +0 -266
- package/src/client/board.js +0 -157
- package/src/client/core.js +0 -331
- package/src/client/editor.js +0 -318
- package/src/client/history.js +0 -137
- package/src/client/metrics.js +0 -38
- package/src/client/milestones.js +0 -77
- package/src/client/notes.js +0 -183
- package/src/client/overview.js +0 -104
- package/src/client/panel.js +0 -637
- package/src/client/styles.css +0 -471
- package/src/client/table.js +0 -111
- package/src/client/template.html +0 -144
- package/src/client/themes.js +0 -261
- package/src/client/workspace.js +0 -164
- package/src/core/agent-scanner.js +0 -260
package/src/client/panel.js
DELETED
|
@@ -1,637 +0,0 @@
|
|
|
1
|
-
/* ══════════════════════════════════════════════════════════════
|
|
2
|
-
CRUD HELPERS
|
|
3
|
-
══════════════════════════════════════════════════════════════ */
|
|
4
|
-
function isSourceWritable() {
|
|
5
|
-
if (!hasWorkspace() || !D.activeSource) return true;
|
|
6
|
-
var src = D.config.workspace.sources.find(function(s) { return s.name === D.activeSource; });
|
|
7
|
-
return src ? !src.readonly : true;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function openCreateDialog(collection) {
|
|
11
|
-
var item = { _isNew: true };
|
|
12
|
-
|
|
13
|
-
if (collection === 'tasks') {
|
|
14
|
-
var ms = D.milestones.length > 0 ? (D.milestones[0].id || D.milestones[0]._dir || '') : '';
|
|
15
|
-
var ep = D.epics.length > 0 ? (D.epics[0]._dir || D.epics[0].id || '') : '';
|
|
16
|
-
item.title = '';
|
|
17
|
-
item.status = 'backlog';
|
|
18
|
-
item.priority = '';
|
|
19
|
-
item.points = null;
|
|
20
|
-
item.assigned = '';
|
|
21
|
-
item.sprint = '';
|
|
22
|
-
item.milestone = ms;
|
|
23
|
-
item.epic = ep;
|
|
24
|
-
item.content = '';
|
|
25
|
-
} else if (collection === 'milestones') {
|
|
26
|
-
item.title = '';
|
|
27
|
-
item.status = 'planned';
|
|
28
|
-
item.deadline = '';
|
|
29
|
-
item.content = '';
|
|
30
|
-
} else if (collection === 'epics') {
|
|
31
|
-
var ms = D.milestones.length > 0 ? (D.milestones[0]._dir || D.milestones[0].id || '') : '';
|
|
32
|
-
item.title = '';
|
|
33
|
-
item.status = 'active';
|
|
34
|
-
item.priority = '';
|
|
35
|
-
item.milestone = ms;
|
|
36
|
-
item.content = '';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
panelState = { open: true, type: collection, item: item, isCreate: true, editor: null };
|
|
40
|
-
renderPanel();
|
|
41
|
-
document.getElementById('detail-panel').classList.add('open');
|
|
42
|
-
document.getElementById('panel-overlay').classList.add('open');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* ══════════════════════════════════════════════════════════════
|
|
46
|
-
DETAIL PANEL — Notion-style: form top + editor bottom
|
|
47
|
-
══════════════════════════════════════════════════════════════ */
|
|
48
|
-
var panelState = { open: false, type: null, item: null, isCreate: false, editor: null };
|
|
49
|
-
|
|
50
|
-
function openPanel(type, item) {
|
|
51
|
-
panelState = { open: true, type: type, item: JSON.parse(JSON.stringify(item)), isCreate: false, editor: null };
|
|
52
|
-
renderPanel();
|
|
53
|
-
document.getElementById('detail-panel').classList.add('open');
|
|
54
|
-
document.getElementById('panel-overlay').classList.add('open');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function closePanel() {
|
|
58
|
-
if (panelState.editor) { destroyEditor(panelState.editor); panelState.editor = null; }
|
|
59
|
-
panelState = { open: false, type: null, item: null, editor: null };
|
|
60
|
-
document.getElementById('detail-panel').classList.remove('open');
|
|
61
|
-
document.getElementById('panel-overlay').classList.remove('open');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function renderPanel() {
|
|
65
|
-
var panel = document.getElementById('detail-panel');
|
|
66
|
-
var t = panelState.type;
|
|
67
|
-
var item = panelState.item;
|
|
68
|
-
var isCreate = panelState.isCreate;
|
|
69
|
-
if (!item) return;
|
|
70
|
-
|
|
71
|
-
var isReadonly = !isCreate && item.readonly;
|
|
72
|
-
|
|
73
|
-
var typeLabel;
|
|
74
|
-
if (t === 'tasks') typeLabel = ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task';
|
|
75
|
-
else if (t === 'epics') typeLabel = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.singular : 'Epic';
|
|
76
|
-
else typeLabel = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.singular : 'Milestone';
|
|
77
|
-
|
|
78
|
-
// ── Header ──
|
|
79
|
-
var html = '<div class="panel-header">' +
|
|
80
|
-
'<span class="panel-type">' + escHtml(isCreate ? 'New ' + typeLabel : typeLabel) + '</span>' +
|
|
81
|
-
'<span class="panel-item-id">' + escHtml(isCreate ? '' : (item.id || '')) + '</span>';
|
|
82
|
-
|
|
83
|
-
if (item.source && item.sourceColor) {
|
|
84
|
-
html += '<span class="pill" style="background:' + item.sourceColor + '20;color:' + item.sourceColor + ';font-size:10px">' + escHtml(item.sourceLabel || item.source) + '</span>';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
html += '<button class="panel-close" id="panel-close-btn">×</button></div>';
|
|
88
|
-
|
|
89
|
-
// ── Title (large, Notion-style) ──
|
|
90
|
-
html += '<div class="panel-title-wrap">' +
|
|
91
|
-
'<input type="text" id="p-title" class="panel-title-input" value="' + escHtml(item.title || '') + '" placeholder="Untitled"' + (isReadonly ? ' disabled' : '') + '>' +
|
|
92
|
-
'</div>';
|
|
93
|
-
|
|
94
|
-
// ── Properties (compact inline) ──
|
|
95
|
-
html += '<div class="panel-properties">';
|
|
96
|
-
|
|
97
|
-
// Properties toggle header
|
|
98
|
-
html += '<div class="prop-row prop-row-toggle" id="props-toggle"><span class="prop-label" style="font-weight:600"><span class="ai-toggle-arrow" id="props-arrow">▶</span> Properties</span><div class="prop-value"></div></div>';
|
|
99
|
-
html += '<div class="props-fields-body" id="props-fields-body" style="display:none">';
|
|
100
|
-
|
|
101
|
-
// Status
|
|
102
|
-
var statusOptions, statusLabels;
|
|
103
|
-
if (t === 'tasks' && D.config && D.config.statuses && D.config.statuses.task) {
|
|
104
|
-
statusOptions = D.config.statuses.task.map(function(s) { return s.key; });
|
|
105
|
-
statusLabels = {};
|
|
106
|
-
D.config.statuses.task.forEach(function(s) { statusLabels[s.key] = s.label; });
|
|
107
|
-
} else if (t === 'epics' && D.config && D.config.statuses && D.config.statuses.epic) {
|
|
108
|
-
statusOptions = D.config.statuses.epic.map(function(s) { return s.key; });
|
|
109
|
-
statusLabels = {};
|
|
110
|
-
D.config.statuses.epic.forEach(function(s) { statusLabels[s.key] = s.label; });
|
|
111
|
-
} else if (t === 'milestones' && D.config && D.config.statuses && D.config.statuses.milestone) {
|
|
112
|
-
statusOptions = D.config.statuses.milestone.map(function(s) { return s.key; });
|
|
113
|
-
statusLabels = {};
|
|
114
|
-
D.config.statuses.milestone.forEach(function(s) { statusLabels[s.key] = s.label; });
|
|
115
|
-
} else if (t === 'tasks') {
|
|
116
|
-
statusOptions = STATUSES;
|
|
117
|
-
statusLabels = STATUS_LABELS;
|
|
118
|
-
} else {
|
|
119
|
-
statusOptions = ['planned', 'active', 'completed', 'cancelled'];
|
|
120
|
-
statusLabels = { planned: 'Planned', active: 'Active', completed: 'Completed', cancelled: 'Cancelled' };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
html += '<div class="prop-row"><span class="prop-label">Status</span><div class="prop-value"><span id="p-status-icon">' + statusIcon(item.status) + '</span>' +
|
|
124
|
-
'<select id="p-status"' + (isReadonly ? ' disabled' : '') + '>' + statusOptions.map(function(s) {
|
|
125
|
-
return '<option value="' + s + '"' + (item.status === s ? ' selected' : '') + '>' + (statusLabels[s] || s) + '</option>';
|
|
126
|
-
}).join('') + '</select></div></div>';
|
|
127
|
-
|
|
128
|
-
if (t === 'tasks' || t === 'epics') {
|
|
129
|
-
html += '<div class="prop-row"><span class="prop-label">Priority</span><div class="prop-value"><span id="p-priority-icon">' + priorityIcon(item.priority) + '</span>' +
|
|
130
|
-
'<select id="p-priority"' + (isReadonly ? ' disabled' : '') + '><option value="">None</option>' + PRIORITIES.map(function(p) {
|
|
131
|
-
return '<option value="' + p + '"' + (item.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>';
|
|
132
|
-
}).join('') + '</select></div></div>';
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (t === 'tasks') {
|
|
136
|
-
html += '<div class="prop-row"><span class="prop-label">Points</span><div class="prop-value"><input type="number" id="p-points" min="0" max="100" value="' + (item.points != null ? item.points : '') + '" placeholder="0"' + (isReadonly ? ' disabled' : '') + '></div></div>';
|
|
137
|
-
|
|
138
|
-
var assignedVal = Array.isArray(item.assigned) ? item.assigned.join(', ') : (item.assigned || '');
|
|
139
|
-
html += '<div class="prop-row"><span class="prop-label">Assigned</span><div class="prop-value"><input type="text" id="p-assigned" value="' + escHtml(assignedVal) + '" placeholder="Unassigned"' + (isReadonly ? ' disabled' : '') + '></div></div>';
|
|
140
|
-
|
|
141
|
-
html += '<div class="prop-row"><span class="prop-label">Sprint</span><div class="prop-value"><select id="p-sprint"' + (isReadonly ? ' disabled' : '') + '><option value="">None</option>' +
|
|
142
|
-
D.allSprints.map(function(s) { return '<option value="' + escHtml(s.id || '') + '"' + (item.sprint === s.id ? ' selected' : '') + '>' + escHtml(s.id || '') + '</option>'; }).join('') +
|
|
143
|
-
'</select></div></div>';
|
|
144
|
-
|
|
145
|
-
if (isCreate) {
|
|
146
|
-
html += '<div class="prop-row"><span class="prop-label">Milestone</span><div class="prop-value"><select id="p-milestone">' +
|
|
147
|
-
D.milestones.map(function(m) { return '<option value="' + escHtml(m._dir || m.id || '') + '"' + (item.milestone === (m._dir || m.id) ? ' selected' : '') + '>' + escHtml(m.title || m.id || '') + '</option>'; }).join('') +
|
|
148
|
-
'</select></div></div>';
|
|
149
|
-
html += '<div class="prop-row"><span class="prop-label">Epic</span><div class="prop-value"><select id="p-epic-select">' +
|
|
150
|
-
D.epics.map(function(e) { return '<option value="' + escHtml(e._dir || e.id || '') + '"' + (item.epic === (e._dir || e.id) ? ' selected' : '') + '>' + escHtml(e.title || e.id || '') + '</option>'; }).join('') +
|
|
151
|
-
'</select></div></div>';
|
|
152
|
-
} else {
|
|
153
|
-
html += '<div class="prop-row"><span class="prop-label">Epic</span><div class="prop-value"><span class="prop-text">' + escHtml(item.epic || 'None') + '</span></div></div>';
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (t === 'milestones') {
|
|
158
|
-
html += '<div class="prop-row"><span class="prop-label">Deadline</span><div class="prop-value"><input type="date" id="p-deadline" value="' + fmtDate(item.deadline) + '"' + (isReadonly ? ' disabled' : '') + '></div></div>';
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (isCreate && (t === 'epics' || t === 'milestones')) {
|
|
162
|
-
if (t === 'epics') {
|
|
163
|
-
html += '<div class="prop-row"><span class="prop-label">Milestone</span><div class="prop-value"><select id="p-milestone">' +
|
|
164
|
-
D.milestones.map(function(m) { return '<option value="' + escHtml(m._dir || m.id || '') + '">' + escHtml(m.title || m.id || '') + '</option>'; }).join('') +
|
|
165
|
-
'</select></div></div>';
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Links
|
|
170
|
-
if (t === 'tasks' && !isCreate && item.links && Array.isArray(item.links) && item.links.length > 0) {
|
|
171
|
-
html += '<div class="prop-row"><span class="prop-label">Links</span><div class="prop-value"><div class="link-chips">';
|
|
172
|
-
item.links.forEach(function(link) {
|
|
173
|
-
var parts = String(link).split(':');
|
|
174
|
-
var srcName = parts.length === 2 ? parts[0] : null;
|
|
175
|
-
var srcColor = 'var(--accent)';
|
|
176
|
-
if (srcName && D.config && D.config.workspace && D.config.workspace.sources) {
|
|
177
|
-
var src = D.config.workspace.sources.find(function(s) { return s.name === srcName; });
|
|
178
|
-
if (src && src.color) srcColor = src.color;
|
|
179
|
-
}
|
|
180
|
-
html += '<span class="link-chip" data-link="' + escHtml(String(link)) + '">' +
|
|
181
|
-
'<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
|
|
182
|
-
escHtml(String(link)) + '</span>';
|
|
183
|
-
});
|
|
184
|
-
html += '</div></div></div>';
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Reverse links
|
|
188
|
-
if (t === 'tasks' && !isCreate && D.overviewLinks && D.overviewLinks.reverseLinks && item.id) {
|
|
189
|
-
var reverseRefs = D.overviewLinks.reverseLinks[item.id] || [];
|
|
190
|
-
if (reverseRefs.length > 0) {
|
|
191
|
-
html += '<div class="prop-row"><span class="prop-label">Referenced By</span><div class="prop-value"><div class="link-chips">';
|
|
192
|
-
reverseRefs.forEach(function(ref) {
|
|
193
|
-
var srcColor = ref.sourceColor || 'var(--accent)';
|
|
194
|
-
html += '<span class="link-chip" data-link="' + escHtml(ref.from || '') + '">' +
|
|
195
|
-
'<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
|
|
196
|
-
escHtml(ref.from || '') + '</span>';
|
|
197
|
-
});
|
|
198
|
-
html += '</div></div></div>';
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
html += '</div>'; // close props-fields-body
|
|
203
|
-
|
|
204
|
-
// ── AI Properties — integrated as prop-rows ──
|
|
205
|
-
var aiData = item.ai || {};
|
|
206
|
-
var aiOwnData = item.aiOwn || {};
|
|
207
|
-
var aiFieldDefs = [
|
|
208
|
-
{ key: 'skills', label: 'Skills', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>' },
|
|
209
|
-
{ key: 'agents', label: 'Agents', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="3"/><circle cx="9" cy="16" r="1" fill="currentColor"/><circle cx="15" cy="16" r="1" fill="currentColor"/></svg>' },
|
|
210
|
-
{ key: 'mcps', label: 'MCPs', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16v16H4z"/><path d="M9 9h6v6H9z"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3"/></svg>' },
|
|
211
|
-
{ key: 'commands', label: 'Commands', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>' },
|
|
212
|
-
{ key: 'context', label: 'Context', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' }
|
|
213
|
-
];
|
|
214
|
-
var aiTotalCount = 0;
|
|
215
|
-
for (var ai = 0; ai < aiFieldDefs.length; ai++) {
|
|
216
|
-
var arr = aiData[aiFieldDefs[ai].key];
|
|
217
|
-
if (arr) aiTotalCount += arr.length;
|
|
218
|
-
}
|
|
219
|
-
var showAiSection = aiTotalCount > 0 || !isReadonly;
|
|
220
|
-
if (showAiSection) {
|
|
221
|
-
// AI divider row — clickable toggle
|
|
222
|
-
html += '<div class="prop-row prop-row-divider prop-row-toggle" id="ai-toggle"><span class="prop-label" style="font-weight:600"><span class="ai-toggle-arrow" id="ai-arrow">▶</span> AI</span><div class="prop-value">';
|
|
223
|
-
if (aiTotalCount > 0) html += '<span class="pill" style="font-size:10px;padding:1px 6px">' + aiTotalCount + '</span>';
|
|
224
|
-
html += '</div></div>';
|
|
225
|
-
html += '<div class="ai-fields-body" id="ai-fields-body" style="display:none">';
|
|
226
|
-
for (var ag = 0; ag < aiFieldDefs.length; ag++) {
|
|
227
|
-
var def = aiFieldDefs[ag];
|
|
228
|
-
var resolved = aiData[def.key] || [];
|
|
229
|
-
var own = aiOwnData[def.key] || [];
|
|
230
|
-
var inherited = resolved.filter(function(v) { return own.indexOf(v) === -1; });
|
|
231
|
-
if (isReadonly && resolved.length === 0) continue;
|
|
232
|
-
html += '<div class="prop-row"><span class="prop-label">' + def.icon + ' ' + escHtml(def.label) + '</span><div class="prop-value">';
|
|
233
|
-
if (isReadonly) {
|
|
234
|
-
// Readonly: just show tags
|
|
235
|
-
for (var ci = 0; ci < resolved.length; ci++) {
|
|
236
|
-
var isOwn = own.indexOf(resolved[ci]) !== -1;
|
|
237
|
-
html += '<span class="ai-tag' + (isOwn ? ' ai-tag-own' : ' ai-tag-inherited') + '">' + escHtml(resolved[ci]) + '</span>';
|
|
238
|
-
}
|
|
239
|
-
} else {
|
|
240
|
-
// Editable: tag input container
|
|
241
|
-
html += '<div class="ai-tag-input" data-ai-field="' + def.key + '">';
|
|
242
|
-
// Inherited tags (dashed, no remove)
|
|
243
|
-
for (var ii = 0; ii < inherited.length; ii++) {
|
|
244
|
-
html += '<span class="ai-tag ai-tag-inherited">' + escHtml(inherited[ii]) + '</span>';
|
|
245
|
-
}
|
|
246
|
-
// Own tags (accent, with remove button)
|
|
247
|
-
for (var oi = 0; oi < own.length; oi++) {
|
|
248
|
-
html += '<span class="ai-tag ai-tag-own"><span class="ai-tag-val">' + escHtml(own[oi]) + '</span><button class="ai-tag-remove" type="button">×</button></span>';
|
|
249
|
-
}
|
|
250
|
-
// Text input for adding
|
|
251
|
-
html += '<input type="text" class="ai-tag-text" placeholder="Add...">';
|
|
252
|
-
// Autocomplete dropdown
|
|
253
|
-
html += '<div class="ai-autocomplete"></div>';
|
|
254
|
-
html += '</div>';
|
|
255
|
-
}
|
|
256
|
-
html += '</div></div>';
|
|
257
|
-
}
|
|
258
|
-
html += '</div>'; // close ai-fields-body
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
html += '</div>'; // close panel-properties
|
|
262
|
-
|
|
263
|
-
// ── Content Editor (fills remaining space) ──
|
|
264
|
-
if (isReadonly) {
|
|
265
|
-
html += '<div class="panel-content-area"><div id="p-content-editor" class="panel-editor-zone md-rendered">' + simpleMarkdownRender(item.content || '') + '</div></div>';
|
|
266
|
-
} else {
|
|
267
|
-
html += '<div class="panel-content-area"><div id="p-content-editor" class="panel-editor-zone"></div></div>';
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ── Footer ──
|
|
271
|
-
html += '<div class="panel-footer">';
|
|
272
|
-
if (!isCreate && !isReadonly && isSourceWritable()) {
|
|
273
|
-
html += '<button class="btn btn-danger btn-sm" id="panel-archive-btn">Archive</button>';
|
|
274
|
-
}
|
|
275
|
-
html += '<span style="flex:1"></span>';
|
|
276
|
-
html += '<button class="btn" id="panel-cancel-btn">Cancel</button>';
|
|
277
|
-
if (!isReadonly) {
|
|
278
|
-
html += '<button class="btn btn-primary" id="panel-save-btn">' + (isCreate ? 'Create' : 'Save') + '</button>';
|
|
279
|
-
}
|
|
280
|
-
html += '</div>';
|
|
281
|
-
|
|
282
|
-
panel.innerHTML = html;
|
|
283
|
-
|
|
284
|
-
// Initialize Editor.js for content (if not readonly)
|
|
285
|
-
if (!isReadonly) {
|
|
286
|
-
panelState.editor = initEditor('p-content-editor', item.content || '', {
|
|
287
|
-
placeholder: 'Type / for commands, or start writing...',
|
|
288
|
-
minHeight: 0,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Event listeners
|
|
293
|
-
document.getElementById('panel-close-btn').addEventListener('click', closePanel);
|
|
294
|
-
document.getElementById('panel-cancel-btn').addEventListener('click', closePanel);
|
|
295
|
-
var saveBtn = document.getElementById('panel-save-btn');
|
|
296
|
-
if (saveBtn) saveBtn.addEventListener('click', isCreate ? saveCreatePanel : savePanel);
|
|
297
|
-
|
|
298
|
-
var archiveBtn = document.getElementById('panel-archive-btn');
|
|
299
|
-
if (archiveBtn) {
|
|
300
|
-
archiveBtn.addEventListener('click', function() {
|
|
301
|
-
if (!confirm('Archive this ' + typeLabel.toLowerCase() + '?')) return;
|
|
302
|
-
deleteItem(t, item.id).then(function(ok) {
|
|
303
|
-
if (ok) { closePanel(); refreshData(); }
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Properties section toggle
|
|
309
|
-
var propsToggle = document.getElementById('props-toggle');
|
|
310
|
-
if (propsToggle) {
|
|
311
|
-
propsToggle.addEventListener('click', function() {
|
|
312
|
-
var body = document.getElementById('props-fields-body');
|
|
313
|
-
var arrow = document.getElementById('props-arrow');
|
|
314
|
-
if (body.style.display === 'none') {
|
|
315
|
-
body.style.display = '';
|
|
316
|
-
arrow.innerHTML = '▼';
|
|
317
|
-
} else {
|
|
318
|
-
body.style.display = 'none';
|
|
319
|
-
arrow.innerHTML = '▶';
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// AI section toggle
|
|
325
|
-
var aiToggle = document.getElementById('ai-toggle');
|
|
326
|
-
if (aiToggle) {
|
|
327
|
-
aiToggle.addEventListener('click', function() {
|
|
328
|
-
var body = document.getElementById('ai-fields-body');
|
|
329
|
-
var arrow = document.getElementById('ai-arrow');
|
|
330
|
-
if (body.style.display === 'none') {
|
|
331
|
-
body.style.display = '';
|
|
332
|
-
arrow.innerHTML = '▼';
|
|
333
|
-
} else {
|
|
334
|
-
body.style.display = 'none';
|
|
335
|
-
arrow.innerHTML = '▶';
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// AI tag input event listeners
|
|
341
|
-
panel.querySelectorAll('.ai-tag-input').forEach(function(container) {
|
|
342
|
-
var field = container.dataset.aiField;
|
|
343
|
-
var textInput = container.querySelector('.ai-tag-text');
|
|
344
|
-
var dropdown = container.querySelector('.ai-autocomplete');
|
|
345
|
-
if (!textInput || !dropdown) return;
|
|
346
|
-
|
|
347
|
-
function getOwnValues() {
|
|
348
|
-
var vals = [];
|
|
349
|
-
container.querySelectorAll('.ai-tag-own .ai-tag-val').forEach(function(el) {
|
|
350
|
-
vals.push(el.textContent);
|
|
351
|
-
});
|
|
352
|
-
return vals;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function getAllValues() {
|
|
356
|
-
var vals = [];
|
|
357
|
-
container.querySelectorAll('.ai-tag').forEach(function(el) {
|
|
358
|
-
var valEl = el.querySelector('.ai-tag-val');
|
|
359
|
-
vals.push(valEl ? valEl.textContent : el.textContent);
|
|
360
|
-
});
|
|
361
|
-
return vals;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function addTag(value) {
|
|
365
|
-
var v = value.trim();
|
|
366
|
-
if (!v) return;
|
|
367
|
-
var existing = getAllValues();
|
|
368
|
-
if (existing.indexOf(v) !== -1) return;
|
|
369
|
-
var tag = document.createElement('span');
|
|
370
|
-
tag.className = 'ai-tag ai-tag-own';
|
|
371
|
-
tag.innerHTML = '<span class="ai-tag-val">' + escHtml(v) + '</span><button class="ai-tag-remove" type="button">×</button>';
|
|
372
|
-
tag.querySelector('.ai-tag-remove').addEventListener('click', function() { tag.remove(); });
|
|
373
|
-
container.insertBefore(tag, textInput);
|
|
374
|
-
textInput.value = '';
|
|
375
|
-
closeDropdown();
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function closeDropdown() {
|
|
379
|
-
dropdown.classList.remove('open');
|
|
380
|
-
dropdown.innerHTML = '';
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function showDropdown() {
|
|
384
|
-
var suggestions = D.aiSuggestions ? (D.aiSuggestions[field] || []) : [];
|
|
385
|
-
var present = getAllValues();
|
|
386
|
-
var query = textInput.value.trim().toLowerCase();
|
|
387
|
-
var filtered = suggestions.filter(function(s) {
|
|
388
|
-
return present.indexOf(s) === -1 && (!query || s.toLowerCase().indexOf(query) !== -1);
|
|
389
|
-
});
|
|
390
|
-
if (filtered.length === 0) { closeDropdown(); return; }
|
|
391
|
-
dropdown.innerHTML = '';
|
|
392
|
-
filtered.forEach(function(s) {
|
|
393
|
-
var item = document.createElement('div');
|
|
394
|
-
item.className = 'ai-autocomplete-item';
|
|
395
|
-
item.textContent = s;
|
|
396
|
-
item.addEventListener('mousedown', function(e) {
|
|
397
|
-
e.preventDefault();
|
|
398
|
-
addTag(s);
|
|
399
|
-
});
|
|
400
|
-
dropdown.appendChild(item);
|
|
401
|
-
});
|
|
402
|
-
dropdown.classList.add('open');
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Click on × to remove own tag
|
|
406
|
-
container.querySelectorAll('.ai-tag-remove').forEach(function(btn) {
|
|
407
|
-
btn.addEventListener('click', function() { btn.closest('.ai-tag').remove(); });
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// Click on container focuses input
|
|
411
|
-
container.addEventListener('click', function(e) {
|
|
412
|
-
if (e.target === container) textInput.focus();
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// Input typing → show autocomplete
|
|
416
|
-
textInput.addEventListener('input', showDropdown);
|
|
417
|
-
textInput.addEventListener('focus', showDropdown);
|
|
418
|
-
|
|
419
|
-
// Enter → add tag
|
|
420
|
-
textInput.addEventListener('keydown', function(e) {
|
|
421
|
-
if (e.key === 'Enter') {
|
|
422
|
-
e.preventDefault();
|
|
423
|
-
if (textInput.value.trim()) addTag(textInput.value);
|
|
424
|
-
} else if (e.key === 'Backspace' && textInput.value === '') {
|
|
425
|
-
// Remove last own tag
|
|
426
|
-
var ownTags = container.querySelectorAll('.ai-tag-own');
|
|
427
|
-
if (ownTags.length > 0) ownTags[ownTags.length - 1].remove();
|
|
428
|
-
} else if (e.key === 'Escape') {
|
|
429
|
-
closeDropdown();
|
|
430
|
-
textInput.blur();
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
// Close dropdown on blur
|
|
435
|
-
textInput.addEventListener('blur', function() {
|
|
436
|
-
setTimeout(closeDropdown, 150);
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
// Live icon updates
|
|
441
|
-
var statusSel = document.getElementById('p-status');
|
|
442
|
-
if (statusSel) {
|
|
443
|
-
statusSel.addEventListener('change', function() {
|
|
444
|
-
var iconEl = document.getElementById('p-status-icon');
|
|
445
|
-
if (iconEl) iconEl.innerHTML = statusIcon(statusSel.value);
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
var prioritySel = document.getElementById('p-priority');
|
|
449
|
-
if (prioritySel) {
|
|
450
|
-
prioritySel.addEventListener('change', function() {
|
|
451
|
-
var iconEl = document.getElementById('p-priority-icon');
|
|
452
|
-
if (iconEl) iconEl.innerHTML = priorityIcon(prioritySel.value);
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Ctrl/Cmd+S to save
|
|
457
|
-
panel.addEventListener('keydown', function(e) {
|
|
458
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
459
|
-
e.preventDefault();
|
|
460
|
-
if (isCreate) saveCreatePanel();
|
|
461
|
-
else savePanel();
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
// Link chip click handlers
|
|
466
|
-
panel.querySelectorAll('.link-chip[data-link]').forEach(function(chip) {
|
|
467
|
-
chip.addEventListener('click', function() {
|
|
468
|
-
var ref = chip.dataset.link;
|
|
469
|
-
var parts = ref.split(':');
|
|
470
|
-
if (parts.length === 2 && hasWorkspace()) {
|
|
471
|
-
closePanel();
|
|
472
|
-
switchSource(parts[0]);
|
|
473
|
-
setTimeout(function() {
|
|
474
|
-
var task = D.tasks.find(function(t) { return t.id === ref || t.id === parts[1]; });
|
|
475
|
-
if (task) openPanel('tasks', task);
|
|
476
|
-
}, 500);
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
async function savePanel() {
|
|
483
|
-
var t = panelState.type;
|
|
484
|
-
var item = panelState.item;
|
|
485
|
-
if (!t || !item) return;
|
|
486
|
-
|
|
487
|
-
var updates = {};
|
|
488
|
-
|
|
489
|
-
var titleEl = document.getElementById('p-title');
|
|
490
|
-
if (titleEl && titleEl.value !== (item.title || '')) updates.title = titleEl.value;
|
|
491
|
-
|
|
492
|
-
var statusEl = document.getElementById('p-status');
|
|
493
|
-
if (statusEl && statusEl.value !== (item.status || '')) updates.status = statusEl.value;
|
|
494
|
-
|
|
495
|
-
if (t === 'tasks' || t === 'epics') {
|
|
496
|
-
var priorityEl = document.getElementById('p-priority');
|
|
497
|
-
if (priorityEl && priorityEl.value !== (item.priority || '')) updates.priority = priorityEl.value || null;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (t === 'tasks') {
|
|
501
|
-
var pointsEl = document.getElementById('p-points');
|
|
502
|
-
if (pointsEl) {
|
|
503
|
-
var pv = pointsEl.value ? Number(pointsEl.value) : null;
|
|
504
|
-
if (pv !== item.points) updates.points = pv;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
var assignedEl = document.getElementById('p-assigned');
|
|
508
|
-
if (assignedEl) {
|
|
509
|
-
var av = assignedEl.value.trim();
|
|
510
|
-
var origAssigned = Array.isArray(item.assigned) ? item.assigned.join(', ') : (item.assigned || '');
|
|
511
|
-
if (av !== origAssigned) updates.assigned = av || null;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
var sprintEl = document.getElementById('p-sprint');
|
|
515
|
-
if (sprintEl && sprintEl.value !== (item.sprint || '')) updates.sprint = sprintEl.value || null;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (t === 'milestones') {
|
|
519
|
-
var deadlineEl = document.getElementById('p-deadline');
|
|
520
|
-
if (deadlineEl && deadlineEl.value !== fmtDate(item.deadline)) updates.deadline = deadlineEl.value || null;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (panelState.editor) {
|
|
524
|
-
var newContent = await getEditorMarkdown(panelState.editor);
|
|
525
|
-
if (newContent !== (item.content || '')) updates.content = newContent;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// AI properties — read from tag inputs
|
|
529
|
-
var aiTagInputs = document.querySelectorAll('.ai-tag-input[data-ai-field]');
|
|
530
|
-
if (aiTagInputs.length > 0) {
|
|
531
|
-
var aiObj = {};
|
|
532
|
-
aiTagInputs.forEach(function(container) {
|
|
533
|
-
var field = container.dataset.aiField;
|
|
534
|
-
var vals = [];
|
|
535
|
-
container.querySelectorAll('.ai-tag-own .ai-tag-val').forEach(function(el) {
|
|
536
|
-
var v = el.textContent.trim();
|
|
537
|
-
if (v) vals.push(v);
|
|
538
|
-
});
|
|
539
|
-
if (vals.length > 0) aiObj[field] = vals;
|
|
540
|
-
});
|
|
541
|
-
if (Object.keys(aiObj).length > 0) {
|
|
542
|
-
updates.ai = aiObj;
|
|
543
|
-
} else if (item.aiOwn && Object.keys(item.aiOwn).length > 0) {
|
|
544
|
-
updates.ai = null;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (Object.keys(updates).length === 0) {
|
|
549
|
-
showToast('No changes to save', 'success');
|
|
550
|
-
closePanel();
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
var ok = await patchItem(t, item.id, updates);
|
|
555
|
-
if (ok) {
|
|
556
|
-
closePanel();
|
|
557
|
-
refreshData();
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
async function saveCreatePanel() {
|
|
562
|
-
var t = panelState.type;
|
|
563
|
-
if (!t) return;
|
|
564
|
-
|
|
565
|
-
var data = {};
|
|
566
|
-
|
|
567
|
-
var titleEl = document.getElementById('p-title');
|
|
568
|
-
if (titleEl) data.title = titleEl.value || 'Untitled';
|
|
569
|
-
|
|
570
|
-
var statusEl = document.getElementById('p-status');
|
|
571
|
-
if (statusEl) data.status = statusEl.value;
|
|
572
|
-
|
|
573
|
-
if (t === 'tasks' || t === 'epics') {
|
|
574
|
-
var priorityEl = document.getElementById('p-priority');
|
|
575
|
-
if (priorityEl && priorityEl.value) data.priority = priorityEl.value;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
if (t === 'tasks') {
|
|
579
|
-
var pointsEl = document.getElementById('p-points');
|
|
580
|
-
if (pointsEl && pointsEl.value) data.points = Number(pointsEl.value);
|
|
581
|
-
|
|
582
|
-
var assignedEl = document.getElementById('p-assigned');
|
|
583
|
-
if (assignedEl && assignedEl.value.trim()) data.assigned = assignedEl.value.trim();
|
|
584
|
-
|
|
585
|
-
var sprintEl = document.getElementById('p-sprint');
|
|
586
|
-
if (sprintEl && sprintEl.value) data.sprint = sprintEl.value;
|
|
587
|
-
|
|
588
|
-
var milestoneEl = document.getElementById('p-milestone');
|
|
589
|
-
if (milestoneEl) data.milestone = milestoneEl.value;
|
|
590
|
-
|
|
591
|
-
var epicEl = document.getElementById('p-epic-select');
|
|
592
|
-
if (epicEl) data.epic = epicEl.value;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (t === 'epics' || t === 'milestones') {
|
|
596
|
-
var milestoneEl = document.getElementById('p-milestone');
|
|
597
|
-
if (milestoneEl) data.milestone = milestoneEl.value;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (t === 'milestones') {
|
|
601
|
-
var deadlineEl = document.getElementById('p-deadline');
|
|
602
|
-
if (deadlineEl && deadlineEl.value) data.deadline = deadlineEl.value;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
if (panelState.editor) {
|
|
606
|
-
var editorContent = await getEditorMarkdown(panelState.editor);
|
|
607
|
-
if (editorContent) data.content = editorContent;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// AI properties — read from tag inputs
|
|
611
|
-
var aiTagInputs = document.querySelectorAll('.ai-tag-input[data-ai-field]');
|
|
612
|
-
if (aiTagInputs.length > 0) {
|
|
613
|
-
var aiObj = {};
|
|
614
|
-
aiTagInputs.forEach(function(container) {
|
|
615
|
-
var field = container.dataset.aiField;
|
|
616
|
-
var vals = [];
|
|
617
|
-
container.querySelectorAll('.ai-tag-own .ai-tag-val').forEach(function(el) {
|
|
618
|
-
var v = el.textContent.trim();
|
|
619
|
-
if (v) vals.push(v);
|
|
620
|
-
});
|
|
621
|
-
if (vals.length > 0) aiObj[field] = vals;
|
|
622
|
-
});
|
|
623
|
-
if (Object.keys(aiObj).length > 0) data.ai = aiObj;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
var result = await createItem(t, data);
|
|
627
|
-
if (result) {
|
|
628
|
-
closePanel();
|
|
629
|
-
refreshData();
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Close panel on overlay click or Escape
|
|
634
|
-
document.getElementById('panel-overlay').addEventListener('click', closePanel);
|
|
635
|
-
document.addEventListener('keydown', function(e) {
|
|
636
|
-
if (e.key === 'Escape' && panelState.open) closePanel();
|
|
637
|
-
});
|