@gaia-ai/addon-gaia-ui 0.17.2 → 0.19.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/dist/src/Entity/ticket/Component/edit-form/index.js +9 -2
- package/dist/src/Entity/ticket/Component/sections/index.js +5 -3
- package/dist/src/Entity/ticket/Component/ticket-content/index.d.ts +17 -0
- package/dist/src/Entity/ticket/Component/ticket-content/index.js +60 -2
- package/dist/src/Entity/ticket/Component/ticket-content/model.d.ts +1 -1
- package/dist/src/Entity/ticket/Component/ticket-content/model.js +33 -5
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +6 -2
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +59 -4
- package/dist/src/Entity/ticket/Data/addons.d.ts +53 -0
- package/dist/src/Entity/ticket/Data/addons.js +35 -0
- package/dist/src/Entity/ticket/Data/detail.js +2 -0
- package/dist/src/Entity/ticket/Data/store.js +2 -0
- package/dist/src/Entity/ticket/Data/tickets.d.ts +13 -9
- package/dist/src/Entity/ticket/Data/tickets.js +12 -4
- package/dist/src/Entity/ticket/Data/write.d.ts +7 -0
- package/dist/src/Entity/ticket/Data/write.js +17 -0
- package/dist/src/Entity/ticket/Form/addon-action.d.ts +37 -0
- package/dist/src/Entity/ticket/Form/addon-action.js +80 -0
- package/dist/src/Entity/ticket/Form/create-data.d.ts +2 -0
- package/dist/src/Entity/ticket/Form/create-data.js +9 -1
- package/dist/src/Entity/ticket/Form/create.d.ts +5 -1
- package/dist/src/Entity/ticket/Form/create.js +3 -0
- package/dist/src/Entity/ticket/Form/edit-data.d.ts +2 -0
- package/dist/src/Entity/ticket/Form/edit-data.js +14 -1
- package/dist/src/Entity/ticket/Form/edit.d.ts +14 -1
- package/dist/src/Entity/ticket/Form/edit.js +31 -0
- package/dist/src/Entity/ticket/Form/reviewer-options.d.ts +10 -0
- package/dist/src/Entity/ticket/Form/reviewer-options.js +48 -0
- package/dist/src/Entity/ticket/Screen/conductor-tickets.d.ts +2 -2
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +4 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +30 -1
- package/dist/src/Entity/ticket/Screen/ticket-addons.d.ts +94 -0
- package/dist/src/Entity/ticket/Screen/ticket-addons.js +492 -0
- package/dist/src/Entity/ticket/Screen/ticket-content-screen.d.ts +21 -0
- package/dist/src/Entity/ticket/Screen/ticket-content-screen.js +122 -8
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +4 -2
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +2 -2
- package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +2 -2
- package/dist/src/Routing/routes.js +3 -0
- package/dist/src/Widget/multiselect.js +7 -3
- package/dist/src/types.d.ts +5 -0
- package/package.json +3 -2
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// The ticket's addon messages (GAIA-661): one entry per addon of the
|
|
2
|
+
// ticket's conductor in an addon bar (←/→), the selected addon's permitted
|
|
3
|
+
// messages as one list, Enter to send through the conductor inbox, and a
|
|
4
|
+
// modal form for a message with input. Definitions and states come from the
|
|
5
|
+
// server, so this tab needs no addon installed locally. Contract 1
|
|
6
|
+
// (views/actions/rows) is retired; only contract-2 messages render here.
|
|
7
|
+
import { actionItem, } from '../../../Component/footer-actions/index.js';
|
|
8
|
+
import { Markdown, markdownLines } from '../../../Component/markdown/index.js';
|
|
9
|
+
import { flattenSections } from '../../../Component/section-list/index.js';
|
|
10
|
+
import { copyThrough } from '../../../lib/clipboard.js';
|
|
11
|
+
import { ListCursor } from '../../../lib/cursor.js';
|
|
12
|
+
import { columnNode, messageTree } from '../../../lib/vnode.js';
|
|
13
|
+
import { AsyncModalScreen } from '../../../Screen/async-modal-screen.js';
|
|
14
|
+
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
15
|
+
import { runRowAction } from '../../../Screen/row-action.js';
|
|
16
|
+
import { FIELDS_REGION, renderDeclaredFieldsTree, } from '../../../Widget/render.js';
|
|
17
|
+
import { listRowIds, listTableRows, listTableTree, } from '../../collection.js';
|
|
18
|
+
import { ticketEditAction, ticketHeaderTree, ticketTitleOf, } from '../Component/ticket-detail/index.js';
|
|
19
|
+
import { addonEntries, isOpen, readTicketAddons, sendAddonAction, } from '../Data/addons.js';
|
|
20
|
+
import { ticketInputContext } from '../Data/input.js';
|
|
21
|
+
import { readTicket } from '../Data/store.js';
|
|
22
|
+
import { addonForm, formFields, payloadFrom, } from '../Form/addon-action.js';
|
|
23
|
+
import { keyToIntent } from '../Form/keys.js';
|
|
24
|
+
/** Re-read cadence while a message of the shown addon is pending/running. */
|
|
25
|
+
const OPEN_POLL_MS = 3_000;
|
|
26
|
+
function ticketUuidOf(context, ticket) {
|
|
27
|
+
return ticket?.data?.id ?? context.params?.ticketId ?? '';
|
|
28
|
+
}
|
|
29
|
+
/** The table of one addon: label and an Enter-to-run hint per message. */
|
|
30
|
+
function tableFor() {
|
|
31
|
+
const columns = [
|
|
32
|
+
{
|
|
33
|
+
fields: [],
|
|
34
|
+
key: 'c0',
|
|
35
|
+
label: 'LABEL',
|
|
36
|
+
width: 28,
|
|
37
|
+
get: (l) => l.cells[0] ?? '',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
fields: [],
|
|
41
|
+
key: 'actions',
|
|
42
|
+
label: 'ACTIONS',
|
|
43
|
+
width: 0,
|
|
44
|
+
get: (l) => l.cells[1] ?? '',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
return {
|
|
48
|
+
noun: 'addon',
|
|
49
|
+
columns,
|
|
50
|
+
emptyMessage: 'No data yet.',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function linesFor(entries) {
|
|
54
|
+
return entries.map((entry) => ({
|
|
55
|
+
id: entry.id,
|
|
56
|
+
cells: [
|
|
57
|
+
`▶ ${entry.message.label}`,
|
|
58
|
+
entry.message.input ? 'Enter: form' : 'Enter: send',
|
|
59
|
+
],
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
/** The addon bar: every addon, the selected one in brackets. */
|
|
63
|
+
function addonBar(model, selected) {
|
|
64
|
+
return model.addons
|
|
65
|
+
.map((a, i) => (i === selected ? `[${a.label}]` : a.label))
|
|
66
|
+
.join(' ');
|
|
67
|
+
}
|
|
68
|
+
/** The newest message of the addon, as one status line. */
|
|
69
|
+
function latestLine(addon) {
|
|
70
|
+
const latest = addon.latest;
|
|
71
|
+
if (!latest)
|
|
72
|
+
return undefined;
|
|
73
|
+
return `${latest.label}: ${latest.status}`;
|
|
74
|
+
}
|
|
75
|
+
/** The latest answer's Markdown body. */
|
|
76
|
+
function latestMarkdown(addon) {
|
|
77
|
+
return addon.latest?.result?.markdown;
|
|
78
|
+
}
|
|
79
|
+
/** The first safe http(s) link a Markdown answer carries, if any (`l` key). */
|
|
80
|
+
function firstHttpLink(markdown) {
|
|
81
|
+
// Prefer the URL inside markdown link syntax `[text](url)` first: a bare
|
|
82
|
+
// `\S+` scan greedily swallows the closing `)` and any wrapping brackets
|
|
83
|
+
// of a real `[443](https://host:443)` answer (GAIA-661).
|
|
84
|
+
return (markdown.match(/\]\((https?:[^)\s]+)\)/)?.[1] ??
|
|
85
|
+
markdown.match(/\bhttps?:\/\/\S+/)?.[0]?.replace(/[)\]."'`]+$/, ''));
|
|
86
|
+
}
|
|
87
|
+
export class TicketAddonsScreen extends AsyncScreen {
|
|
88
|
+
// No `tab` here (GAIA-661): this screen is the full send/poll surface, kept
|
|
89
|
+
// navigable from the Overview sidebar's inline card, never a tab a person
|
|
90
|
+
// sees — the tab bar never shows the word "addon".
|
|
91
|
+
static route = {
|
|
92
|
+
id: 'ticket.addons',
|
|
93
|
+
path: '/tickets/:ticketId/addons',
|
|
94
|
+
parent: 'project.tickets',
|
|
95
|
+
fallback: { routeId: 'my.work' },
|
|
96
|
+
context: ticketInputContext,
|
|
97
|
+
};
|
|
98
|
+
id = TicketAddonsScreen.route.id;
|
|
99
|
+
frameId = 'ticket';
|
|
100
|
+
cursor = new ListCursor();
|
|
101
|
+
selected = 0;
|
|
102
|
+
sending = new Set();
|
|
103
|
+
timer;
|
|
104
|
+
async load(context) {
|
|
105
|
+
const ticket = readTicket(context, this.readyValue?.ticket);
|
|
106
|
+
const client = context.services?.client;
|
|
107
|
+
// No control plane (a bare pipe): nothing to show rather than an error.
|
|
108
|
+
if (!client)
|
|
109
|
+
return { ticket, model: { conductor: null, addons: [] } };
|
|
110
|
+
return {
|
|
111
|
+
ticket,
|
|
112
|
+
model: await readTicketAddons(client, ticketUuidOf(context, ticket)),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
didLoad(data, context) {
|
|
116
|
+
this.selected = Math.min(this.selected, Math.max(0, data.model.addons.length - 1));
|
|
117
|
+
this.clampCursor();
|
|
118
|
+
this.schedulePoll(context);
|
|
119
|
+
}
|
|
120
|
+
reload(context) {
|
|
121
|
+
void this.retry(context);
|
|
122
|
+
}
|
|
123
|
+
leave() {
|
|
124
|
+
this.stopPoll();
|
|
125
|
+
super.leave();
|
|
126
|
+
}
|
|
127
|
+
reset() {
|
|
128
|
+
this.stopPoll();
|
|
129
|
+
super.reset();
|
|
130
|
+
this.cursor.reset();
|
|
131
|
+
this.selected = 0;
|
|
132
|
+
this.sending = new Set();
|
|
133
|
+
this.toast = '';
|
|
134
|
+
}
|
|
135
|
+
/** Re-read every 3 s while the shown addon has an open message. */
|
|
136
|
+
schedulePoll(context) {
|
|
137
|
+
const addon = this.addon();
|
|
138
|
+
if (!addon || !isOpen(addon)) {
|
|
139
|
+
this.stopPoll();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (this.timer)
|
|
143
|
+
return;
|
|
144
|
+
this.timer = setInterval(() => {
|
|
145
|
+
if (context.isActive())
|
|
146
|
+
this.reload(context);
|
|
147
|
+
}, OPEN_POLL_MS);
|
|
148
|
+
this.timer.unref?.();
|
|
149
|
+
}
|
|
150
|
+
stopPoll() {
|
|
151
|
+
if (this.timer)
|
|
152
|
+
clearInterval(this.timer);
|
|
153
|
+
this.timer = undefined;
|
|
154
|
+
}
|
|
155
|
+
addon() {
|
|
156
|
+
return this.readyValue?.model.addons[this.selected];
|
|
157
|
+
}
|
|
158
|
+
entries() {
|
|
159
|
+
const addon = this.addon();
|
|
160
|
+
return addon ? addonEntries(addon) : [];
|
|
161
|
+
}
|
|
162
|
+
clampCursor() {
|
|
163
|
+
this.cursor.index = Math.max(0, Math.min(this.cursor.index, this.entries().length - 1));
|
|
164
|
+
}
|
|
165
|
+
renderLoading() {
|
|
166
|
+
return ['Loading addons…'];
|
|
167
|
+
}
|
|
168
|
+
lines() {
|
|
169
|
+
const addon = this.addon();
|
|
170
|
+
if (!addon)
|
|
171
|
+
return undefined;
|
|
172
|
+
const entries = this.entries();
|
|
173
|
+
const table = tableFor();
|
|
174
|
+
const lines = linesFor(entries).map((line) => this.sending.has(line.id)
|
|
175
|
+
? { ...line, cells: [...line.cells.slice(0, -1), 'sending …'] }
|
|
176
|
+
: line);
|
|
177
|
+
return { table, lines };
|
|
178
|
+
}
|
|
179
|
+
renderReady(data, context) {
|
|
180
|
+
const view = this.lines();
|
|
181
|
+
if (!view)
|
|
182
|
+
return messageTree('addons', ['No addons for this ticket.']);
|
|
183
|
+
const addon = this.addon();
|
|
184
|
+
const markdown = addon ? latestMarkdown(addon) : undefined;
|
|
185
|
+
const children = [
|
|
186
|
+
messageTree('addons-bar', [addonBar(data.model, this.selected)]),
|
|
187
|
+
listTableTree(view.table, view.lines, undefined, {
|
|
188
|
+
selectedIndex: this.cursor.index,
|
|
189
|
+
}),
|
|
190
|
+
];
|
|
191
|
+
if (markdown) {
|
|
192
|
+
children.push(Markdown({
|
|
193
|
+
id: 'addons-answer',
|
|
194
|
+
width: Math.max(16, (context.terminalColumns ?? 80) - 12),
|
|
195
|
+
source: markdown,
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
return columnNode('addons', ...children);
|
|
199
|
+
}
|
|
200
|
+
renderNonInteractive(context) {
|
|
201
|
+
const notice = this.noticeRows(context);
|
|
202
|
+
if (notice)
|
|
203
|
+
return notice;
|
|
204
|
+
const data = this.readyValue;
|
|
205
|
+
const view = this.lines();
|
|
206
|
+
// A ticket report prints every tab; one without addons adds nothing.
|
|
207
|
+
if (!data || !view)
|
|
208
|
+
return [];
|
|
209
|
+
const addon = this.addon();
|
|
210
|
+
const markdown = addon ? latestMarkdown(addon) : undefined;
|
|
211
|
+
return flattenSections([
|
|
212
|
+
{
|
|
213
|
+
heading: addonBar(data.model, this.selected),
|
|
214
|
+
lines: [
|
|
215
|
+
...listTableRows(view.table, view.lines, undefined),
|
|
216
|
+
...(markdown ? markdownLines(markdown).map((l) => l.text) : []),
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
]).lines;
|
|
220
|
+
}
|
|
221
|
+
chrome(context) {
|
|
222
|
+
const ticket = this.readyValue?.ticket;
|
|
223
|
+
const { identifier, title } = ticketTitleOf(ticket ?? {});
|
|
224
|
+
const view = this.lines();
|
|
225
|
+
const count = view?.lines.length ?? 0;
|
|
226
|
+
return {
|
|
227
|
+
title: ticket?.data
|
|
228
|
+
? `${identifier} · ${title}`
|
|
229
|
+
: (context.params?.ticketId ?? 'ticket'),
|
|
230
|
+
...(ticket?.data
|
|
231
|
+
? { header: ticketHeaderTree(ticket, identifier, title, []) }
|
|
232
|
+
: {}),
|
|
233
|
+
selectable: count,
|
|
234
|
+
selectedIndex: this.cursor.index,
|
|
235
|
+
cursorMode: count > 1 ? 'select' : 'scroll',
|
|
236
|
+
...(view ? { rowIds: listRowIds(view.table, view.lines) } : {}),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
actions() {
|
|
240
|
+
const many = (this.readyValue?.model.addons.length ?? 0) > 1;
|
|
241
|
+
return [
|
|
242
|
+
actionItem('detail-tab', 'Tab', 'section'),
|
|
243
|
+
actionItem('detail-edit', 'e', 'edit'),
|
|
244
|
+
...(many ? [actionItem('addons-switch', '←→', 'addon')] : []),
|
|
245
|
+
actionItem('detail-arrows', '↑↓', 'select'),
|
|
246
|
+
actionItem('addons-enter', 'Enter', 'run'),
|
|
247
|
+
actionItem('addons-link', 'l', 'copy link'),
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
notice() {
|
|
251
|
+
const addon = this.addon();
|
|
252
|
+
const offline = this.readyValue?.model.conductor?.status === 'offline';
|
|
253
|
+
return ([
|
|
254
|
+
offline
|
|
255
|
+
? 'Conductor offline — messages are queued until it returns'
|
|
256
|
+
: '',
|
|
257
|
+
addon && addon.contract !== 2
|
|
258
|
+
? `Addon contract ${addon.contract} is not supported`
|
|
259
|
+
: '',
|
|
260
|
+
addon
|
|
261
|
+
? (latestLine(addon) ?? 'You have not sent a message to this addon.')
|
|
262
|
+
: '',
|
|
263
|
+
]
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
.join(' · ') || undefined);
|
|
266
|
+
}
|
|
267
|
+
select(index, context) {
|
|
268
|
+
const next = Math.max(0, Math.min(index, this.entries().length - 1));
|
|
269
|
+
if (next === this.cursor.index)
|
|
270
|
+
return false;
|
|
271
|
+
this.cursor.index = next;
|
|
272
|
+
context.requestDraw();
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
activate(index, context) {
|
|
276
|
+
return this.run(index, context);
|
|
277
|
+
}
|
|
278
|
+
/** Runs an entry's message: sends it, or opens its form. */
|
|
279
|
+
run(index, context) {
|
|
280
|
+
const entry = this.entries()[index];
|
|
281
|
+
const addon = this.addon();
|
|
282
|
+
const data = this.readyValue;
|
|
283
|
+
if (!entry || !addon || !data)
|
|
284
|
+
return false;
|
|
285
|
+
const ticketUuid = ticketUuidOf(context, data.ticket);
|
|
286
|
+
const { message } = entry;
|
|
287
|
+
if (message.input) {
|
|
288
|
+
const state = {
|
|
289
|
+
ticketUuid,
|
|
290
|
+
addon: addon.name,
|
|
291
|
+
key: message.key,
|
|
292
|
+
label: message.label,
|
|
293
|
+
input: message.input,
|
|
294
|
+
};
|
|
295
|
+
context.navigate?.('ticket.addons.action', { ticketId: context.params?.ticketId ?? ticketUuid }, state);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
const client = context.services?.client;
|
|
299
|
+
if (!client)
|
|
300
|
+
return false;
|
|
301
|
+
runRowAction({
|
|
302
|
+
rowId: entry.id,
|
|
303
|
+
busy: this.sending,
|
|
304
|
+
context,
|
|
305
|
+
perform: async () => {
|
|
306
|
+
await sendAddonAction(client, ticketUuid, `${addon.name}.${message.key}`, {});
|
|
307
|
+
return { message: `${message.label} sent` };
|
|
308
|
+
},
|
|
309
|
+
refresh: (ctx) => this.retry(ctx),
|
|
310
|
+
setStatus: (status) => {
|
|
311
|
+
this.toast = status;
|
|
312
|
+
},
|
|
313
|
+
captureLifecycle: () => this.captureLifecycle(context),
|
|
314
|
+
lifecycleIsCurrent: (guard, ctx) => this.lifecycleIsCurrent(guard, ctx),
|
|
315
|
+
statusIsCurrent: (guard, ctx) => this.actionStillCurrent(guard, ctx),
|
|
316
|
+
failPrefix: `${addon.name}.${message.key} failed`,
|
|
317
|
+
});
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
handleInput(input, context) {
|
|
321
|
+
if (super.handleInput(input, context))
|
|
322
|
+
return true;
|
|
323
|
+
const data = this.readyValue;
|
|
324
|
+
if (this.state !== 'ready' || !data)
|
|
325
|
+
return false;
|
|
326
|
+
const count = data.model.addons.length;
|
|
327
|
+
if ((input === 'left' || input === 'right') && count > 1) {
|
|
328
|
+
this.selected =
|
|
329
|
+
(this.selected + (input === 'right' ? 1 : -1) + count) % count;
|
|
330
|
+
this.cursor.reset();
|
|
331
|
+
this.schedulePoll(context);
|
|
332
|
+
context.requestDraw();
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (input === 'enter' || input === ' ')
|
|
336
|
+
return this.run(this.cursor.index, context);
|
|
337
|
+
if (input === 'l') {
|
|
338
|
+
const addon = this.addon();
|
|
339
|
+
const markdown = addon ? latestMarkdown(addon) : undefined;
|
|
340
|
+
const href = markdown ? firstHttpLink(markdown) : undefined;
|
|
341
|
+
this.toast = copyThrough(context.copyToClipboard, href, href ?? '', 'no link in the latest answer');
|
|
342
|
+
context.requestDraw();
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
const entries = this.entries().length;
|
|
346
|
+
if (entries > 1) {
|
|
347
|
+
const before = this.cursor.index;
|
|
348
|
+
if (this.cursor.handle(input, entries) && this.cursor.index !== before) {
|
|
349
|
+
context.requestDraw();
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
commandForInput(input) {
|
|
356
|
+
return this.state === 'ready' && input === 'e'
|
|
357
|
+
? 'edit-assignment'
|
|
358
|
+
: undefined;
|
|
359
|
+
}
|
|
360
|
+
actionForInput(input, context) {
|
|
361
|
+
return this.commandForInput(input)
|
|
362
|
+
? ticketEditAction(context, this.readyValue?.ticket)
|
|
363
|
+
: undefined;
|
|
364
|
+
}
|
|
365
|
+
receiveResult(result, context) {
|
|
366
|
+
const returned = result;
|
|
367
|
+
if (returned?.type === 'addon-action-sent') {
|
|
368
|
+
this.toast = returned.message ?? '';
|
|
369
|
+
this.reload(context);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/** The form of an action with input, built from its input subset. */
|
|
374
|
+
export class AddonActionFormScreen extends AsyncModalScreen {
|
|
375
|
+
static route = {
|
|
376
|
+
id: 'ticket.addons.action',
|
|
377
|
+
path: '/tickets/:ticketId/addons/action',
|
|
378
|
+
parent: 'ticket.addons',
|
|
379
|
+
presentation: 'modal',
|
|
380
|
+
};
|
|
381
|
+
id = AddonActionFormScreen.route.id;
|
|
382
|
+
target;
|
|
383
|
+
form;
|
|
384
|
+
values;
|
|
385
|
+
errors = {};
|
|
386
|
+
sending = false;
|
|
387
|
+
async load(context) {
|
|
388
|
+
const target = context.navigationState;
|
|
389
|
+
if (!target?.input)
|
|
390
|
+
throw new Error('No addon action to fill in');
|
|
391
|
+
this.target = target;
|
|
392
|
+
this.form = addonForm(target.input);
|
|
393
|
+
const options = {};
|
|
394
|
+
for (const field of formFields(target.input))
|
|
395
|
+
if (field.options)
|
|
396
|
+
options[field.id] = field.options;
|
|
397
|
+
return options;
|
|
398
|
+
}
|
|
399
|
+
didLoad(options) {
|
|
400
|
+
if (!this.form || !this.target)
|
|
401
|
+
return;
|
|
402
|
+
const prefill = {};
|
|
403
|
+
for (const [id, field] of Object.entries(this.target.input.properties)) {
|
|
404
|
+
if (field.type === 'boolean')
|
|
405
|
+
prefill[id] = field.default === true ? 'yes' : 'no';
|
|
406
|
+
else if (field.default !== undefined)
|
|
407
|
+
prefill[id] = String(field.default);
|
|
408
|
+
}
|
|
409
|
+
this.values = this.form.initial(options, prefill);
|
|
410
|
+
}
|
|
411
|
+
renderLoading() {
|
|
412
|
+
return ['Loading…'];
|
|
413
|
+
}
|
|
414
|
+
renderReady(options) {
|
|
415
|
+
if (!this.form || !this.values)
|
|
416
|
+
return columnNode(FIELDS_REGION);
|
|
417
|
+
return renderDeclaredFieldsTree(this.form.fields, this.values, options, {
|
|
418
|
+
width: 58,
|
|
419
|
+
errorFor: (field) => this.errors[field.id],
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
modalChrome() {
|
|
423
|
+
return { title: this.target?.label ?? 'Addon action' };
|
|
424
|
+
}
|
|
425
|
+
actions() {
|
|
426
|
+
return [
|
|
427
|
+
actionItem('addon-form-field', '↑↓', 'field'),
|
|
428
|
+
actionItem('addon-form-send', 'Ctrl+S', 'send'),
|
|
429
|
+
actionItem('addon-form-esc', 'Esc', 'cancel'),
|
|
430
|
+
];
|
|
431
|
+
}
|
|
432
|
+
handleInput(input, context) {
|
|
433
|
+
if (super.handleInput(input, context))
|
|
434
|
+
return true;
|
|
435
|
+
const options = this.readyValue;
|
|
436
|
+
if (!options || !this.form || !this.values)
|
|
437
|
+
return false;
|
|
438
|
+
const intent = keyToIntent(input, this.form.activeField(this.values));
|
|
439
|
+
if (!intent)
|
|
440
|
+
return true;
|
|
441
|
+
if (intent.kind === 'cancel') {
|
|
442
|
+
context.back?.();
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
if (intent.kind === 'submit') {
|
|
446
|
+
void this.submit(context);
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
this.values = this.form.apply(this.values, options, intent);
|
|
450
|
+
context.requestDraw();
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
async submit(context) {
|
|
454
|
+
const target = this.target;
|
|
455
|
+
const client = context.services?.client;
|
|
456
|
+
if (!target || !client || !this.values || this.sending)
|
|
457
|
+
return;
|
|
458
|
+
const outcome = payloadFrom(target.input, this.values);
|
|
459
|
+
if ('errors' in outcome) {
|
|
460
|
+
this.errors = outcome.errors;
|
|
461
|
+
context.requestDraw();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.errors = {};
|
|
465
|
+
this.sending = true;
|
|
466
|
+
try {
|
|
467
|
+
await sendAddonAction(client, target.ticketUuid, `${target.addon}.${target.key}`, outcome.payload);
|
|
468
|
+
context.back?.({
|
|
469
|
+
type: 'addon-action-sent',
|
|
470
|
+
message: `${target.label} sent`,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
this.toast = error instanceof Error ? error.message : String(error);
|
|
475
|
+
context.requestDraw();
|
|
476
|
+
}
|
|
477
|
+
finally {
|
|
478
|
+
this.sending = false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
notice() {
|
|
482
|
+
return this.toast || undefined;
|
|
483
|
+
}
|
|
484
|
+
reset() {
|
|
485
|
+
this.target = undefined;
|
|
486
|
+
this.form = undefined;
|
|
487
|
+
this.values = undefined;
|
|
488
|
+
this.errors = {};
|
|
489
|
+
this.sending = false;
|
|
490
|
+
super.reset();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
@@ -18,12 +18,16 @@ export declare abstract class TicketContentScreen extends AsyncScreen<TicketDeta
|
|
|
18
18
|
private initializedKey;
|
|
19
19
|
protected labels: AssignmentLabels | undefined;
|
|
20
20
|
private status;
|
|
21
|
+
private readonly freezeBusy;
|
|
21
22
|
private releaseStore;
|
|
22
23
|
private expanded;
|
|
23
24
|
protected viewMode: UiViewMode;
|
|
24
25
|
/** Visit-local: the sidebar pane and Planning document tab last chosen. */
|
|
25
26
|
private sidebarTab;
|
|
26
27
|
private documentKey;
|
|
28
|
+
/** Overview only (GAIA-661); a stale load after navigating away is dropped. */
|
|
29
|
+
private addonsPreview;
|
|
30
|
+
private addonsPreviewGeneration;
|
|
27
31
|
private modelMemo;
|
|
28
32
|
compact(): boolean;
|
|
29
33
|
get document(): TicketDetailDocument | undefined;
|
|
@@ -37,6 +41,8 @@ export declare abstract class TicketContentScreen extends AsyncScreen<TicketDeta
|
|
|
37
41
|
contentProps(context: ScreenContext): TicketContentProps;
|
|
38
42
|
reload(context: ScreenContext): void;
|
|
39
43
|
enter(context: ScreenContext): void;
|
|
44
|
+
/** The inline card's data (GAIA-661); Overview only, loaded once per visit. */
|
|
45
|
+
private loadAddonsPreview;
|
|
40
46
|
leave(): void;
|
|
41
47
|
protected load(context: ScreenContext): Promise<TicketDetailDocument>;
|
|
42
48
|
protected didLoad(document: TicketDetailDocument, context: ScreenContext): void;
|
|
@@ -45,8 +51,23 @@ export declare abstract class TicketContentScreen extends AsyncScreen<TicketDeta
|
|
|
45
51
|
};
|
|
46
52
|
restoreState(token: unknown): void;
|
|
47
53
|
setLabels(labels: AssignmentLabels | undefined): void;
|
|
54
|
+
/**
|
|
55
|
+
* The model's selectable keys, plus the inline addons card's row (GAIA-661
|
|
56
|
+
* D1) when the Overview preview has a message worth reaching: a Tab/arrow
|
|
57
|
+
* visit stops there and Enter opens the full send/poll screen, the same
|
|
58
|
+
* keyboard reach the Details/Stats/Related sidebar card already has via its
|
|
59
|
+
* own (Technical details) row.
|
|
60
|
+
*/
|
|
61
|
+
private selectableKeys;
|
|
62
|
+
private openAddons;
|
|
48
63
|
select(index: number, context: ScreenContext): boolean;
|
|
49
64
|
activate(index: number, context: ScreenContext): boolean;
|
|
65
|
+
private isFrozen;
|
|
66
|
+
/**
|
|
67
|
+
* PATCH `frozen` through the write client, then re-read on the session
|
|
68
|
+
* client so the ladder and document agree with the server.
|
|
69
|
+
*/
|
|
70
|
+
private toggleFrozen;
|
|
50
71
|
handleInput(input: string, context: ScreenContext): boolean;
|
|
51
72
|
commandForInput(input: string): string | undefined;
|
|
52
73
|
actionForInput(input: string, context: ScreenContext): ScreenAction | undefined;
|