@gaia-ai/addon-gaia-ui 0.16.1 → 0.16.2
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/team/Data/workload.d.ts +3 -1
- package/dist/src/Entity/team/Data/workload.js +6 -4
- package/dist/src/Entity/team/Screen/team-work.d.ts +12 -1
- package/dist/src/Entity/team/Screen/team-work.js +47 -2
- package/dist/src/Entity/ticket/Data/parents.js +1 -1
- package/dist/src/Entity/ticket/Data/query.d.ts +2 -2
- package/dist/src/Entity/ticket/Data/query.js +13 -9
- package/dist/src/Entity/ticket/Data/tickets.d.ts +1 -1
- package/dist/src/Entity/ticket/Data/tickets.js +1 -1
- package/dist/src/Entity/ticket/Form/edit.d.ts +13 -0
- package/dist/src/Entity/ticket/Form/edit.js +17 -7
- package/dist/src/Entity/ticket/Screen/conductor-tickets.d.ts +7 -1
- package/dist/src/Entity/ticket/Screen/conductor-tickets.js +30 -6
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +3 -1
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +21 -6
- package/dist/src/Entity/ticket/Screen/my-work.d.ts +8 -1
- package/dist/src/Entity/ticket/Screen/my-work.js +37 -4
- package/dist/src/Entity/ticket/Screen/ticket-filter-bar.js +2 -9
- package/dist/src/Entity/ticket/Screen/ticket-search.d.ts +44 -0
- package/dist/src/Entity/ticket/Screen/ticket-search.js +97 -0
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +9 -7
- package/dist/src/Entity/ticket/Screen/tickets.js +25 -85
- package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +7 -0
- package/dist/src/Entity/ticket/Screen/user-tickets.js +30 -2
- package/dist/src/Screen/async-screen.d.ts +7 -0
- package/dist/src/Screen/async-screen.js +25 -4
- package/dist/src/Screen/list-screen.d.ts +11 -2
- package/dist/src/Screen/list-screen.js +20 -2
- package/dist/src/route.js +37 -0
- package/dist/src/types.d.ts +5 -1
- package/package.json +2 -2
|
@@ -105,18 +105,11 @@ export class TicketFilterBar {
|
|
|
105
105
|
}
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
|
-
if (!this.disable.has('search') && input === '/') {
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
108
|
return false;
|
|
112
109
|
}
|
|
113
110
|
applyTo(row) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
row.identifier.toLocaleLowerCase().includes(query) ||
|
|
117
|
-
row.title.toLocaleLowerCase().includes(query)) &&
|
|
118
|
-
(this.host.criteria.state === null ||
|
|
119
|
-
row.state === this.host.criteria.state) &&
|
|
111
|
+
return ((this.host.criteria.state === null ||
|
|
112
|
+
row.state === this.host.criteria.state) &&
|
|
120
113
|
(this.host.criteria.closed === null ||
|
|
121
114
|
row.closed === this.host.criteria.closed) &&
|
|
122
115
|
(this.host.criteria.parent === null ||
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ListFilterBadge } from '../../../Screen/full-screen.js';
|
|
2
|
+
import type { ScreenContext } from '../../../Screen/screen.js';
|
|
3
|
+
import type { TicketRow } from '../Data/tickets.js';
|
|
4
|
+
export interface TicketSearchHost {
|
|
5
|
+
/** The typed line; the host's loader sends it as `filter[search]`. */
|
|
6
|
+
searchQuery: string;
|
|
7
|
+
/** The rows on screen, narrowed locally while the server read is in flight.
|
|
8
|
+
* Absent on a screen that shows no ticket rows (Team → Work). */
|
|
9
|
+
readonly rows?: readonly TicketRow[];
|
|
10
|
+
/** Show these rows until the server answers; `[]` after a refusal. */
|
|
11
|
+
showRows(rows: readonly TicketRow[]): void;
|
|
12
|
+
/** Re-read for the current query — `AsyncScreen.reloadInPlace` plus any
|
|
13
|
+
* criteria-change bookkeeping (page one, cursor). */
|
|
14
|
+
reloadSearch(context: ScreenContext): Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The `/` search every terminal ticket list shares (GAIA-544 AC9).
|
|
18
|
+
*
|
|
19
|
+
* Composed, not inherited: the lists sit on `ListScreen` and on bare
|
|
20
|
+
* `AsyncScreen`. The server resolves the line — identifiers and fragments,
|
|
21
|
+
* origin references (`#19`, `gaia#19`, URLs) — so the screen only narrows
|
|
22
|
+
* optimistically, debounces the reload, drops stale answers (through
|
|
23
|
+
* `reloadInPlace`) and states a refusal as the server's sentence over an
|
|
24
|
+
* emptied list, only while that line is still the query.
|
|
25
|
+
*/
|
|
26
|
+
export declare class TicketSearch {
|
|
27
|
+
private readonly host;
|
|
28
|
+
open: boolean;
|
|
29
|
+
private refusal;
|
|
30
|
+
private refusedQuery;
|
|
31
|
+
private base;
|
|
32
|
+
private cancel;
|
|
33
|
+
constructor(host: TicketSearchHost);
|
|
34
|
+
/** The server's refusal of the current line, or ''. */
|
|
35
|
+
get status(): string;
|
|
36
|
+
badge(): ListFilterBadge;
|
|
37
|
+
/** The meta-line hint while the field is open, or ''. */
|
|
38
|
+
hint(): string;
|
|
39
|
+
/** Claims `/` to open the field and, while it is open, every key. */
|
|
40
|
+
handleKey(input: string, context: ScreenContext): boolean;
|
|
41
|
+
/** Close the field and drop a pending reload — on leave and reset. */
|
|
42
|
+
close(): void;
|
|
43
|
+
reset(): void;
|
|
44
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { serverMessage } from '../../../lib/jsonapi-error.js';
|
|
2
|
+
import { timerDebounce } from '../../../Widget/option-source.js';
|
|
3
|
+
/**
|
|
4
|
+
* The `/` search every terminal ticket list shares (GAIA-544 AC9).
|
|
5
|
+
*
|
|
6
|
+
* Composed, not inherited: the lists sit on `ListScreen` and on bare
|
|
7
|
+
* `AsyncScreen`. The server resolves the line — identifiers and fragments,
|
|
8
|
+
* origin references (`#19`, `gaia#19`, URLs) — so the screen only narrows
|
|
9
|
+
* optimistically, debounces the reload, drops stale answers (through
|
|
10
|
+
* `reloadInPlace`) and states a refusal as the server's sentence over an
|
|
11
|
+
* emptied list, only while that line is still the query.
|
|
12
|
+
*/
|
|
13
|
+
export class TicketSearch {
|
|
14
|
+
host;
|
|
15
|
+
open = false;
|
|
16
|
+
refusal = '';
|
|
17
|
+
refusedQuery = '';
|
|
18
|
+
base = [];
|
|
19
|
+
cancel;
|
|
20
|
+
constructor(host) {
|
|
21
|
+
this.host = host;
|
|
22
|
+
}
|
|
23
|
+
/** The server's refusal of the current line, or ''. */
|
|
24
|
+
get status() {
|
|
25
|
+
return this.host.searchQuery === this.refusedQuery ? this.refusal : '';
|
|
26
|
+
}
|
|
27
|
+
badge() {
|
|
28
|
+
const query = this.host.searchQuery;
|
|
29
|
+
return query
|
|
30
|
+
? { id: 'search', label: '/ search', value: `search: ${query}` }
|
|
31
|
+
: { id: 'search', label: '/ search' };
|
|
32
|
+
}
|
|
33
|
+
/** The meta-line hint while the field is open, or ''. */
|
|
34
|
+
hint() {
|
|
35
|
+
return this.open
|
|
36
|
+
? 'searching IDENTIFIER + TITLE + ORIGIN · type to filter · Backspace erase · Enter keep · Esc leave'
|
|
37
|
+
: '';
|
|
38
|
+
}
|
|
39
|
+
/** Claims `/` to open the field and, while it is open, every key. */
|
|
40
|
+
handleKey(input, context) {
|
|
41
|
+
if (!this.open) {
|
|
42
|
+
if (input !== '/')
|
|
43
|
+
return false;
|
|
44
|
+
this.open = true;
|
|
45
|
+
this.base = [...(this.host.rows ?? [])];
|
|
46
|
+
context.requestDraw();
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (input === 'escape' || input === 'enter') {
|
|
50
|
+
this.open = false;
|
|
51
|
+
context.requestDraw();
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const query = this.host.searchQuery;
|
|
55
|
+
if (input === 'backspace')
|
|
56
|
+
this.host.searchQuery = query.slice(0, -1);
|
|
57
|
+
else if (input.length === 1 && input >= ' ')
|
|
58
|
+
this.host.searchQuery = query + input;
|
|
59
|
+
else
|
|
60
|
+
return true;
|
|
61
|
+
if (this.host.rows !== undefined) {
|
|
62
|
+
const needle = this.host.searchQuery.toLocaleLowerCase();
|
|
63
|
+
this.host.showRows(this.base.filter((row) => row.identifier.toLocaleLowerCase().includes(needle) ||
|
|
64
|
+
row.title.toLocaleLowerCase().includes(needle)));
|
|
65
|
+
}
|
|
66
|
+
this.cancel?.();
|
|
67
|
+
const debounce = context.options?.debounce ?? timerDebounce;
|
|
68
|
+
this.cancel = debounce(() => {
|
|
69
|
+
void this.host.reloadSearch(context).then((current) => {
|
|
70
|
+
if (!current)
|
|
71
|
+
return;
|
|
72
|
+
this.refusal = '';
|
|
73
|
+
this.refusedQuery = '';
|
|
74
|
+
context.requestDraw();
|
|
75
|
+
}, (caught) => {
|
|
76
|
+
this.refusal = serverMessage(caught);
|
|
77
|
+
this.refusedQuery = this.host.searchQuery;
|
|
78
|
+
this.host.showRows([]);
|
|
79
|
+
context.requestDraw();
|
|
80
|
+
});
|
|
81
|
+
}, 200);
|
|
82
|
+
context.requestDraw();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/** Close the field and drop a pending reload — on leave and reset. */
|
|
86
|
+
close() {
|
|
87
|
+
this.open = false;
|
|
88
|
+
this.cancel?.();
|
|
89
|
+
this.cancel = undefined;
|
|
90
|
+
}
|
|
91
|
+
reset() {
|
|
92
|
+
this.close();
|
|
93
|
+
this.refusal = '';
|
|
94
|
+
this.refusedQuery = '';
|
|
95
|
+
this.base = [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -84,10 +84,12 @@ export declare class TicketsScreen extends ListScreen<TicketRow, void> {
|
|
|
84
84
|
receiveResult(result: unknown, context: ScreenContext): void;
|
|
85
85
|
private openingEdit;
|
|
86
86
|
private status;
|
|
87
|
-
private
|
|
88
|
-
private cancelSearch;
|
|
89
|
-
private searchBase;
|
|
87
|
+
private readonly search;
|
|
90
88
|
leave(): void;
|
|
89
|
+
get searchQuery(): string;
|
|
90
|
+
set searchQuery(query: string);
|
|
91
|
+
/** The search is a criteria change like any other (D5): page one. */
|
|
92
|
+
reloadSearch(context: ScreenContext): Promise<boolean>;
|
|
91
93
|
row(id: string): TicketRow | undefined;
|
|
92
94
|
selectId(id: string): boolean;
|
|
93
95
|
saveState(): {
|
|
@@ -169,10 +171,10 @@ export declare class TicketsScreen extends ListScreen<TicketRow, void> {
|
|
|
169
171
|
* for the criteria on screen right now. */
|
|
170
172
|
private hiddenNotice;
|
|
171
173
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
174
|
+
* Re-apply the bar's criteria except the search line: the server already
|
|
175
|
+
* resolved `filter[search]`, including origin references whose identifier
|
|
176
|
+
* and title omit the typed text. The rest protects a cached re-entry from
|
|
177
|
+
* clients that ignore the other query parameters.
|
|
176
178
|
*/
|
|
177
179
|
protected filterRow(row: TicketRow): boolean;
|
|
178
180
|
/** Every criteria change starts over at page one: a narrowed filter applied on
|
|
@@ -2,13 +2,13 @@ import { actionItem } from '../../../Component/footer-actions/index.js';
|
|
|
2
2
|
import { copyThrough } from '../../../lib/clipboard.js';
|
|
3
3
|
import { serverMessage } from '../../../lib/jsonapi-error.js';
|
|
4
4
|
import { ListScreen } from '../../../Screen/list-screen.js';
|
|
5
|
-
import { timerDebounce } from '../../../Widget/option-source.js';
|
|
6
5
|
import { resolveWhoCandidate } from '../../user/Data/users.js';
|
|
7
6
|
import { loadTicketDetail } from '../Data/detail.js';
|
|
8
7
|
import { CLOSED_FILTER_VALUES, cycleValue, DEFAULT_TICKET_CRITERIA, STATE_FILTER_VALUES, } from '../Data/query.js';
|
|
9
8
|
import { countTicketsIgnoringClosed, loadFilteredTickets, mapTicketDocument, TICKET_TABLE, } from '../Data/tickets.js';
|
|
10
9
|
import { ticketUrl } from '../Data/url.js';
|
|
11
10
|
import { TicketFilterBar } from './ticket-filter-bar.js';
|
|
11
|
+
import { TicketSearch } from './ticket-search.js';
|
|
12
12
|
export class TicketsScreen extends ListScreen {
|
|
13
13
|
frameId = 'tickets';
|
|
14
14
|
table = TICKET_TABLE;
|
|
@@ -105,18 +105,22 @@ export class TicketsScreen extends ListScreen {
|
|
|
105
105
|
}
|
|
106
106
|
openingEdit = false;
|
|
107
107
|
status = '';
|
|
108
|
-
|
|
109
|
-
cancelSearch;
|
|
110
|
-
searchBase = [];
|
|
108
|
+
search = new TicketSearch(this);
|
|
111
109
|
leave() {
|
|
112
|
-
|
|
113
|
-
this.searchMode = false;
|
|
114
|
-
if (cancelPendingSearch) {
|
|
115
|
-
this.cancelSearch?.();
|
|
116
|
-
this.cancelSearch = undefined;
|
|
117
|
-
}
|
|
110
|
+
this.search.close();
|
|
118
111
|
super.leave();
|
|
119
112
|
}
|
|
113
|
+
get searchQuery() {
|
|
114
|
+
return this.criteria.query;
|
|
115
|
+
}
|
|
116
|
+
set searchQuery(query) {
|
|
117
|
+
this.criteria = { ...this.criteria, query };
|
|
118
|
+
}
|
|
119
|
+
/** The search is a criteria change like any other (D5): page one. */
|
|
120
|
+
reloadSearch(context) {
|
|
121
|
+
this.page = 0;
|
|
122
|
+
return this.reloadInPlace(context);
|
|
123
|
+
}
|
|
120
124
|
row(id) {
|
|
121
125
|
return this.readyValue?.rows.find((candidate) => candidate.id === id);
|
|
122
126
|
}
|
|
@@ -285,10 +289,10 @@ export class TicketsScreen extends ListScreen {
|
|
|
285
289
|
return `${hidden.count} match${hidden.count === 1 ? '' : 'es'} hidden by closed — press x`;
|
|
286
290
|
}
|
|
287
291
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
+
* Re-apply the bar's criteria except the search line: the server already
|
|
293
|
+
* resolved `filter[search]`, including origin references whose identifier
|
|
294
|
+
* and title omit the typed text. The rest protects a cached re-entry from
|
|
295
|
+
* clients that ignore the other query parameters.
|
|
292
296
|
*/
|
|
293
297
|
filterRow(row) {
|
|
294
298
|
return this.filter.applyTo(row);
|
|
@@ -387,7 +391,7 @@ export class TicketsScreen extends ListScreen {
|
|
|
387
391
|
this.criteria.who?.id === this.mine.id;
|
|
388
392
|
const badge = (id, label, value) => (value ? { id, label, value } : { id, label });
|
|
389
393
|
return [
|
|
390
|
-
|
|
394
|
+
this.search.badge(),
|
|
391
395
|
badge('state', 's state', this.criteria.state ? `state: ${this.criteria.state}` : undefined),
|
|
392
396
|
// Label, never the uuid (GAIA-299 D3) — including when mine set who.
|
|
393
397
|
badge('who', 'w who', this.criteria.who ? `who: ${this.criteria.who.label}` : undefined),
|
|
@@ -416,9 +420,7 @@ export class TicketsScreen extends ListScreen {
|
|
|
416
420
|
.filter(Boolean)
|
|
417
421
|
.join(' · ');
|
|
418
422
|
const hidden = this.hiddenNotice();
|
|
419
|
-
const searchOpen = this.
|
|
420
|
-
? 'searching IDENTIFIER + TITLE · type to filter · Backspace erase · Enter keep · Esc leave'
|
|
421
|
-
: '';
|
|
423
|
+
const searchOpen = this.search.hint();
|
|
422
424
|
return {
|
|
423
425
|
selectable: this.rows.length,
|
|
424
426
|
selectedIndex: this.selectedIndex,
|
|
@@ -435,7 +437,7 @@ export class TicketsScreen extends ListScreen {
|
|
|
435
437
|
];
|
|
436
438
|
}
|
|
437
439
|
notice() {
|
|
438
|
-
return this.status || undefined;
|
|
440
|
+
return this.search.status || this.status || undefined;
|
|
439
441
|
}
|
|
440
442
|
activate(index, context) {
|
|
441
443
|
const row = this.readyValue?.rows[index];
|
|
@@ -456,67 +458,8 @@ export class TicketsScreen extends ListScreen {
|
|
|
456
458
|
return true;
|
|
457
459
|
if (this.openingEdit)
|
|
458
460
|
return true;
|
|
459
|
-
if (this.
|
|
460
|
-
if (input === 'escape' || input === 'enter') {
|
|
461
|
-
this.searchMode = false;
|
|
462
|
-
context.requestDraw();
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
if (input === 'backspace')
|
|
466
|
-
this.criteria = {
|
|
467
|
-
...this.criteria,
|
|
468
|
-
query: this.criteria.query.slice(0, -1),
|
|
469
|
-
};
|
|
470
|
-
else if (input.length === 1 && input >= ' ')
|
|
471
|
-
this.criteria = {
|
|
472
|
-
...this.criteria,
|
|
473
|
-
query: this.criteria.query + input,
|
|
474
|
-
};
|
|
475
|
-
else
|
|
476
|
-
return true;
|
|
477
|
-
const query = this.criteria.query.toLocaleLowerCase();
|
|
478
|
-
const narrowed = this.searchBase.filter((row) => row.identifier.toLocaleLowerCase().includes(query) ||
|
|
479
|
-
row.title.toLocaleLowerCase().includes(query));
|
|
480
|
-
// Optimistic local narrowing until the debounced reload lands; it keeps
|
|
481
|
-
// the page it is narrowing — the LOADED one, the page these rows came
|
|
482
|
-
// from — so neither `hasNext` nor the page number can flip on a keystroke.
|
|
483
|
-
this.replaceReady({
|
|
484
|
-
hasNext: this.readyValue?.hasNext ?? false,
|
|
485
|
-
page: this.loadedPage,
|
|
486
|
-
// The criteria just changed, so the previous collection's total must not
|
|
487
|
-
// stand beside these narrowed rows (AC 8.3). Nothing has been established
|
|
488
|
-
// for the new collection yet, so the line states the explicit unknown
|
|
489
|
-
// (`N of ?`, D6d) until the debounced reload's page answers with its own
|
|
490
|
-
// `meta.count`.
|
|
491
|
-
total: null,
|
|
492
|
-
// Same rule as `ListScreen.load()` (GAIA-312 D4): a narrowing that
|
|
493
|
-
// dropped rows scatters the survivors through the window, so the window
|
|
494
|
-
// stops positioning them; one that dropped nothing keeps it.
|
|
495
|
-
window: narrowed.length === this.searchBase.length
|
|
496
|
-
? (this.readyValue?.window ?? null)
|
|
497
|
-
: null,
|
|
498
|
-
rows: narrowed,
|
|
499
|
-
});
|
|
500
|
-
this.cancelSearch?.();
|
|
501
|
-
const debounce = context.options?.debounce ?? timerDebounce;
|
|
502
|
-
this.cancelSearch = debounce(() => {
|
|
503
|
-
// The search is a criteria change like any other (D5).
|
|
504
|
-
this.page = 0;
|
|
505
|
-
const guard = this.captureLifecycle(context, true);
|
|
506
|
-
void this.load(context).then((rows) => {
|
|
507
|
-
if (this.replaceReadyIfCurrent(guard, rows, context))
|
|
508
|
-
this.requestActiveDraw(context);
|
|
509
|
-
});
|
|
510
|
-
}, 200);
|
|
511
|
-
context.requestDraw();
|
|
512
|
-
return true;
|
|
513
|
-
}
|
|
514
|
-
if (input === '/') {
|
|
515
|
-
this.searchMode = true;
|
|
516
|
-
this.searchBase = [...this.rows];
|
|
517
|
-
context.requestDraw();
|
|
461
|
+
if (this.search.handleKey(input, context))
|
|
518
462
|
return true;
|
|
519
|
-
}
|
|
520
463
|
if (input === 'e') {
|
|
521
464
|
const ticketId = this.rows[this.selectedIndex]?.id;
|
|
522
465
|
if (!ticketId || this.openingEdit || !context.services)
|
|
@@ -543,7 +486,7 @@ export class TicketsScreen extends ListScreen {
|
|
|
543
486
|
return true;
|
|
544
487
|
}
|
|
545
488
|
// The paging arrows, dispatched HERE and not by the base (GAIA-294 D3). The
|
|
546
|
-
// position is the load-bearing part: they are reached only PAST the
|
|
489
|
+
// position is the load-bearing part: they are reached only PAST the search
|
|
547
490
|
// branch above, which consumes them, so while the `/` field is open the arrows
|
|
548
491
|
// stay inert — not appended as text (its text branch takes single characters
|
|
549
492
|
// only) and not paging underneath the field. A base that claimed them first
|
|
@@ -555,7 +498,7 @@ export class TicketsScreen extends ListScreen {
|
|
|
555
498
|
if (this.handlePagingInput(input, context))
|
|
556
499
|
return true;
|
|
557
500
|
// GAIA-326 — the copy keys, and their POSITION is the whole reason `y` is
|
|
558
|
-
// safe: they are reached only past the
|
|
501
|
+
// safe: they are reached only past the search branch above, which
|
|
559
502
|
// consumes every printable character, so `y` typed into the `/` field is a
|
|
560
503
|
// letter and never a copy. Nothing else in `src/` binds `y` or `Y`.
|
|
561
504
|
if (input === 'y' || input === 'Y') {
|
|
@@ -632,10 +575,7 @@ export class TicketsScreen extends ListScreen {
|
|
|
632
575
|
this.mine = undefined;
|
|
633
576
|
this.openingEdit = false;
|
|
634
577
|
this.status = '';
|
|
635
|
-
this.
|
|
636
|
-
this.cancelSearch?.();
|
|
637
|
-
this.cancelSearch = undefined;
|
|
638
|
-
this.searchBase = [];
|
|
578
|
+
this.search.reset();
|
|
639
579
|
this.hiddenByClosed = null;
|
|
640
580
|
super.reset();
|
|
641
581
|
}
|
|
@@ -69,16 +69,23 @@ export declare abstract class MemberTicketsScreen extends ListScreen<TicketRow,
|
|
|
69
69
|
readonly emptyMessage: "No tickets.";
|
|
70
70
|
};
|
|
71
71
|
private criteria;
|
|
72
|
+
private readonly search;
|
|
73
|
+
get searchQuery(): string;
|
|
74
|
+
set searchQuery(query: string);
|
|
75
|
+
reloadSearch(context: ScreenContext): Promise<boolean>;
|
|
72
76
|
protected tableContext(): void;
|
|
73
77
|
protected abstract loadRows(context: ScreenContext, page: number): Promise<TicketPage>;
|
|
74
78
|
protected ticketCriteria(): TicketCriteria;
|
|
75
79
|
protected filterRow(row: TicketRow): boolean;
|
|
76
80
|
protected renderLoading(): string[];
|
|
77
81
|
protected badges(): readonly ListFilterBadge[];
|
|
82
|
+
protected notice(): string | undefined;
|
|
78
83
|
protected chrome(context: ScreenContext): FullScreenChrome;
|
|
79
84
|
protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
|
|
80
85
|
activate(index: number, context: ScreenContext): boolean;
|
|
81
86
|
handleInput(input: string, context: ScreenContext): boolean;
|
|
87
|
+
leave(): void;
|
|
88
|
+
reset(): void;
|
|
82
89
|
}
|
|
83
90
|
export declare class WorkspaceMemberTicketsScreen extends MemberTicketsScreen {
|
|
84
91
|
static readonly route: {
|
|
@@ -4,11 +4,24 @@ import { CLOSED_FILTER_VALUES, cycleValue } from '../../cycle.js';
|
|
|
4
4
|
import { assertActiveMember } from '../../user/Data/member-context.js';
|
|
5
5
|
import { DEFAULT_TICKET_CRITERIA } from '../Data/query.js';
|
|
6
6
|
import { loadUserTickets, TICKET_TABLE, } from '../Data/tickets.js';
|
|
7
|
+
import { TicketSearch } from './ticket-search.js';
|
|
7
8
|
/** Shared member ticket list behavior; subclasses own scope and loading. */
|
|
8
9
|
export class MemberTicketsScreen extends ListScreen {
|
|
9
10
|
frameId = 'user-tickets';
|
|
10
11
|
table = TICKET_TABLE;
|
|
11
12
|
criteria = { ...DEFAULT_TICKET_CRITERIA };
|
|
13
|
+
search = new TicketSearch(this);
|
|
14
|
+
get searchQuery() {
|
|
15
|
+
return this.criteria.query;
|
|
16
|
+
}
|
|
17
|
+
set searchQuery(query) {
|
|
18
|
+
this.criteria = { ...this.criteria, query };
|
|
19
|
+
}
|
|
20
|
+
reloadSearch(context) {
|
|
21
|
+
this.page = 0;
|
|
22
|
+
this.selectedIndex = 0;
|
|
23
|
+
return this.reloadInPlace(context);
|
|
24
|
+
}
|
|
12
25
|
tableContext() { }
|
|
13
26
|
ticketCriteria() {
|
|
14
27
|
return this.criteria;
|
|
@@ -21,9 +34,10 @@ export class MemberTicketsScreen extends ListScreen {
|
|
|
21
34
|
}
|
|
22
35
|
badges() {
|
|
23
36
|
if (this.criteria.closed === null) {
|
|
24
|
-
return [{ id: 'closed', label: 'x closed' }];
|
|
37
|
+
return [this.search.badge(), { id: 'closed', label: 'x closed' }];
|
|
25
38
|
}
|
|
26
39
|
return [
|
|
40
|
+
this.search.badge(),
|
|
27
41
|
{
|
|
28
42
|
id: 'closed',
|
|
29
43
|
label: 'x closed',
|
|
@@ -31,6 +45,9 @@ export class MemberTicketsScreen extends ListScreen {
|
|
|
31
45
|
},
|
|
32
46
|
];
|
|
33
47
|
}
|
|
48
|
+
notice() {
|
|
49
|
+
return this.search.status || undefined;
|
|
50
|
+
}
|
|
34
51
|
chrome(context) {
|
|
35
52
|
const name = context.user?.name ?? context.params?.userName ?? 'user';
|
|
36
53
|
const pagingLine = [this.countLabel(), ...this.pagingSegments()]
|
|
@@ -41,7 +58,7 @@ export class MemberTicketsScreen extends ListScreen {
|
|
|
41
58
|
selectedIndex: this.selectedIndex,
|
|
42
59
|
rowIds: this.rowNodeIds,
|
|
43
60
|
title: `${name} · tickets`,
|
|
44
|
-
meta: pagingLine,
|
|
61
|
+
meta: [pagingLine, this.search.hint()].filter(Boolean).join(' · '),
|
|
45
62
|
};
|
|
46
63
|
}
|
|
47
64
|
actions() {
|
|
@@ -59,6 +76,8 @@ export class MemberTicketsScreen extends ListScreen {
|
|
|
59
76
|
return true;
|
|
60
77
|
}
|
|
61
78
|
handleInput(input, context) {
|
|
79
|
+
if (this.search.handleKey(input, context))
|
|
80
|
+
return true;
|
|
62
81
|
if (input === 'x') {
|
|
63
82
|
this.criteria = {
|
|
64
83
|
...this.criteria,
|
|
@@ -76,6 +95,15 @@ export class MemberTicketsScreen extends ListScreen {
|
|
|
76
95
|
return this.activate(this.selectedIndex, context);
|
|
77
96
|
return super.handleInput(input, context);
|
|
78
97
|
}
|
|
98
|
+
leave() {
|
|
99
|
+
this.search.close();
|
|
100
|
+
super.leave();
|
|
101
|
+
}
|
|
102
|
+
reset() {
|
|
103
|
+
this.criteria = { ...this.criteria, query: '' };
|
|
104
|
+
this.search.reset();
|
|
105
|
+
super.reset();
|
|
106
|
+
}
|
|
79
107
|
}
|
|
80
108
|
export class WorkspaceMemberTicketsScreen extends MemberTicketsScreen {
|
|
81
109
|
static route = {
|
|
@@ -80,6 +80,13 @@ export declare abstract class AsyncScreen<T> extends BaseScreen {
|
|
|
80
80
|
*/
|
|
81
81
|
protected actionStillCurrent(guard: LifecycleGuard, context: ScreenContext): boolean;
|
|
82
82
|
protected replaceReadyIfCurrent(guard: LifecycleGuard, data: T, context: ScreenContext): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Re-read over the ready surface, superseding any read still in flight.
|
|
85
|
+
* Resolves `false` when a newer read or a local mutation overtook this one;
|
|
86
|
+
* rejects only while it is still the current read, so a stale failure
|
|
87
|
+
* cannot overwrite a newer answer.
|
|
88
|
+
*/
|
|
89
|
+
protected reloadInPlace(context: ScreenContext): Promise<boolean>;
|
|
83
90
|
protected requestActiveDraw(context: ScreenContext): boolean;
|
|
84
91
|
enter(context: ScreenContext): void;
|
|
85
92
|
retry(context: ScreenContext): Promise<void>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { filterBadgeStrip, } from '../Component/list-chrome/index.js';
|
|
2
|
+
import { serverMessage } from '../lib/jsonapi-error.js';
|
|
2
3
|
import { columnNode, messageTree } from '../lib/vnode.js';
|
|
3
4
|
import { BaseScreen, presentation, } from './screen.js';
|
|
4
5
|
/**
|
|
@@ -103,6 +104,28 @@ export class AsyncScreen extends BaseScreen {
|
|
|
103
104
|
this.state = 'ready';
|
|
104
105
|
return true;
|
|
105
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Re-read over the ready surface, superseding any read still in flight.
|
|
109
|
+
* Resolves `false` when a newer read or a local mutation overtook this one;
|
|
110
|
+
* rejects only while it is still the current read, so a stale failure
|
|
111
|
+
* cannot overwrite a newer answer.
|
|
112
|
+
*/
|
|
113
|
+
async reloadInPlace(context) {
|
|
114
|
+
const guard = this.captureLifecycle(context, true);
|
|
115
|
+
let data;
|
|
116
|
+
try {
|
|
117
|
+
data = await this.load(context);
|
|
118
|
+
}
|
|
119
|
+
catch (caught) {
|
|
120
|
+
if (!this.lifecycleIsCurrent(guard, context))
|
|
121
|
+
return false;
|
|
122
|
+
throw caught;
|
|
123
|
+
}
|
|
124
|
+
if (!this.replaceReadyIfCurrent(guard, data, context))
|
|
125
|
+
return false;
|
|
126
|
+
this.requestActiveDraw(context);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
106
129
|
requestActiveDraw(context) {
|
|
107
130
|
if (!context.isActive()) {
|
|
108
131
|
this.staleRedrawCount++;
|
|
@@ -167,13 +190,11 @@ export class AsyncScreen extends BaseScreen {
|
|
|
167
190
|
return;
|
|
168
191
|
this.refreshing = false;
|
|
169
192
|
if (keepSurface) {
|
|
170
|
-
this.toast =
|
|
171
|
-
caught instanceof Error ? caught.message : String(caught);
|
|
193
|
+
this.toast = serverMessage(caught);
|
|
172
194
|
this.requestActiveDraw(context);
|
|
173
195
|
return;
|
|
174
196
|
}
|
|
175
|
-
this.failure =
|
|
176
|
-
caught instanceof Error ? caught : new Error(String(caught));
|
|
197
|
+
this.failure = new Error(serverMessage(caught));
|
|
177
198
|
this.state = 'error';
|
|
178
199
|
this.requestActiveDraw(context);
|
|
179
200
|
});
|
|
@@ -14,7 +14,7 @@ import type { ScreenContext } from './screen.js';
|
|
|
14
14
|
* enough hooks to be a framework.
|
|
15
15
|
*
|
|
16
16
|
* It does not claim keys from `handleInput` either. The current dispatch ORDER is
|
|
17
|
-
* load-bearing: `←`/`→` sit after the ticket list's
|
|
17
|
+
* load-bearing: `←`/`→` sit after the ticket list's `/` search branch, where the
|
|
18
18
|
* arrows are inert while the `/` field is open. A base that claimed them first
|
|
19
19
|
* would make them page there — a behaviour change, and GAIA-294 is
|
|
20
20
|
* behaviour-neutral. So the dispatch TABLE lives here once and each subclass calls
|
|
@@ -93,7 +93,7 @@ export declare abstract class ListScreen<Row extends {
|
|
|
93
93
|
* `←` forwards too (GAIA-292 R1).
|
|
94
94
|
*
|
|
95
95
|
* WHERE it is called stays the subclass's decision (GAIA-294 D3) — on the ticket
|
|
96
|
-
* list it must sit after the
|
|
96
|
+
* list it must sit after the `/` search branch, where the arrows are inert
|
|
97
97
|
* while the `/` field is open.
|
|
98
98
|
*/
|
|
99
99
|
protected handlePagingInput(input: string, context: ScreenContext): boolean;
|
|
@@ -103,6 +103,15 @@ export declare abstract class ListScreen<Row extends {
|
|
|
103
103
|
* chain continues.
|
|
104
104
|
*/
|
|
105
105
|
protected handleCursorInput(input: string, context: ScreenContext): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Show `rows` in place of the loaded page until the next read answers — the
|
|
108
|
+
* optimistic narrowing of a search. It keeps the LOADED page number so a
|
|
109
|
+
* keystroke does not flip `page N`; the total becomes the explicit unknown
|
|
110
|
+
* (`N of ?`); a narrowing that dropped rows stops the window positioning
|
|
111
|
+
* the survivors (GAIA-312 D4). An emptied list (a refusal) also drops
|
|
112
|
+
* `hasNext`, so `→` cannot re-send the refused query.
|
|
113
|
+
*/
|
|
114
|
+
showRows(rows: readonly Row[]): void;
|
|
106
115
|
/** `page N` for the strip above the table (count sits beside it). */
|
|
107
116
|
protected pagingSegments(): string[];
|
|
108
117
|
/**
|
|
@@ -24,7 +24,7 @@ import { AsyncScreen } from './async-screen.js';
|
|
|
24
24
|
* enough hooks to be a framework.
|
|
25
25
|
*
|
|
26
26
|
* It does not claim keys from `handleInput` either. The current dispatch ORDER is
|
|
27
|
-
* load-bearing: `←`/`→` sit after the ticket list's
|
|
27
|
+
* load-bearing: `←`/`→` sit after the ticket list's `/` search branch, where the
|
|
28
28
|
* arrows are inert while the `/` field is open. A base that claimed them first
|
|
29
29
|
* would make them page there — a behaviour change, and GAIA-294 is
|
|
30
30
|
* behaviour-neutral. So the dispatch TABLE lives here once and each subclass calls
|
|
@@ -92,7 +92,7 @@ export class ListScreen extends AsyncScreen {
|
|
|
92
92
|
* `←` forwards too (GAIA-292 R1).
|
|
93
93
|
*
|
|
94
94
|
* WHERE it is called stays the subclass's decision (GAIA-294 D3) — on the ticket
|
|
95
|
-
* list it must sit after the
|
|
95
|
+
* list it must sit after the `/` search branch, where the arrows are inert
|
|
96
96
|
* while the `/` field is open.
|
|
97
97
|
*/
|
|
98
98
|
handlePagingInput(input, context) {
|
|
@@ -116,6 +116,24 @@ export class ListScreen extends AsyncScreen {
|
|
|
116
116
|
context.requestDraw();
|
|
117
117
|
return true;
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Show `rows` in place of the loaded page until the next read answers — the
|
|
121
|
+
* optimistic narrowing of a search. It keeps the LOADED page number so a
|
|
122
|
+
* keystroke does not flip `page N`; the total becomes the explicit unknown
|
|
123
|
+
* (`N of ?`); a narrowing that dropped rows stops the window positioning
|
|
124
|
+
* the survivors (GAIA-312 D4). An emptied list (a refusal) also drops
|
|
125
|
+
* `hasNext`, so `→` cannot re-send the refused query.
|
|
126
|
+
*/
|
|
127
|
+
showRows(rows) {
|
|
128
|
+
const current = this.readyValue;
|
|
129
|
+
this.replaceReady({
|
|
130
|
+
hasNext: rows.length === 0 ? false : (current?.hasNext ?? false),
|
|
131
|
+
page: this.loadedPage,
|
|
132
|
+
total: null,
|
|
133
|
+
window: current && rows.length === current.rows.length ? current.window : null,
|
|
134
|
+
rows: [...rows],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
119
137
|
/** `page N` for the strip above the table (count sits beside it). */
|
|
120
138
|
pagingSegments() {
|
|
121
139
|
return [`page ${this.loadedPage + 1}`];
|