@zeyos/client 0.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +458 -0
- package/agents/README.md +66 -0
- package/agents/shared/business-app-benchmarks.md +111 -0
- package/agents/shared/zeyos-entity-map.md +142 -0
- package/agents/shared/zeyos-entity-reference.md +570 -0
- package/agents/shared/zeyos-query-patterns.md +89 -0
- package/agents/zeyos-account-intelligence/SKILL.md +34 -0
- package/agents/zeyos-account-intelligence/agents/openai.yaml +4 -0
- package/agents/zeyos-account-intelligence/references/workflows.md +84 -0
- package/agents/zeyos-billing-insights/SKILL.md +41 -0
- package/agents/zeyos-billing-insights/agents/openai.yaml +4 -0
- package/agents/zeyos-billing-insights/references/workflows.md +106 -0
- package/agents/zeyos-campaign-and-outreach/SKILL.md +44 -0
- package/agents/zeyos-campaign-and-outreach/agents/openai.yaml +4 -0
- package/agents/zeyos-campaign-and-outreach/references/workflows.md +100 -0
- package/agents/zeyos-collaboration-and-activity/SKILL.md +37 -0
- package/agents/zeyos-collaboration-and-activity/agents/openai.yaml +4 -0
- package/agents/zeyos-collaboration-and-activity/references/workflows.md +104 -0
- package/agents/zeyos-collections-and-dunning/SKILL.md +46 -0
- package/agents/zeyos-collections-and-dunning/agents/openai.yaml +4 -0
- package/agents/zeyos-collections-and-dunning/references/workflows.md +132 -0
- package/agents/zeyos-commerce-and-inventory/SKILL.md +38 -0
- package/agents/zeyos-commerce-and-inventory/agents/openai.yaml +4 -0
- package/agents/zeyos-commerce-and-inventory/references/workflows.md +101 -0
- package/agents/zeyos-mail-operations/SKILL.md +35 -0
- package/agents/zeyos-mail-operations/agents/openai.yaml +4 -0
- package/agents/zeyos-mail-operations/references/workflows.md +110 -0
- package/agents/zeyos-notes-and-sops/SKILL.md +31 -0
- package/agents/zeyos-notes-and-sops/agents/openai.yaml +4 -0
- package/agents/zeyos-notes-and-sops/references/workflows.md +85 -0
- package/agents/zeyos-platform-and-schema/SKILL.md +37 -0
- package/agents/zeyos-platform-and-schema/agents/openai.yaml +4 -0
- package/agents/zeyos-platform-and-schema/references/workflows.md +97 -0
- package/agents/zeyos-work-management/SKILL.md +45 -0
- package/agents/zeyos-work-management/agents/openai.yaml +4 -0
- package/agents/zeyos-work-management/references/workflows.md +148 -0
- package/docs/01-api-reference/01-data-retrieval.md +601 -0
- package/docs/01-api-reference/02-authentication.md +288 -0
- package/docs/01-api-reference/03-resources.md +270 -0
- package/docs/01-api-reference/04-schema.md +539 -0
- package/docs/01-api-reference/_category_.json +9 -0
- package/docs/02-javascript-client/01-getting-started.md +146 -0
- package/docs/02-javascript-client/02-authentication.md +287 -0
- package/docs/02-javascript-client/03-making-requests.md +572 -0
- package/docs/02-javascript-client/04-practical-guide.md +348 -0
- package/docs/02-javascript-client/_category_.json +9 -0
- package/docs/03-cli/01-getting-started.md +219 -0
- package/docs/03-cli/02-commands.md +407 -0
- package/docs/03-cli/03-configuration.md +220 -0
- package/docs/03-cli/_category_.json +9 -0
- package/docs/04-agent-workflows/00-coding-agents.md +35 -0
- package/docs/04-agent-workflows/01-agent-quickstart.md +147 -0
- package/docs/04-agent-workflows/02-agent-recipes.md +109 -0
- package/docs/04-agent-workflows/03-cli-coverage-and-escalation.md +65 -0
- package/docs/04-agent-workflows/_category_.json +9 -0
- package/docs/04-sample-apps/01-kanban.md +89 -0
- package/docs/04-sample-apps/02-crm.md +81 -0
- package/docs/04-sample-apps/03-dashboard.md +80 -0
- package/docs/04-sample-apps/_category_.json +9 -0
- package/docs/05-tutorials/00-application-developers.md +43 -0
- package/docs/05-tutorials/01-integration-architecture.md +60 -0
- package/docs/05-tutorials/02-build-your-own-zeyos-frontend.md +517 -0
- package/docs/05-tutorials/03-server-side-integrations.md +185 -0
- package/docs/05-tutorials/_category_.json +9 -0
- package/docs/intro.md +197 -0
- package/openapi/api.json +24308 -0
- package/openapi/auth.json +415 -0
- package/openapi/dbref.json +56223 -0
- package/openapi/oauth2.json +781 -0
- package/openapi/sdk.json +949 -0
- package/openapi/views.txt +642 -0
- package/package.json +49 -0
- package/samples/crm/README.md +28 -0
- package/samples/crm/index.html +327 -0
- package/samples/crm/js/api.js +208 -0
- package/samples/crm/js/auth.js +61 -0
- package/samples/crm/js/main.js +545 -0
- package/samples/crm/js/state.js +90 -0
- package/samples/crm/js/ui.js +51 -0
- package/samples/dashboard/README.md +28 -0
- package/samples/dashboard/index.html +280 -0
- package/samples/dashboard/js/api.js +197 -0
- package/samples/dashboard/js/auth.js +59 -0
- package/samples/dashboard/js/main.js +382 -0
- package/samples/dashboard/js/state.js +81 -0
- package/samples/dashboard/js/ui.js +48 -0
- package/samples/kanban/README.md +28 -0
- package/samples/kanban/index.html +263 -0
- package/samples/kanban/js/api.js +152 -0
- package/samples/kanban/js/auth.js +59 -0
- package/samples/kanban/js/constants.js +40 -0
- package/samples/kanban/js/kanban.js +246 -0
- package/samples/kanban/js/main.js +362 -0
- package/samples/kanban/js/modals.js +474 -0
- package/samples/kanban/js/settings.js +82 -0
- package/samples/kanban/js/state.js +118 -0
- package/samples/kanban/js/ui.js +49 -0
- package/scripts/generate-client.mjs +344 -0
- package/src/generated/operations.js +9772 -0
- package/src/generated/schema.js +8982 -0
- package/src/index.js +85 -0
- package/src/runtime/client.js +1208 -0
- package/src/runtime/error.js +29 -0
- package/src/runtime/http.js +174 -0
- package/src/runtime/request-shape.js +35 -0
- package/src/runtime/schema.js +206 -0
- package/src/runtime/suggest.js +74 -0
- package/src/runtime/token-store.js +105 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket detail popup + create/edit form modal.
|
|
3
|
+
* Both use the native <dialog> element for proper focus-trap & backdrop.
|
|
4
|
+
*/
|
|
5
|
+
import { STATUSES, STATUS_MAP, PRIORITIES, PRIORITY_MAP } from './constants.js';
|
|
6
|
+
import { runtime } from './state.js';
|
|
7
|
+
import {
|
|
8
|
+
getTicket, createTicket, updateTicket, deleteTicket,
|
|
9
|
+
fetchTasksForTicket, createTask, deleteTask,
|
|
10
|
+
} from './api.js';
|
|
11
|
+
import { showToast } from './ui.js';
|
|
12
|
+
|
|
13
|
+
// ── Detail Modal ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export async function openTicketDetail(ticketId, initialMode = 'view') {
|
|
16
|
+
const dlg = document.getElementById('ticket-detail-modal');
|
|
17
|
+
if (!dlg) return;
|
|
18
|
+
|
|
19
|
+
dlg.innerHTML = '<div class="p-8 text-center text-slate-400">Loading…</div>';
|
|
20
|
+
dlg.showModal();
|
|
21
|
+
|
|
22
|
+
let ticket, tasks;
|
|
23
|
+
try {
|
|
24
|
+
[ticket, tasks] = await Promise.all([
|
|
25
|
+
getTicket(ticketId),
|
|
26
|
+
fetchTasksForTicket(ticketId),
|
|
27
|
+
]);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
dlg.innerHTML = `<div class="p-8 text-red-500">Error: ${_esc(err.message)}</div>`;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (initialMode === 'edit') {
|
|
34
|
+
_renderEditForm(dlg, ticket, tasks, true);
|
|
35
|
+
} else {
|
|
36
|
+
_renderDetailView(dlg, ticket, tasks);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _renderDetailView(dlg, ticket, tasks) {
|
|
41
|
+
const priority = PRIORITY_MAP[ticket.priority ?? 2];
|
|
42
|
+
const status = STATUS_MAP[ticket.status ?? 0];
|
|
43
|
+
|
|
44
|
+
dlg.innerHTML = `
|
|
45
|
+
<div class="flex flex-col max-h-[90vh] w-full">
|
|
46
|
+
|
|
47
|
+
<div class="flex items-start justify-between p-5 border-b gap-3 flex-shrink-0">
|
|
48
|
+
<div class="flex-1 min-w-0">
|
|
49
|
+
<div class="flex items-center gap-2 mb-1">
|
|
50
|
+
<span class="text-xs font-mono text-slate-400">${_esc(ticket.ticketnum ?? `#${ticket.ID}`)}</span>
|
|
51
|
+
<span class="text-xs px-2 py-0.5 rounded-full font-medium" style="background:${status.headerBg};color:${status.headerText}">${_esc(status.label)}</span>
|
|
52
|
+
<span class="text-xs font-semibold" style="color:${priority.color}">${priority.symbol} ${_esc(priority.label)}</span>
|
|
53
|
+
</div>
|
|
54
|
+
<h2 class="text-lg font-semibold text-slate-800 leading-snug">${_esc(ticket.name ?? '')}</h2>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="flex gap-2 flex-shrink-0">
|
|
57
|
+
<button id="detail-btn-edit" class="px-3 py-1.5 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors">Edit</button>
|
|
58
|
+
<button id="detail-btn-close" class="px-3 py-1.5 text-sm rounded-lg hover:bg-slate-100 text-slate-500">✕</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="overflow-y-auto flex-1 p-5 space-y-5">
|
|
63
|
+
|
|
64
|
+
<div class="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
|
65
|
+
${_detailRow('Due Date', ticket.duedate ? _formatDate(ticket.duedate) : '—')}
|
|
66
|
+
${_detailRow('Assigned', ticket.assigneduser ?? '—')}
|
|
67
|
+
${_detailRow('Account', ticket.account ?? '—')}
|
|
68
|
+
${_detailRow('Project', ticket.project ?? '—')}
|
|
69
|
+
${_detailRow('Created', ticket.creationdate ? _formatDate(ticket.creationdate) : '—')}
|
|
70
|
+
${_detailRow('Modified', ticket.lastmodified ? _formatDate(ticket.lastmodified) : '—')}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
${ticket.description ? `
|
|
74
|
+
<div>
|
|
75
|
+
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500 mb-2">Description</h3>
|
|
76
|
+
<p class="text-sm text-slate-700 whitespace-pre-wrap leading-relaxed">${_esc(ticket.description)}</p>
|
|
77
|
+
</div>
|
|
78
|
+
` : ''}
|
|
79
|
+
|
|
80
|
+
<div>
|
|
81
|
+
<div class="flex items-center justify-between mb-2">
|
|
82
|
+
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500">Tasks (${tasks.length})</h3>
|
|
83
|
+
<button id="detail-btn-add-task" class="text-xs font-medium text-blue-600 hover:underline">+ Add Task</button>
|
|
84
|
+
</div>
|
|
85
|
+
<div id="task-list">
|
|
86
|
+
${_taskTable(tasks, runtime.url)}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="flex items-center justify-between p-4 border-t flex-shrink-0">
|
|
93
|
+
<button id="detail-btn-delete" class="text-sm text-red-500 hover:text-red-700 hover:underline">Delete Ticket</button>
|
|
94
|
+
<button id="detail-btn-close2" class="px-4 py-1.5 text-sm rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-700">Close</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
dlg.querySelector('#detail-btn-close').onclick = () => dlg.close();
|
|
100
|
+
dlg.querySelector('#detail-btn-close2').onclick = () => dlg.close();
|
|
101
|
+
|
|
102
|
+
dlg.querySelector('#detail-btn-edit').onclick = () => {
|
|
103
|
+
const t = runtime.tickets.find(x => x.ID === ticket.ID) ?? ticket;
|
|
104
|
+
_renderEditForm(dlg, t, tasks, false);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
dlg.querySelector('#detail-btn-delete').onclick = async () => {
|
|
108
|
+
if (!confirm(`Delete "${ticket.name}"? This cannot be undone.`)) return;
|
|
109
|
+
try {
|
|
110
|
+
await deleteTicket(ticket.ID);
|
|
111
|
+
dlg.close();
|
|
112
|
+
document.dispatchEvent(new CustomEvent('app:reload'));
|
|
113
|
+
showToast('Ticket deleted.', 'success');
|
|
114
|
+
} catch (err) {
|
|
115
|
+
showToast(`Delete failed: ${err.message}`, 'error');
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Task interactions — use event delegation on the task list to avoid
|
|
120
|
+
// listener accumulation across re-renders of the detail view.
|
|
121
|
+
const taskList = dlg.querySelector('#task-list');
|
|
122
|
+
|
|
123
|
+
taskList.addEventListener('click', async e => {
|
|
124
|
+
const deleteBtn = e.target.closest('.task-delete');
|
|
125
|
+
|
|
126
|
+
if (deleteBtn) {
|
|
127
|
+
const taskId = Number(deleteBtn.dataset.taskId);
|
|
128
|
+
try {
|
|
129
|
+
await deleteTask(taskId);
|
|
130
|
+
const t = runtime.tickets.find(x => x.ID === ticket.ID) ?? ticket;
|
|
131
|
+
const refreshed = await fetchTasksForTicket(ticket.ID);
|
|
132
|
+
_renderDetailView(dlg, t, refreshed);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
showToast(`Could not delete task: ${err.message}`, 'error');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
dlg.querySelector('#detail-btn-add-task').onclick = () => {
|
|
140
|
+
_openAddTaskForm(dlg, ticket.ID);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
dlg.onclick = e => { if (e.target === dlg) dlg.close(); };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _openAddTaskForm(dlg, ticketId) {
|
|
147
|
+
const list = dlg.querySelector('#task-list');
|
|
148
|
+
const form = document.createElement('div');
|
|
149
|
+
form.className = 'mt-2 p-3 border border-slate-200 rounded-lg bg-slate-50 space-y-2';
|
|
150
|
+
form.innerHTML = `
|
|
151
|
+
<div class="flex gap-2">
|
|
152
|
+
<input id="new-task-name" type="text" placeholder="Task name…"
|
|
153
|
+
class="flex-1 border border-slate-300 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
154
|
+
<input id="new-task-due" type="date"
|
|
155
|
+
class="border border-slate-300 rounded-lg px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
156
|
+
</div>
|
|
157
|
+
<div class="flex gap-2 justify-end">
|
|
158
|
+
<button id="task-cancel" class="px-3 py-1.5 bg-white border border-slate-200 text-slate-600 rounded-lg text-sm hover:bg-slate-100">Cancel</button>
|
|
159
|
+
<button id="task-save" class="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700">Add Task</button>
|
|
160
|
+
</div>
|
|
161
|
+
`;
|
|
162
|
+
list.appendChild(form);
|
|
163
|
+
|
|
164
|
+
const nameInput = form.querySelector('#new-task-name');
|
|
165
|
+
nameInput.focus();
|
|
166
|
+
|
|
167
|
+
form.querySelector('#task-cancel').onclick = () => form.remove();
|
|
168
|
+
|
|
169
|
+
const save = async () => {
|
|
170
|
+
const name = nameInput.value.trim();
|
|
171
|
+
if (!name) { nameInput.focus(); return; }
|
|
172
|
+
|
|
173
|
+
const dueDateVal = form.querySelector('#new-task-due').value;
|
|
174
|
+
const data = {
|
|
175
|
+
name,
|
|
176
|
+
ticket: ticketId,
|
|
177
|
+
status: 0,
|
|
178
|
+
visibility: 0,
|
|
179
|
+
};
|
|
180
|
+
if (dueDateVal) data.duedate = Math.floor(new Date(dueDateVal).getTime() / 1000);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await createTask(data);
|
|
184
|
+
const t = runtime.tickets.find(x => x.ID === ticketId);
|
|
185
|
+
const refreshed = await fetchTasksForTicket(ticketId);
|
|
186
|
+
if (t) _renderDetailView(dlg, t, refreshed);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
showToast(`Could not create task: ${err.message}`, 'error');
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
let _saving = false;
|
|
193
|
+
const guardedSave = async () => {
|
|
194
|
+
if (_saving) return;
|
|
195
|
+
_saving = true;
|
|
196
|
+
await save();
|
|
197
|
+
_saving = false;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
form.querySelector('#task-save').onclick = guardedSave;
|
|
201
|
+
nameInput.addEventListener('keydown', async e => { if (e.key === 'Enter') await guardedSave(); });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Edit / Create Form Modal ───────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
export function openCreateTicket(defaultStatus = 0) {
|
|
207
|
+
const dlg = document.getElementById('ticket-form-modal');
|
|
208
|
+
if (!dlg) return;
|
|
209
|
+
_renderEditForm(dlg, { status: defaultStatus, priority: 2 }, [], false, true);
|
|
210
|
+
dlg.showModal();
|
|
211
|
+
dlg.onclick = e => { if (e.target === dlg) dlg.close(); };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _renderEditForm(dlg, ticket, tasks, isInDetailDlg, isCreate = false) {
|
|
215
|
+
const usedDlg = isInDetailDlg ? dlg : document.getElementById('ticket-form-modal');
|
|
216
|
+
|
|
217
|
+
usedDlg.innerHTML = `
|
|
218
|
+
<form id="ticket-form" class="flex flex-col max-h-[90vh] w-full" novalidate>
|
|
219
|
+
|
|
220
|
+
<div class="flex items-center justify-between p-5 border-b flex-shrink-0">
|
|
221
|
+
<h2 class="font-semibold text-slate-800">${isCreate ? 'New Ticket' : 'Edit Ticket'}</h2>
|
|
222
|
+
<button type="button" id="form-btn-close" class="p-1.5 rounded-lg hover:bg-slate-100 text-slate-500 text-lg leading-none">✕</button>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div class="overflow-y-auto flex-1 p-5 space-y-4">
|
|
226
|
+
|
|
227
|
+
<div>
|
|
228
|
+
<label class="block text-xs font-medium text-slate-600 mb-1">Name <span class="text-red-400">*</span></label>
|
|
229
|
+
<input id="f-name" type="text" value="${_esc(ticket.name ?? '')}"
|
|
230
|
+
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" required>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div class="grid grid-cols-2 gap-4">
|
|
234
|
+
<div>
|
|
235
|
+
<label class="block text-xs font-medium text-slate-600 mb-1">Status</label>
|
|
236
|
+
<select id="f-status" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
237
|
+
${STATUSES.map(s => `<option value="${s.value}" ${s.value === ticket.status ? 'selected' : ''}>${_esc(s.label)}</option>`).join('')}
|
|
238
|
+
</select>
|
|
239
|
+
</div>
|
|
240
|
+
<div>
|
|
241
|
+
<label class="block text-xs font-medium text-slate-600 mb-1">Priority</label>
|
|
242
|
+
<select id="f-priority" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
243
|
+
${PRIORITIES.map(p => `<option value="${p.value}" ${p.value === ticket.priority ? 'selected' : ''}>${p.symbol} ${_esc(p.label)}</option>`).join('')}
|
|
244
|
+
</select>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div>
|
|
249
|
+
<label class="block text-xs font-medium text-slate-600 mb-1">Due Date</label>
|
|
250
|
+
<input id="f-duedate" type="date" value="${ticket.duedate ? _toDateInput(ticket.duedate) : ''}"
|
|
251
|
+
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div>
|
|
255
|
+
<label class="block text-xs font-medium text-slate-600 mb-1">Description</label>
|
|
256
|
+
<textarea id="f-description" rows="4"
|
|
257
|
+
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y">${_esc(ticket.description ?? '')}</textarea>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div>
|
|
261
|
+
<div class="flex items-center justify-between mb-2">
|
|
262
|
+
<label class="text-xs font-medium text-slate-600">Extended Data</label>
|
|
263
|
+
<button type="button" id="extdata-add-row" class="text-xs text-blue-600 hover:underline">+ Add field</button>
|
|
264
|
+
</div>
|
|
265
|
+
<div id="extdata-rows" class="space-y-2">
|
|
266
|
+
${_buildExtdataRows(ticket.extdata)}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div class="flex items-center justify-between p-4 border-t flex-shrink-0">
|
|
273
|
+
${isCreate ? '<span></span>' : `<button type="button" id="form-btn-delete" class="text-sm text-red-500 hover:underline">Delete</button>`}
|
|
274
|
+
<div class="flex gap-3">
|
|
275
|
+
<button type="button" id="form-btn-cancel" class="px-4 py-1.5 text-sm rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-700">Cancel</button>
|
|
276
|
+
<button type="submit" id="form-btn-save" class="px-4 py-1.5 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors">
|
|
277
|
+
${isCreate ? 'Create Ticket' : 'Save Changes'}
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
</form>
|
|
283
|
+
`;
|
|
284
|
+
|
|
285
|
+
const form = usedDlg.querySelector('#ticket-form');
|
|
286
|
+
|
|
287
|
+
usedDlg.querySelector('#form-btn-close').onclick = () => {
|
|
288
|
+
if (isInDetailDlg) _renderDetailView(usedDlg, ticket, tasks);
|
|
289
|
+
else usedDlg.close();
|
|
290
|
+
};
|
|
291
|
+
usedDlg.querySelector('#form-btn-cancel').onclick = () => {
|
|
292
|
+
if (isInDetailDlg) _renderDetailView(usedDlg, ticket, tasks);
|
|
293
|
+
else usedDlg.close();
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
if (!isCreate) {
|
|
297
|
+
usedDlg.querySelector('#form-btn-delete')?.addEventListener('click', async () => {
|
|
298
|
+
if (!confirm(`Delete "${ticket.name}"? This cannot be undone.`)) return;
|
|
299
|
+
try {
|
|
300
|
+
await deleteTicket(ticket.ID);
|
|
301
|
+
usedDlg.close();
|
|
302
|
+
document.dispatchEvent(new CustomEvent('app:reload'));
|
|
303
|
+
showToast('Ticket deleted.', 'success');
|
|
304
|
+
} catch (err) {
|
|
305
|
+
showToast(`Delete failed: ${err.message}`, 'error');
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
usedDlg.querySelector('#extdata-add-row').onclick = () => {
|
|
311
|
+
const rows = usedDlg.querySelector('#extdata-rows');
|
|
312
|
+
rows.insertAdjacentHTML('beforeend', _extdataEmptyRow());
|
|
313
|
+
rows.lastElementChild.querySelector('input').focus();
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
form.addEventListener('submit', async e => {
|
|
317
|
+
e.preventDefault();
|
|
318
|
+
const name = usedDlg.querySelector('#f-name').value.trim();
|
|
319
|
+
if (!name) { usedDlg.querySelector('#f-name').focus(); return; }
|
|
320
|
+
|
|
321
|
+
const dueDateVal = usedDlg.querySelector('#f-duedate').value;
|
|
322
|
+
const extdata = _readExtdata(usedDlg);
|
|
323
|
+
|
|
324
|
+
const data = {
|
|
325
|
+
name,
|
|
326
|
+
status: Number(usedDlg.querySelector('#f-status').value),
|
|
327
|
+
priority: Number(usedDlg.querySelector('#f-priority').value),
|
|
328
|
+
description: usedDlg.querySelector('#f-description').value,
|
|
329
|
+
duedate: dueDateVal ? Math.floor(new Date(dueDateVal).getTime() / 1000) : null,
|
|
330
|
+
extdata,
|
|
331
|
+
visibility: 0,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const saveBtn = usedDlg.querySelector('#form-btn-save');
|
|
335
|
+
saveBtn.disabled = true;
|
|
336
|
+
saveBtn.textContent = 'Saving…';
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
if (isCreate) {
|
|
340
|
+
await createTicket(data);
|
|
341
|
+
showToast('Ticket created!', 'success');
|
|
342
|
+
} else {
|
|
343
|
+
await updateTicket(ticket.ID, data);
|
|
344
|
+
showToast('Ticket updated.', 'success');
|
|
345
|
+
}
|
|
346
|
+
usedDlg.close();
|
|
347
|
+
document.dispatchEvent(new CustomEvent('app:reload'));
|
|
348
|
+
} catch (err) {
|
|
349
|
+
showToast(`Save failed: ${err.message}`, 'error');
|
|
350
|
+
saveBtn.disabled = false;
|
|
351
|
+
saveBtn.textContent = isCreate ? 'Create Ticket' : 'Save Changes';
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
function _detailRow(label, value) {
|
|
359
|
+
return `
|
|
360
|
+
<div>
|
|
361
|
+
<dt class="text-xs text-slate-400 mb-0.5">${_esc(label)}</dt>
|
|
362
|
+
<dd class="text-sm font-medium text-slate-700">${_esc(String(value))}</dd>
|
|
363
|
+
</div>
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Render tasks as a table.
|
|
369
|
+
* Link format: <baseUrl>?umi=tickets&page=details_ticket&id=<TASK_ID>&tab=0
|
|
370
|
+
*/
|
|
371
|
+
function _taskTable(tasks, baseUrl) {
|
|
372
|
+
if (tasks.length === 0) {
|
|
373
|
+
return '<p class="text-xs text-slate-400 italic">No tasks yet.</p>';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const base = (baseUrl ?? '').replace(/\/+$/, '');
|
|
377
|
+
|
|
378
|
+
const rows = tasks.map(task => {
|
|
379
|
+
const taskUrl = `${base}?umi=tickets&page=details_ticket&id=${task.ID}&tab=0`;
|
|
380
|
+
const taskNum = _esc(task.tasknum ?? `#${task.ID}`);
|
|
381
|
+
const nameLink = `<a href="${taskUrl}" target="_blank" rel="noopener"
|
|
382
|
+
class="text-blue-600 hover:underline font-medium">${_esc(task.name ?? '(untitled)')}</a>`;
|
|
383
|
+
const due = task.duedate ? _formatDate(task.duedate) : '—';
|
|
384
|
+
const assigned = _esc(task.assigneduser ?? '—');
|
|
385
|
+
|
|
386
|
+
return `
|
|
387
|
+
<tr class="border-t border-slate-100 group/row">
|
|
388
|
+
<td class="py-1.5 pr-3 text-xs font-mono text-slate-400 whitespace-nowrap">${taskNum}</td>
|
|
389
|
+
<td class="py-1.5 pr-3 text-sm">${nameLink}</td>
|
|
390
|
+
<td class="py-1.5 pr-3 text-xs text-slate-500 whitespace-nowrap">${_esc(due)}</td>
|
|
391
|
+
<td class="py-1.5 pr-3 text-xs text-slate-500 whitespace-nowrap">${assigned}</td>
|
|
392
|
+
<td class="py-1.5 text-right">
|
|
393
|
+
<button class="task-delete opacity-0 group-hover/row:opacity-100 transition-opacity text-slate-400 hover:text-red-500 text-xs px-1"
|
|
394
|
+
data-task-id="${task.ID}" title="Delete task">✕</button>
|
|
395
|
+
</td>
|
|
396
|
+
</tr>
|
|
397
|
+
`;
|
|
398
|
+
}).join('');
|
|
399
|
+
|
|
400
|
+
return `
|
|
401
|
+
<table class="w-full text-left">
|
|
402
|
+
<thead>
|
|
403
|
+
<tr class="text-xs font-semibold text-slate-400 uppercase tracking-wide">
|
|
404
|
+
<th class="pb-1.5 pr-3 font-medium">Task #</th>
|
|
405
|
+
<th class="pb-1.5 pr-3 font-medium">Name</th>
|
|
406
|
+
<th class="pb-1.5 pr-3 font-medium">Due Date</th>
|
|
407
|
+
<th class="pb-1.5 pr-3 font-medium">Assigned</th>
|
|
408
|
+
<th></th>
|
|
409
|
+
</tr>
|
|
410
|
+
</thead>
|
|
411
|
+
<tbody>${rows}</tbody>
|
|
412
|
+
</table>
|
|
413
|
+
`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function _buildExtdataRows(extdata) {
|
|
417
|
+
if (!extdata || typeof extdata !== 'object') return '';
|
|
418
|
+
return Object.entries(extdata)
|
|
419
|
+
.map(([k, v]) => `
|
|
420
|
+
<div class="flex gap-2 extdata-row">
|
|
421
|
+
<input type="text" placeholder="key" value="${_esc(k)}"
|
|
422
|
+
class="extdata-key w-1/3 border border-slate-300 rounded-lg px-2 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
423
|
+
<input type="text" placeholder="value" value="${_esc(String(v ?? ''))}"
|
|
424
|
+
class="extdata-val flex-1 border border-slate-300 rounded-lg px-2 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
425
|
+
<button type="button" class="extdata-rm px-2 rounded-lg bg-slate-100 hover:bg-red-100 text-slate-400 hover:text-red-500 text-xs">✕</button>
|
|
426
|
+
</div>
|
|
427
|
+
`).join('');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function _extdataEmptyRow() {
|
|
431
|
+
return `
|
|
432
|
+
<div class="flex gap-2 extdata-row">
|
|
433
|
+
<input type="text" placeholder="key"
|
|
434
|
+
class="extdata-key w-1/3 border border-slate-300 rounded-lg px-2 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
435
|
+
<input type="text" placeholder="value"
|
|
436
|
+
class="extdata-val flex-1 border border-slate-300 rounded-lg px-2 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
437
|
+
<button type="button" class="extdata-rm px-2 rounded-lg bg-slate-100 hover:bg-red-100 text-slate-400 hover:text-red-500 text-xs">✕</button>
|
|
438
|
+
</div>
|
|
439
|
+
`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function _readExtdata(container) {
|
|
443
|
+
const result = {};
|
|
444
|
+
container.querySelectorAll('.extdata-row').forEach(row => {
|
|
445
|
+
const key = row.querySelector('.extdata-key')?.value.trim();
|
|
446
|
+
const val = row.querySelector('.extdata-val')?.value ?? '';
|
|
447
|
+
if (key) result[key] = val;
|
|
448
|
+
});
|
|
449
|
+
return Object.keys(result).length ? result : undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Delegate removal of extdata rows using event delegation on the modal
|
|
453
|
+
document.addEventListener('click', e => {
|
|
454
|
+
if (e.target.classList.contains('extdata-rm')) {
|
|
455
|
+
e.target.closest('.extdata-row')?.remove();
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
function _formatDate(unix) {
|
|
460
|
+
return new Date(unix * 1000).toLocaleDateString(undefined, {
|
|
461
|
+
month: 'short', day: 'numeric', year: 'numeric',
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function _toDateInput(unix) {
|
|
466
|
+
const d = new Date(unix * 1000);
|
|
467
|
+
return d.toISOString().split('T')[0];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function _esc(str) {
|
|
471
|
+
return String(str)
|
|
472
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
473
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
474
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings drawer — column configuration.
|
|
3
|
+
* Reads/writes runtime.settings and persists via saveSettings().
|
|
4
|
+
*/
|
|
5
|
+
import { STATUSES } from './constants.js';
|
|
6
|
+
import { runtime, saveSettings } from './state.js';
|
|
7
|
+
|
|
8
|
+
let _onChangeCb = null;
|
|
9
|
+
|
|
10
|
+
/** Register a callback invoked whenever settings change (to re-render the board). */
|
|
11
|
+
export function onSettingsChange(cb) {
|
|
12
|
+
_onChangeCb = cb;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Open the settings panel. */
|
|
16
|
+
export function openSettings() {
|
|
17
|
+
_renderColumnConfig();
|
|
18
|
+
document.getElementById('settings-overlay')?.classList.remove('hidden');
|
|
19
|
+
document.getElementById('settings-panel')?.classList.remove('hidden');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Close the settings panel. */
|
|
23
|
+
export function closeSettings() {
|
|
24
|
+
document.getElementById('settings-overlay')?.classList.add('hidden');
|
|
25
|
+
document.getElementById('settings-panel')?.classList.add('hidden');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Rendering ──────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function _renderColumnConfig() {
|
|
31
|
+
const container = document.getElementById('columns-config');
|
|
32
|
+
if (!container) return;
|
|
33
|
+
|
|
34
|
+
const selected = new Set(runtime.settings.columns);
|
|
35
|
+
|
|
36
|
+
container.innerHTML = STATUSES.map(s => `
|
|
37
|
+
<label class="flex items-center gap-3 p-2 rounded-lg hover:bg-slate-50 cursor-pointer group">
|
|
38
|
+
<input type="checkbox" class="col-checkbox w-4 h-4 rounded accent-blue-600"
|
|
39
|
+
value="${s.value}" ${selected.has(s.value) ? 'checked' : ''}>
|
|
40
|
+
<span class="flex items-center gap-2 flex-1">
|
|
41
|
+
<span class="w-2 h-2 rounded-full flex-shrink-0" style="background:${s.cardBorder}"></span>
|
|
42
|
+
<span class="text-sm text-slate-700">${_esc(s.label)}</span>
|
|
43
|
+
</span>
|
|
44
|
+
</label>
|
|
45
|
+
`).join('');
|
|
46
|
+
|
|
47
|
+
// Select all / clear buttons
|
|
48
|
+
container.insertAdjacentHTML('afterbegin', `
|
|
49
|
+
<div class="flex gap-2 mb-2 pb-2 border-b border-slate-100">
|
|
50
|
+
<button id="col-select-all" class="text-xs text-blue-600 hover:underline">Select all</button>
|
|
51
|
+
<span class="text-slate-300">|</span>
|
|
52
|
+
<button id="col-clear-all" class="text-xs text-slate-400 hover:underline">Clear</button>
|
|
53
|
+
</div>
|
|
54
|
+
`);
|
|
55
|
+
|
|
56
|
+
container.querySelector('#col-select-all').onclick = () => {
|
|
57
|
+
container.querySelectorAll('.col-checkbox').forEach(cb => cb.checked = true);
|
|
58
|
+
_saveColumns(container);
|
|
59
|
+
};
|
|
60
|
+
container.querySelector('#col-clear-all').onclick = () => {
|
|
61
|
+
container.querySelectorAll('.col-checkbox').forEach(cb => cb.checked = false);
|
|
62
|
+
_saveColumns(container);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
container.querySelectorAll('.col-checkbox').forEach(cb => {
|
|
66
|
+
cb.addEventListener('change', () => _saveColumns(container));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _saveColumns(container) {
|
|
71
|
+
runtime.settings.columns = Array.from(
|
|
72
|
+
container.querySelectorAll('.col-checkbox:checked')
|
|
73
|
+
).map(cb => Number(cb.value));
|
|
74
|
+
saveSettings(runtime.settings);
|
|
75
|
+
_onChangeCb?.();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _esc(str) {
|
|
79
|
+
return String(str)
|
|
80
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
81
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
82
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application state — persistence via localStorage, in-memory for runtime data.
|
|
3
|
+
* Config resolution merges <body> data attributes with localStorage overrides.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const KEYS = {
|
|
7
|
+
URL: 'zeyos_kanban_url',
|
|
8
|
+
TOKENS: 'zeyos_kanban_tokens',
|
|
9
|
+
SETTINGS: 'zeyos_kanban_settings',
|
|
10
|
+
CONTEXT: 'zeyos_kanban_context',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_SETTINGS = {
|
|
14
|
+
columns: [], // status values (integers) the user wants as columns
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_CONTEXT = {
|
|
18
|
+
type: 'all', // 'all' | 'account' | 'project'
|
|
19
|
+
id: null,
|
|
20
|
+
name: 'All Tickets',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function readJson(key, fallback) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = localStorage.getItem(key);
|
|
28
|
+
return raw != null ? JSON.parse(raw) : fallback;
|
|
29
|
+
} catch {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeJson(key, value) {
|
|
35
|
+
try {
|
|
36
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
37
|
+
} catch {
|
|
38
|
+
// storage full or unavailable — silently ignore
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── URL ───────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export function loadUrl() { return localStorage.getItem(KEYS.URL) ?? null; }
|
|
45
|
+
export function saveUrl(url) { localStorage.setItem(KEYS.URL, url); }
|
|
46
|
+
export function clearUrl() { localStorage.removeItem(KEYS.URL); }
|
|
47
|
+
|
|
48
|
+
// ── tokens ─────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export function loadTokens() {
|
|
51
|
+
return readJson(KEYS.TOKENS, null);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function saveTokens(tokens) {
|
|
55
|
+
writeJson(KEYS.TOKENS, tokens);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function clearTokens() {
|
|
59
|
+
localStorage.removeItem(KEYS.TOKENS);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── settings ───────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export function loadSettings() {
|
|
65
|
+
const saved = readJson(KEYS.SETTINGS, {});
|
|
66
|
+
return { ...DEFAULT_SETTINGS, ...saved };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function saveSettings(settings) {
|
|
70
|
+
writeJson(KEYS.SETTINGS, settings);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── context (account / project filter) ─────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export function loadContext() {
|
|
76
|
+
return readJson(KEYS.CONTEXT, DEFAULT_CONTEXT);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function saveContext(context) {
|
|
80
|
+
writeJson(KEYS.CONTEXT, context);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── config resolution ──────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve effective config by merging <body> data attributes with localStorage.
|
|
87
|
+
* localStorage values (set via ZeyOS console API) override body attributes.
|
|
88
|
+
* Returns { url, accessToken, refreshToken }.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveConfig() {
|
|
91
|
+
const body = document.body;
|
|
92
|
+
|
|
93
|
+
// Body attributes (defaults)
|
|
94
|
+
const bodyUrl = body?.dataset.zeyosUrl?.trim() || null;
|
|
95
|
+
const bodyAccess = body?.dataset.zeyosAccesstoken?.trim() || null;
|
|
96
|
+
const bodyRefresh = body?.dataset.zeyosRefreshtoken?.trim() || null;
|
|
97
|
+
|
|
98
|
+
// localStorage overrides
|
|
99
|
+
const storedUrl = loadUrl();
|
|
100
|
+
const storedTokens = loadTokens();
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
url: storedUrl || bodyUrl,
|
|
104
|
+
accessToken: storedTokens?.accessToken || bodyAccess,
|
|
105
|
+
refreshToken: storedTokens?.refreshToken || bodyRefresh,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── in-memory runtime state (not persisted) ─────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export const runtime = {
|
|
112
|
+
url: null, // resolved ZeyOS instance URL
|
|
113
|
+
authMode: null, // 'token' | 'session' | null
|
|
114
|
+
tickets: [],
|
|
115
|
+
projects: [],
|
|
116
|
+
settings: loadSettings(),
|
|
117
|
+
context: loadContext(),
|
|
118
|
+
};
|