@gaia-ai/addon-gaia-ui 0.17.1 → 0.18.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/ticket-content/model.js +17 -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 +9 -0
- package/dist/src/Entity/ticket/Screen/conductor-tickets.d.ts +2 -2
- package/dist/src/Entity/ticket/Screen/ticket-content-screen.d.ts +7 -0
- package/dist/src/Entity/ticket/Screen/ticket-content-screen.js +53 -1
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +2 -2
- package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +2 -2
- package/package.json +2 -2
|
@@ -94,6 +94,23 @@ const lifecycleProperties = (document) => {
|
|
|
94
94
|
})()),
|
|
95
95
|
property('closed', attributes.closed === true ? 'yes' : 'no'),
|
|
96
96
|
];
|
|
97
|
+
const frozenAtRaw = attributes.frozen_at;
|
|
98
|
+
const frozenAt = asString(frozenAtRaw) ??
|
|
99
|
+
(typeof frozenAtRaw === 'number' && frozenAtRaw > 0
|
|
100
|
+
? new Date(frozenAtRaw * 1000).toISOString()
|
|
101
|
+
: undefined);
|
|
102
|
+
if (attributes.frozen === true) {
|
|
103
|
+
extras.push(property('frozen', 'yes'));
|
|
104
|
+
if (frozenAt !== undefined)
|
|
105
|
+
extras.push(property('frozen at', frozenAt));
|
|
106
|
+
}
|
|
107
|
+
const stall = asString(attributes.stall_reason);
|
|
108
|
+
if (stall !== undefined) {
|
|
109
|
+
// `property()` treats values that start with "no " as empty states
|
|
110
|
+
// (`No conductor.` → none). `no conductor` is a real stall reason, so
|
|
111
|
+
// the value is kept verbatim.
|
|
112
|
+
extras.push({ key: 'stall', value: stall });
|
|
113
|
+
}
|
|
97
114
|
const createdAt = asString(attributes.created);
|
|
98
115
|
if (createdAt !== undefined)
|
|
99
116
|
extras.push(property('created at', createdAt));
|
|
@@ -23,6 +23,10 @@ export interface TicketRow {
|
|
|
23
23
|
projectId: string | null;
|
|
24
24
|
/** Boolean closed flag; an absent attribute reads as open (spec D5). */
|
|
25
25
|
closed: boolean;
|
|
26
|
+
/** Operator hold; an absent attribute reads as not frozen (GAIA-302). */
|
|
27
|
+
frozen: boolean;
|
|
28
|
+
/** Why the ticket is standing still: frozen|blocked|no conductor|failing, else ''. */
|
|
29
|
+
stallReason: string;
|
|
26
30
|
}
|
|
27
31
|
/** A `(id) → resource` lookup over a document's sideloaded `included` entries.
|
|
28
32
|
* JSON:API ids are UUIDs (unique across bundles), so id alone keys uniquely. */
|
|
@@ -83,8 +87,8 @@ export declare const TICKET_COLUMNS: readonly [{
|
|
|
83
87
|
}, {
|
|
84
88
|
readonly key: "run";
|
|
85
89
|
readonly label: "RUN";
|
|
86
|
-
readonly width:
|
|
87
|
-
readonly fields: readonly ["derived_status"];
|
|
90
|
+
readonly width: 12;
|
|
91
|
+
readonly fields: readonly ["derived_status", "stall_reason"];
|
|
88
92
|
readonly get: (r: TicketRow) => string;
|
|
89
93
|
}, {
|
|
90
94
|
readonly key: "parent";
|
|
@@ -153,8 +157,8 @@ export declare const TICKET_TABLE: {
|
|
|
153
157
|
}, {
|
|
154
158
|
readonly key: "run";
|
|
155
159
|
readonly label: "RUN";
|
|
156
|
-
readonly width:
|
|
157
|
-
readonly fields: readonly ["derived_status"];
|
|
160
|
+
readonly width: 12;
|
|
161
|
+
readonly fields: readonly ["derived_status", "stall_reason"];
|
|
158
162
|
readonly get: (r: TicketRow) => string;
|
|
159
163
|
}, {
|
|
160
164
|
readonly key: "parent";
|
|
@@ -217,8 +221,8 @@ export declare const CONDUCTOR_TICKET_COLUMNS: readonly [{
|
|
|
217
221
|
}, {
|
|
218
222
|
readonly key: "run";
|
|
219
223
|
readonly label: "RUN";
|
|
220
|
-
readonly width:
|
|
221
|
-
readonly fields: readonly ["derived_status"];
|
|
224
|
+
readonly width: 12;
|
|
225
|
+
readonly fields: readonly ["derived_status", "stall_reason"];
|
|
222
226
|
readonly get: (r: TicketRow) => string;
|
|
223
227
|
}, {
|
|
224
228
|
readonly key: "conductor";
|
|
@@ -264,8 +268,8 @@ export declare const CONDUCTOR_TICKET_TABLE: {
|
|
|
264
268
|
}, {
|
|
265
269
|
readonly key: "run";
|
|
266
270
|
readonly label: "RUN";
|
|
267
|
-
readonly width:
|
|
268
|
-
readonly fields: readonly ["derived_status"];
|
|
271
|
+
readonly width: 12;
|
|
272
|
+
readonly fields: readonly ["derived_status", "stall_reason"];
|
|
269
273
|
readonly get: (r: TicketRow) => string;
|
|
270
274
|
}, {
|
|
271
275
|
readonly key: "conductor";
|
|
@@ -292,7 +296,7 @@ export declare const CONDUCTOR_TICKET_TABLE: {
|
|
|
292
296
|
* down to the bytes on the wire. Column order would emit `title` last (it rides
|
|
293
297
|
* the trailing TITLE column) instead of second.
|
|
294
298
|
*/
|
|
295
|
-
export declare const TICKET_LIST_ATTRIBUTES: readonly ["identifier", "title", "state", "priority", "assignee_initials", "owner_initials", "derived_status", "workflow", "closed"];
|
|
299
|
+
export declare const TICKET_LIST_ATTRIBUTES: readonly ["identifier", "title", "state", "priority", "assignee_initials", "owner_initials", "derived_status", "stall_reason", "workflow", "closed", "frozen"];
|
|
296
300
|
/** What `addFields('gaia_ticket--gaia_ticket', …)` takes. */
|
|
297
301
|
export declare const TICKET_LIST_FIELDS: string[];
|
|
298
302
|
/** The sideloaded conductor renders one column: its label. */
|
|
@@ -57,7 +57,7 @@ export const TICKET_LIST_RELATIONSHIPS = [
|
|
|
57
57
|
* columns' union PLUS this list, and this list is an explicit declaration rather
|
|
58
58
|
* than an unstated remainder.
|
|
59
59
|
*/
|
|
60
|
-
const TICKET_PREDICATE_ATTRIBUTES = ['workflow', 'closed'];
|
|
60
|
+
const TICKET_PREDICATE_ATTRIBUTES = ['workflow', 'closed', 'frozen'];
|
|
61
61
|
/**
|
|
62
62
|
* The ticket list's columns — the header, the width, the cell value AND the
|
|
63
63
|
* fieldset entry of each, in ONE declaration (GAIA-294 AC 4).
|
|
@@ -125,9 +125,13 @@ export const TICKET_COLUMNS = [
|
|
|
125
125
|
{
|
|
126
126
|
key: 'run',
|
|
127
127
|
label: 'RUN',
|
|
128
|
-
width:
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
width: 12,
|
|
129
|
+
// `stall_reason` rides this column: GAIA-302 shows WHY a ticket is standing
|
|
130
|
+
// still exactly where it would otherwise show what it is doing, so the two
|
|
131
|
+
// share one cell and therefore one fieldset entry. Width 12 fits
|
|
132
|
+
// `no conductor`, the longest operator-facing reason.
|
|
133
|
+
fields: ['derived_status', 'stall_reason'],
|
|
134
|
+
get: (r) => r.stallReason || r.derivedStatus,
|
|
131
135
|
},
|
|
132
136
|
{
|
|
133
137
|
key: 'parent',
|
|
@@ -220,8 +224,10 @@ export const TICKET_LIST_ATTRIBUTES = [
|
|
|
220
224
|
'assignee_initials',
|
|
221
225
|
'owner_initials',
|
|
222
226
|
'derived_status',
|
|
227
|
+
'stall_reason',
|
|
223
228
|
'workflow',
|
|
224
229
|
'closed',
|
|
230
|
+
'frozen',
|
|
225
231
|
];
|
|
226
232
|
/** What `addFields('gaia_ticket--gaia_ticket', …)` takes. */
|
|
227
233
|
export const TICKET_LIST_FIELDS = [
|
|
@@ -265,6 +271,7 @@ export function mapTicketData(item, included) {
|
|
|
265
271
|
assigneeInitials: str('assignee_initials') ?? '',
|
|
266
272
|
ownerInitials: str('owner_initials') ?? '',
|
|
267
273
|
derivedStatus: str('derived_status') ?? 'idle',
|
|
274
|
+
stallReason: str('stall_reason') ?? '',
|
|
268
275
|
labels,
|
|
269
276
|
type: str('workflow') ?? null,
|
|
270
277
|
parent,
|
|
@@ -272,6 +279,7 @@ export function mapTicketData(item, included) {
|
|
|
272
279
|
assigneeId: singleRelId(rel('assignee_user_id')),
|
|
273
280
|
projectId: singleRelId(rel('project_id')),
|
|
274
281
|
closed: typeof raw('closed') === 'boolean' ? raw('closed') : false,
|
|
282
|
+
frozen: typeof raw('frozen') === 'boolean' ? raw('frozen') : false,
|
|
275
283
|
};
|
|
276
284
|
}
|
|
277
285
|
/**
|
|
@@ -11,6 +11,13 @@ export declare const TICKET_TYPE = "gaia_ticket--gaia_ticket";
|
|
|
11
11
|
* silent failure that criterion forbids.
|
|
12
12
|
*/
|
|
13
13
|
export declare function updateTicketAssignment(client: DropshClient | undefined, ticketId: string, body: ResourceWriteBody): Promise<unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* PATCH a ticket's `frozen` flag (GAIA-302).
|
|
16
|
+
*
|
|
17
|
+
* Same write path as assignment: a refusal is not caught here so the caller
|
|
18
|
+
* can put the server's words on the status line.
|
|
19
|
+
*/
|
|
20
|
+
export declare function updateTicketFrozen(client: DropshClient | undefined, ticketId: string, frozen: boolean): Promise<unknown>;
|
|
14
21
|
/** The text format every ticket description is stored in (`gaia_rich`). */
|
|
15
22
|
export declare const TICKET_DESCRIPTION_FORMAT = "gaia_rich";
|
|
16
23
|
/**
|
|
@@ -16,6 +16,15 @@ export function updateTicketAssignment(client, ticketId, body) {
|
|
|
16
16
|
}
|
|
17
17
|
return client.update(TICKET_TYPE, ticketId, body);
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* PATCH a ticket's `frozen` flag (GAIA-302).
|
|
21
|
+
*
|
|
22
|
+
* Same write path as assignment: a refusal is not caught here so the caller
|
|
23
|
+
* can put the server's words on the status line.
|
|
24
|
+
*/
|
|
25
|
+
export function updateTicketFrozen(client, ticketId, frozen) {
|
|
26
|
+
return updateTicketAssignment(client, ticketId, { attributes: { frozen } });
|
|
27
|
+
}
|
|
19
28
|
/** The text format every ticket description is stored in (`gaia_rich`). */
|
|
20
29
|
export const TICKET_DESCRIPTION_FORMAT = 'gaia_rich';
|
|
21
30
|
/** A JSON:API to-one linkage, or nothing at all. */
|
|
@@ -57,8 +57,8 @@ export declare class ConductorTicketsScreen extends ListScreen<TicketRow, void>
|
|
|
57
57
|
}, {
|
|
58
58
|
readonly key: "run";
|
|
59
59
|
readonly label: "RUN";
|
|
60
|
-
readonly width:
|
|
61
|
-
readonly fields: readonly ["derived_status"];
|
|
60
|
+
readonly width: 12;
|
|
61
|
+
readonly fields: readonly ["derived_status", "stall_reason"];
|
|
62
62
|
readonly get: (r: TicketRow) => string;
|
|
63
63
|
}, {
|
|
64
64
|
readonly key: "conductor";
|
|
@@ -18,6 +18,7 @@ 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;
|
|
@@ -47,6 +48,12 @@ export declare abstract class TicketContentScreen extends AsyncScreen<TicketDeta
|
|
|
47
48
|
setLabels(labels: AssignmentLabels | undefined): void;
|
|
48
49
|
select(index: number, context: ScreenContext): boolean;
|
|
49
50
|
activate(index: number, context: ScreenContext): boolean;
|
|
51
|
+
private isFrozen;
|
|
52
|
+
/**
|
|
53
|
+
* PATCH `frozen` through the write client, then re-read on the session
|
|
54
|
+
* client so the ladder and document agree with the server.
|
|
55
|
+
*/
|
|
56
|
+
private toggleFrozen;
|
|
50
57
|
handleInput(input: string, context: ScreenContext): boolean;
|
|
51
58
|
commandForInput(input: string): string | undefined;
|
|
52
59
|
actionForInput(input: string, context: ScreenContext): ScreenAction | undefined;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { actionItem, } from '../../../Component/footer-actions/index.js';
|
|
2
2
|
import { copyThrough } from '../../../lib/clipboard.js';
|
|
3
|
+
import { serverMessage } from '../../../lib/jsonapi-error.js';
|
|
3
4
|
import { BORDER, textNode } from '../../../lib/vnode.js';
|
|
4
5
|
import { AsyncScreen, } from '../../../Screen/async-screen.js';
|
|
6
|
+
import { runRowAction } from '../../../Screen/row-action.js';
|
|
5
7
|
import { TicketContent, } from '../Component/ticket-content/index.js';
|
|
6
8
|
import { initialExpandedKeys, resolveSelectedKey, retainExpandedKeys, ticketContentModel, ticketContentRowId, } from '../Component/ticket-content/model.js';
|
|
7
9
|
import { handleTicketCopy, ticketCopyActions, ticketEditAction, ticketHeaderTree, ticketTitleOf, } from '../Component/ticket-detail/index.js';
|
|
8
10
|
import { ticketTeaserLines, ticketTeaserTree, } from '../Component/ticket-teaser/index.js';
|
|
11
|
+
import { loadTicketDetail } from '../Data/detail.js';
|
|
9
12
|
import { ticketDetailInputContext } from '../Data/input.js';
|
|
10
13
|
import { loadTicketView, readTicket, refreshTicket, saveTicket, ticketLoaded, watchTicket, } from '../Data/store.js';
|
|
14
|
+
import { updateTicketFrozen } from '../Data/write.js';
|
|
11
15
|
/** Overview width when the context carries none — a pipe has no terminal. */
|
|
12
16
|
const DEFAULT_COLUMNS = 80;
|
|
13
17
|
/**
|
|
@@ -20,6 +24,7 @@ export class TicketContentScreen extends AsyncScreen {
|
|
|
20
24
|
initializedKey;
|
|
21
25
|
labels;
|
|
22
26
|
status = '';
|
|
27
|
+
freezeBusy = new Set();
|
|
23
28
|
releaseStore;
|
|
24
29
|
expanded = new Set();
|
|
25
30
|
viewMode = 'full';
|
|
@@ -186,6 +191,47 @@ export class TicketContentScreen extends AsyncScreen {
|
|
|
186
191
|
context.requestDraw();
|
|
187
192
|
return true;
|
|
188
193
|
}
|
|
194
|
+
isFrozen() {
|
|
195
|
+
return this.document?.data?.attributes?.frozen === true;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* PATCH `frozen` through the write client, then re-read on the session
|
|
199
|
+
* client so the ladder and document agree with the server.
|
|
200
|
+
*/
|
|
201
|
+
toggleFrozen(context) {
|
|
202
|
+
const id = this.document?.data?.id ?? context.params?.ticketId;
|
|
203
|
+
if (!id || !context.services)
|
|
204
|
+
return true;
|
|
205
|
+
const next = !this.isFrozen();
|
|
206
|
+
const guard = this.captureLifecycle(context);
|
|
207
|
+
return runRowAction({
|
|
208
|
+
rowId: id,
|
|
209
|
+
busy: this.freezeBusy,
|
|
210
|
+
context,
|
|
211
|
+
perform: async () => {
|
|
212
|
+
try {
|
|
213
|
+
await updateTicketFrozen(context.services?.writeClient ?? context.services?.client, id, next);
|
|
214
|
+
}
|
|
215
|
+
catch (caught) {
|
|
216
|
+
throw new Error(serverMessage(caught));
|
|
217
|
+
}
|
|
218
|
+
return { message: next ? 'frozen' : 'no longer frozen' };
|
|
219
|
+
},
|
|
220
|
+
refresh: async (ctx) => {
|
|
221
|
+
refreshTicket(ctx);
|
|
222
|
+
if (!ctx.services)
|
|
223
|
+
return;
|
|
224
|
+
const document = await loadTicketDetail(ctx.services.client, id, this.includes);
|
|
225
|
+
this.replaceReadyIfCurrent(guard, saveTicket(ctx, document), ctx);
|
|
226
|
+
},
|
|
227
|
+
setStatus: (status) => {
|
|
228
|
+
this.status = status;
|
|
229
|
+
},
|
|
230
|
+
captureLifecycle: () => guard,
|
|
231
|
+
lifecycleIsCurrent: (current, ctx) => this.lifecycleIsCurrent(current, ctx),
|
|
232
|
+
failPrefix: 'freeze failed',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
189
235
|
handleInput(input, context) {
|
|
190
236
|
if (super.handleInput(input, context))
|
|
191
237
|
return true;
|
|
@@ -198,6 +244,8 @@ export class TicketContentScreen extends AsyncScreen {
|
|
|
198
244
|
context.requestDraw();
|
|
199
245
|
return true;
|
|
200
246
|
}
|
|
247
|
+
if (input === 'f')
|
|
248
|
+
return this.toggleFrozen(context);
|
|
201
249
|
if ((input === 'enter' || input === ' ') && keys.length > 0)
|
|
202
250
|
return this.activate(selectedIndex, context);
|
|
203
251
|
if (keys.length > 1 && (input === 'j' || input === 'down'))
|
|
@@ -276,9 +324,12 @@ export class TicketContentScreen extends AsyncScreen {
|
|
|
276
324
|
return [
|
|
277
325
|
actionItem('detail-tab', 'Tab', 'section'),
|
|
278
326
|
actionItem('detail-edit', 'e', 'edit'),
|
|
279
|
-
|
|
327
|
+
// Freeze and paging before copy: at 80 columns the footer clips the
|
|
328
|
+
// tail, and `Y:Copy link` is the hint that yields.
|
|
329
|
+
actionItem('detail-freeze', 'f', this.isFrozen() ? 'unfreeze' : 'freeze'),
|
|
280
330
|
actionItem('detail-arrows', '↑↓', selectable > 1 ? 'select' : 'scroll'),
|
|
281
331
|
actionItem('detail-page', 'PgUp/PgDn', 'page'),
|
|
332
|
+
...ticketCopyActions(),
|
|
282
333
|
...(selectable > 0
|
|
283
334
|
? [actionItem('detail-enter', 'Enter/Space', 'expand')]
|
|
284
335
|
: []),
|
|
@@ -298,6 +349,7 @@ export class TicketContentScreen extends AsyncScreen {
|
|
|
298
349
|
this.initializedKey = undefined;
|
|
299
350
|
this.labels = undefined;
|
|
300
351
|
this.status = '';
|
|
352
|
+
this.freezeBusy.clear();
|
|
301
353
|
this.expanded = new Set();
|
|
302
354
|
this.viewMode = 'full';
|
|
303
355
|
}
|
|
@@ -41,8 +41,8 @@ export declare class TicketsScreen extends ListScreen<TicketRow, void> {
|
|
|
41
41
|
}, {
|
|
42
42
|
readonly key: "run";
|
|
43
43
|
readonly label: "RUN";
|
|
44
|
-
readonly width:
|
|
45
|
-
readonly fields: readonly ["derived_status"];
|
|
44
|
+
readonly width: 12;
|
|
45
|
+
readonly fields: readonly ["derived_status", "stall_reason"];
|
|
46
46
|
readonly get: (r: TicketRow) => string;
|
|
47
47
|
}, {
|
|
48
48
|
readonly key: "parent";
|
|
@@ -42,8 +42,8 @@ export declare abstract class MemberTicketsScreen extends ListScreen<TicketRow,
|
|
|
42
42
|
}, {
|
|
43
43
|
readonly key: "run";
|
|
44
44
|
readonly label: "RUN";
|
|
45
|
-
readonly width:
|
|
46
|
-
readonly fields: readonly ["derived_status"];
|
|
45
|
+
readonly width: 12;
|
|
46
|
+
readonly fields: readonly ["derived_status", "stall_reason"];
|
|
47
47
|
readonly get: (r: TicketRow) => string;
|
|
48
48
|
}, {
|
|
49
49
|
readonly key: "parent";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaia-ai/addon-gaia-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "GAIA TUI: interactive terminal UI (dashboard, projects, tickets) as a dropsh `tui` renderer plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,6 +29,6 @@
|
|
|
29
29
|
"zustand": "^5.0.15"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@gaia-ai/core": "^0.
|
|
32
|
+
"@gaia-ai/core": "^0.18.0"
|
|
33
33
|
}
|
|
34
34
|
}
|