@gaia-ai/addon-gaia-ui 0.7.0 → 0.8.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/Agent/launch.d.ts +4 -4
- package/dist/src/Agent/launch.js +14 -7
- package/dist/src/Component/stats-panel/index.d.ts +13 -2
- package/dist/src/Component/stats-panel/index.js +28 -18
- package/dist/src/Component/top-tabs/index.d.ts +6 -0
- package/dist/src/Component/top-tabs/index.js +15 -6
- package/dist/src/Entity/collection.d.ts +98 -0
- package/dist/src/Entity/collection.js +46 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.d.ts +25 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.js +60 -0
- package/dist/src/Entity/conductor/Data/conductors.d.ts +57 -0
- package/dist/src/Entity/conductor/Data/conductors.js +67 -0
- package/dist/src/Entity/conductor/Data/liveness.d.ts +2 -1
- package/dist/src/Entity/conductor/Data/liveness.js +12 -16
- package/dist/src/Entity/conductor/Data/log-tail.d.ts +25 -0
- package/dist/src/Entity/conductor/Data/log-tail.js +133 -0
- package/dist/src/Entity/conductor/Data/own-conductor.d.ts +30 -0
- package/dist/src/Entity/conductor/Data/own-conductor.js +73 -0
- package/dist/src/Entity/conductor/Data/process.d.ts +78 -0
- package/dist/src/Entity/conductor/Data/process.js +101 -0
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +42 -0
- package/dist/src/Entity/conductor/Screen/conductors.js +200 -0
- package/dist/src/Entity/cycle.d.ts +18 -0
- package/dist/src/Entity/cycle.js +30 -0
- package/dist/src/Entity/project/Data/project.d.ts +8 -0
- package/dist/src/Entity/project/Data/project.js +12 -0
- package/dist/src/Entity/project/Data/projects.d.ts +3 -0
- package/dist/src/Entity/project/Data/projects.js +3 -0
- package/dist/src/Entity/project/Data/with-tickets.d.ts +5 -0
- package/dist/src/Entity/project/Data/with-tickets.js +15 -13
- package/dist/src/Entity/project/Screen/project-picker.d.ts +36 -0
- package/dist/src/Entity/project/Screen/project-picker.js +90 -0
- package/dist/src/Entity/project/Screen/projects-screen.d.ts +40 -0
- package/dist/src/Entity/project/Screen/projects-screen.js +103 -0
- package/dist/src/Entity/run/Component/run-table/index.d.ts +5 -7
- package/dist/src/Entity/run/Component/run-table/index.js +9 -18
- package/dist/src/Entity/run/Data/query.d.ts +22 -0
- package/dist/src/Entity/run/Data/query.js +41 -0
- package/dist/src/Entity/run/Data/runs.d.ts +85 -4
- package/dist/src/Entity/run/Data/runs.js +143 -27
- package/dist/src/Entity/run/Screen/run-detail.d.ts +25 -0
- package/dist/src/Entity/run/Screen/run-detail.js +51 -0
- package/dist/src/Entity/run/Screen/runs.d.ts +82 -0
- package/dist/src/Entity/run/Screen/runs.js +169 -0
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +42 -0
- package/dist/src/Entity/statistics/Screen/dashboard.js +177 -0
- package/dist/src/Entity/ticket/Component/create-form/index.d.ts +10 -0
- package/dist/src/Entity/ticket/Component/create-form/index.js +4 -0
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +3 -2
- package/dist/src/Entity/ticket/Component/edit-form/index.js +30 -8
- package/dist/src/Entity/ticket/Component/tab-bar/index.js +6 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +1 -1
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +33 -2
- package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +4 -2
- package/dist/src/Entity/ticket/Component/ticket-table/index.js +8 -12
- package/dist/src/Entity/ticket/Data/parents.d.ts +13 -1
- package/dist/src/Entity/ticket/Data/parents.js +64 -12
- package/dist/src/Entity/ticket/Data/query.d.ts +44 -7
- package/dist/src/Entity/ticket/Data/query.js +60 -26
- package/dist/src/Entity/ticket/Data/tickets.d.ts +105 -4
- package/dist/src/Entity/ticket/Data/tickets.js +164 -12
- package/dist/src/Entity/ticket/Data/workflow.d.ts +1 -5
- package/dist/src/Entity/ticket/Data/workflow.js +5 -13
- package/dist/src/Entity/ticket/Data/write.d.ts +32 -0
- package/dist/src/Entity/ticket/Data/write.js +80 -0
- package/dist/src/Entity/ticket/Form/create-data.d.ts +1 -11
- package/dist/src/Entity/ticket/Form/create-data.js +7 -36
- package/dist/src/Entity/ticket/Form/create.d.ts +45 -5
- package/dist/src/Entity/ticket/Form/create.js +45 -8
- package/dist/src/Entity/ticket/Form/edit-data.d.ts +1 -1
- package/dist/src/Entity/ticket/Form/edit-data.js +4 -0
- package/dist/src/Entity/ticket/Form/edit.d.ts +79 -12
- package/dist/src/Entity/ticket/Form/edit.js +165 -18
- package/dist/src/Entity/ticket/Form/keys.d.ts +7 -0
- package/dist/src/Entity/ticket/Form/keys.js +7 -0
- package/dist/src/Entity/ticket/Form/parent-filter.d.ts +36 -0
- package/dist/src/Entity/ticket/Form/parent-filter.js +37 -0
- package/dist/src/Entity/ticket/Form/parent-source.d.ts +27 -0
- package/dist/src/Entity/ticket/Form/parent-source.js +56 -0
- package/dist/src/Entity/ticket/Form/who-filter.d.ts +33 -0
- package/dist/src/Entity/ticket/Form/who-filter.js +33 -0
- package/dist/src/Entity/ticket/Form/who-source.d.ts +18 -0
- package/dist/src/Entity/ticket/Form/who-source.js +38 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +68 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +336 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +81 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +200 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +27 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.js +177 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +45 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.js +135 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +61 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +300 -0
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +129 -0
- package/dist/src/Entity/ticket/Screen/tickets.js +507 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +27 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.js +154 -0
- package/dist/src/Entity/user/Data/users.d.ts +17 -0
- package/dist/src/Entity/user/Data/users.js +66 -6
- package/dist/src/Form/field.d.ts +3 -1
- package/dist/src/Form/field.js +1 -0
- package/dist/src/Form/form.d.ts +1 -1
- package/dist/src/Form/form.js +61 -9
- package/dist/src/Form/keys.d.ts +5 -1
- package/dist/src/Form/keys.js +38 -5
- package/dist/src/Kernel/tui-kernel.d.ts +0 -10
- package/dist/src/Kernel/tui-kernel.js +250 -2384
- package/dist/src/Routing/context.d.ts +31 -0
- package/dist/src/Routing/context.js +52 -0
- package/dist/src/Routing/registry.d.ts +19 -0
- package/dist/src/Routing/registry.js +97 -0
- package/dist/src/Routing/route.d.ts +39 -0
- package/dist/src/Routing/route.js +1 -0
- package/dist/src/Routing/router.d.ts +33 -0
- package/dist/src/Routing/router.js +102 -0
- package/dist/src/Routing/routes.d.ts +17 -0
- package/dist/src/Routing/routes.js +57 -0
- package/dist/src/Screen/async-screen.d.ts +50 -0
- package/dist/src/Screen/async-screen.js +164 -0
- package/dist/src/Screen/list-screen.d.ts +158 -0
- package/dist/src/Screen/list-screen.js +237 -0
- package/dist/src/Screen/screen.d.ts +106 -0
- package/dist/src/Screen/screen.js +18 -0
- package/dist/src/Tui/controller.d.ts +13 -0
- package/dist/src/Tui/controller.js +1794 -0
- package/dist/src/Widget/text.js +49 -4
- package/dist/src/Widget/types.d.ts +2 -1
- package/dist/src/launcher.d.ts +9 -2
- package/dist/src/launcher.js +2 -1
- package/dist/src/lib/ansi.js +13 -5
- package/dist/src/lib/cursor.d.ts +31 -0
- package/dist/src/lib/cursor.js +45 -0
- package/dist/src/lib/format.d.ts +21 -0
- package/dist/src/lib/format.js +59 -0
- package/dist/src/lib/table.d.ts +37 -0
- package/dist/src/lib/table.js +20 -0
- package/dist/src/lib/width.d.ts +33 -1
- package/dist/src/lib/width.js +107 -12
- package/dist/src/plugin.js +19 -0
- package/dist/src/types.d.ts +26 -0
- package/dist/src/types.js +0 -4
- package/package.json +2 -2
|
@@ -19,20 +19,15 @@ import { primeSources, } from '../../../Widget/index.js';
|
|
|
19
19
|
import { listConductors } from '../../conductor/Data/conductors.js';
|
|
20
20
|
import { listTerms } from '../../term/Data/terms.js';
|
|
21
21
|
import { listUsers } from '../../user/Data/users.js';
|
|
22
|
-
import { loadParentCandidates } from '../Data/parents.js';
|
|
23
22
|
import { WORKFLOW_OPTIONS, } from './create.js';
|
|
23
|
+
import { parentOptionSource } from './parent-source.js';
|
|
24
24
|
// One back-compat re-export, no implementation: `loadEditFormOptions` lives in
|
|
25
25
|
// `./edit-form-data.js`, because a module named for the create form owning the EDIT
|
|
26
26
|
// form's loader is the category error AC-6 is about.
|
|
27
27
|
export { loadEditFormOptions, } from './edit-data.js';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* A property of THIS field's source, not of the kernel: the kernel now drives
|
|
32
|
-
* whatever sources a form declares and has no interest in how long any of them
|
|
33
|
-
* waits.
|
|
34
|
-
*/
|
|
35
|
-
const PARENT_QUERY_DEBOUNCE_MS = 150;
|
|
28
|
+
// The parent label policy moved to `parent-source.ts` with the source (GAIA-284
|
|
29
|
+
// D5); re-exported so the existing import site keeps working unchanged.
|
|
30
|
+
export { parentLabel } from './parent-source.js';
|
|
36
31
|
/**
|
|
37
32
|
* Stable-partition the machine's own row to the front: index 0 is what a ring
|
|
38
33
|
* defaults to. This popup's only ordering rule, and it reads the same for users and
|
|
@@ -52,22 +47,6 @@ const toOption = (row) => ({ id: row.id, label: row.label });
|
|
|
52
47
|
* type's vocabularies a given widget wants is the widget's business.
|
|
53
48
|
*/
|
|
54
49
|
const LABEL_VOCABULARY = 'gaia_labels';
|
|
55
|
-
/**
|
|
56
|
-
* `GAIA-nnn · <title>` — the picker label, composed HERE (GAIA-257 D5).
|
|
57
|
-
*
|
|
58
|
-
* The kernel filters candidates by a case-insensitive substring of
|
|
59
|
-
* `Option.label` and knows nothing of identifiers or titles. AC-5's "matching
|
|
60
|
-
* on identifier AND on title" is therefore met by what the label is MADE OF,
|
|
61
|
-
* not by a second rule in the kernel: a substring of this string is a match on
|
|
62
|
-
* either half. Exported so the popup's own tests can state the rule once.
|
|
63
|
-
*/
|
|
64
|
-
export const parentLabel = (row) => `${row.identifier} · ${row.title}`;
|
|
65
|
-
const toParentOption = (row) => ({
|
|
66
|
-
id: row.id,
|
|
67
|
-
label: parentLabel(row),
|
|
68
|
-
identifier: row.identifier,
|
|
69
|
-
title: row.title,
|
|
70
|
-
});
|
|
71
50
|
/**
|
|
72
51
|
* The option sources this popup's fields load through, keyed by FIELD id.
|
|
73
52
|
*
|
|
@@ -127,17 +106,9 @@ export function createFormSources(client, hints = {}) {
|
|
|
127
106
|
// "nothing can be offered" and "searched, nothing matched" are different
|
|
128
107
|
// things and the field says different things about them (D8).
|
|
129
108
|
if (projectId !== undefined) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
async load(query) {
|
|
134
|
-
const outcome = await loadParentCandidates(client, projectId, query);
|
|
135
|
-
return {
|
|
136
|
-
options: outcome.rows.map(toParentOption),
|
|
137
|
-
error: outcome.error,
|
|
138
|
-
};
|
|
139
|
-
},
|
|
140
|
-
};
|
|
109
|
+
// The shared parent source (GAIA-284 D5): the same read, label policy and
|
|
110
|
+
// error channel the parent-filter picker composes.
|
|
111
|
+
sources.parent = parentOptionSource(client, projectId);
|
|
141
112
|
}
|
|
142
113
|
return sources;
|
|
143
114
|
}
|
|
@@ -57,9 +57,27 @@ export { WORKFLOW_OPTIONS } from '../Data/workflow.js';
|
|
|
57
57
|
export type FieldDef<Id extends string = string> = Omit<FieldDecl, 'id'> & {
|
|
58
58
|
id: Id;
|
|
59
59
|
};
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The two action rows' field ids (GAIA-309 D3/D7).
|
|
62
|
+
*
|
|
63
|
+
* Exported because the SCREEN branches on them: the kernel's one `submit`
|
|
64
|
+
* intent cannot say which of two actions was fired, so the screen resolves the
|
|
65
|
+
* active field and compares. `'submit'` keeps its name for the agent row so
|
|
66
|
+
* every pre-GAIA-309 assertion about it stays literally true.
|
|
67
|
+
*/
|
|
68
|
+
export declare const AGENT_ACTION_ID = "submit";
|
|
69
|
+
export declare const DIRECT_ACTION_ID = "direct";
|
|
70
|
+
export type CreateFieldId = 'title' | 'description' | 'parent' | 'assignee' | 'conductor' | 'labels' | 'workflow' | 'submit' | 'direct';
|
|
61
71
|
/** What the form HOLDS. `active`/`cursor` are the kernel's and must not appear. */
|
|
62
|
-
interface CreateValues {
|
|
72
|
+
export interface CreateValues {
|
|
73
|
+
/**
|
|
74
|
+
* The ticket's title. `gaia_ticket.title` is `setRequired(TRUE)` server-side,
|
|
75
|
+
* so a direct create without one is rejected — this was never optional
|
|
76
|
+
* (GAIA-309 D1). Deriving it from the description's first line was considered
|
|
77
|
+
* and rejected: the operator could not correct a bad title without rewriting
|
|
78
|
+
* their own opening sentence.
|
|
79
|
+
*/
|
|
80
|
+
title: string;
|
|
63
81
|
description: string;
|
|
64
82
|
/** The PICKED parent's uuid, or null. `parentQuery` holds what was typed —
|
|
65
83
|
* an autocomplete field owns two keys (GAIA-257 D5). */
|
|
@@ -78,6 +96,8 @@ export interface ParentRef {
|
|
|
78
96
|
}
|
|
79
97
|
/** The submitted payload (resolved options), handed to the create agent. */
|
|
80
98
|
export interface CreateFormPayload {
|
|
99
|
+
/** What the operator NAMED the ticket. Required server-side (GAIA-309 D1). */
|
|
100
|
+
title: string;
|
|
81
101
|
description: string;
|
|
82
102
|
workflow: Option | null;
|
|
83
103
|
assignee: Option | null;
|
|
@@ -93,8 +113,9 @@ export interface CreateFormPayload {
|
|
|
93
113
|
/** The option with `id`, or null — used by both forms' `toObject`. */
|
|
94
114
|
export declare const optionFor: (list: Option[], id: string | null) => Option | null;
|
|
95
115
|
/**
|
|
96
|
-
* The declaration. No field carries a function
|
|
97
|
-
*
|
|
116
|
+
* The declaration. No field carries a function. Exactly ONE select is
|
|
117
|
+
* `clearable` — the conductor (GAIA-309 D2); every other one cycles over real
|
|
118
|
+
* options only, exactly as before GAIA-233.
|
|
98
119
|
*/
|
|
99
120
|
export declare const CREATE_FORM: {
|
|
100
121
|
fields: FieldDecl[];
|
|
@@ -107,7 +128,26 @@ export declare const CREATE_FORM: {
|
|
|
107
128
|
/** Field order in the popup (top to bottom). */
|
|
108
129
|
export declare const FIELDS: FieldDef<CreateFieldId>[];
|
|
109
130
|
export type CreateFormState = FormState<CreateValues>;
|
|
110
|
-
|
|
131
|
+
/**
|
|
132
|
+
* The values the popup carries from one open to the next (GAIA-314 D2).
|
|
133
|
+
*
|
|
134
|
+
* The popup is ONE screen instance, entered again and again (spec F1/F2), so
|
|
135
|
+
* something has to say which of its values are the ticket being written and
|
|
136
|
+
* which are the operator's standing routing choice. That answer belongs beside
|
|
137
|
+
* the field declaration, not in the screen: the `Pick` ties the list to the
|
|
138
|
+
* value shape, so renaming a field breaks the build instead of silently
|
|
139
|
+
* dropping it out of the memory.
|
|
140
|
+
*
|
|
141
|
+
* `workflow` is NOT here, deliberately. A ticket type is a per-ticket decision,
|
|
142
|
+
* and a silently retained `gaia_bug` on the next feature ticket is a wrong
|
|
143
|
+
* value nobody looks at (D5). The parent contributes BOTH of its keys, because
|
|
144
|
+
* an autocomplete owns two and `apply` never produces one without the other
|
|
145
|
+
* (D4).
|
|
146
|
+
*/
|
|
147
|
+
export type CreateRemembered = Pick<CreateValues, 'parent' | 'parentQuery' | 'assignee' | 'conductor' | 'labels'>;
|
|
148
|
+
/** Snapshot the routing values of a form that is about to be replaced. */
|
|
149
|
+
export declare const rememberedValues: (s: CreateFormState) => CreateRemembered;
|
|
150
|
+
export declare const initialForm: (opts: CreateFormOptions, prefill?: Partial<CreateValues>) => CreateFormState;
|
|
111
151
|
export declare const activeField: (s: CreateFormState) => FieldDef<CreateFieldId>;
|
|
112
152
|
export declare const moveField: (s: CreateFormState, delta: number) => CreateFormState;
|
|
113
153
|
/** Append typed text to the description (only meaningful on the text field). */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The "New ticket" popup form (GAIA-190) as a DECLARATION over the form kernel
|
|
2
|
-
// (GAIA-236):
|
|
3
|
-
//
|
|
4
|
-
// multiselect cursor — all live in `Form/`; nothing of them
|
|
2
|
+
// (GAIA-236): seven fields plus two actions, and one `toObject` that yields the
|
|
3
|
+
// payload BOTH submit paths consume (GAIA-309). The mechanics — navigation, text
|
|
4
|
+
// editing, cycling, the multiselect cursor — all live in `Form/`; nothing of them
|
|
5
|
+
// is repeated here.
|
|
5
6
|
import { cycleValue, declareForm, } from '../../../Form/index.js';
|
|
6
7
|
import { PARENT_PAGE_SIZE } from '../Data/parents.js';
|
|
7
8
|
// `cycleValue` is re-exported because GAIA-233's contract cases pin it against
|
|
@@ -20,6 +21,16 @@ export { cycleValue };
|
|
|
20
21
|
// previously kept its own second copy (GAIA-236 phase 4). Re-exported so this form's
|
|
21
22
|
// existing import sites are unchanged.
|
|
22
23
|
export { WORKFLOW_OPTIONS } from '../Data/workflow.js';
|
|
24
|
+
/**
|
|
25
|
+
* The two action rows' field ids (GAIA-309 D3/D7).
|
|
26
|
+
*
|
|
27
|
+
* Exported because the SCREEN branches on them: the kernel's one `submit`
|
|
28
|
+
* intent cannot say which of two actions was fired, so the screen resolves the
|
|
29
|
+
* active field and compares. `'submit'` keeps its name for the agent row so
|
|
30
|
+
* every pre-GAIA-309 assertion about it stays literally true.
|
|
31
|
+
*/
|
|
32
|
+
export const AGENT_ACTION_ID = 'submit';
|
|
33
|
+
export const DIRECT_ACTION_ID = 'direct';
|
|
23
34
|
/** The option with `id`, or null — used by both forms' `toObject`. */
|
|
24
35
|
export const optionFor = (list, id) => list.find((o) => o.id === id) ?? null;
|
|
25
36
|
/**
|
|
@@ -37,11 +48,16 @@ const parentFor = (list, id) => {
|
|
|
37
48
|
: null;
|
|
38
49
|
};
|
|
39
50
|
/**
|
|
40
|
-
* The declaration. No field carries a function
|
|
41
|
-
*
|
|
51
|
+
* The declaration. No field carries a function. Exactly ONE select is
|
|
52
|
+
* `clearable` — the conductor (GAIA-309 D2); every other one cycles over real
|
|
53
|
+
* options only, exactly as before GAIA-233.
|
|
42
54
|
*/
|
|
43
55
|
export const CREATE_FORM = declareForm({
|
|
44
56
|
fields: (f) => [
|
|
57
|
+
// First, because it is the one field the server REQUIRES (D1). Both submit
|
|
58
|
+
// paths read it: the direct write sends it, the agent adopts it from the
|
|
59
|
+
// temp JSON spec.
|
|
60
|
+
f.text('title', 'Title'),
|
|
45
61
|
f.text('description', 'Description'),
|
|
46
62
|
// Between the description and the assignee on purpose: a parent is chosen
|
|
47
63
|
// WITH what the ticket is about, before who works on it (GAIA-257 D5).
|
|
@@ -60,12 +76,22 @@ export const CREATE_FORM = declareForm({
|
|
|
60
76
|
pageSize: PARENT_PAGE_SIZE,
|
|
61
77
|
}),
|
|
62
78
|
f.select('assignee', 'Assignee', { from: 'users' }),
|
|
63
|
-
|
|
79
|
+
// Clearable (D2): a SET conductor means the ticket is claimed at the next
|
|
80
|
+
// poll and a qualification run starts; a cleared one means it waits for a
|
|
81
|
+
// human. The field is clearable precisely so that choice is the operator's
|
|
82
|
+
// and not implicit. The preselection survives — `initial()` fills a select
|
|
83
|
+
// from `listOf(options)[0]` regardless of `clearable`, and `ownFirst` in
|
|
84
|
+
// `create-data.ts` puts the machine's own conductor there.
|
|
85
|
+
f.select('conductor', 'Conductor', { from: 'conductors', clearable: true }),
|
|
64
86
|
f.multi('labels', 'Labels', { from: 'labels' }),
|
|
65
87
|
f.select('workflow', 'Workflow', { from: 'workflows' }),
|
|
66
|
-
|
|
88
|
+
// Agent row FIRST and keeping the id `'submit'` (D7): nothing that exists
|
|
89
|
+
// today changes its meaning or its position.
|
|
90
|
+
f.action('Create with agent', AGENT_ACTION_ID),
|
|
91
|
+
f.action('Create directly', DIRECT_ACTION_ID),
|
|
67
92
|
],
|
|
68
93
|
toObject: (v, o) => ({
|
|
94
|
+
title: v.title,
|
|
69
95
|
description: v.description,
|
|
70
96
|
workflow: optionFor(o.workflows, v.workflow),
|
|
71
97
|
assignee: optionFor(o.users, v.assignee),
|
|
@@ -76,13 +102,24 @@ export const CREATE_FORM = declareForm({
|
|
|
76
102
|
});
|
|
77
103
|
/** Field order in the popup (top to bottom). */
|
|
78
104
|
export const FIELDS = CREATE_FORM.fields;
|
|
105
|
+
/** Snapshot the routing values of a form that is about to be replaced. */
|
|
106
|
+
export const rememberedValues = (s) => ({
|
|
107
|
+
parent: s.parent,
|
|
108
|
+
parentQuery: s.parentQuery,
|
|
109
|
+
assignee: s.assignee,
|
|
110
|
+
conductor: s.conductor,
|
|
111
|
+
// Copied, not referenced: the snapshot outlives the state object it came
|
|
112
|
+
// from, and the reducer builds a NEW Set on every toggle — an alias would
|
|
113
|
+
// make the memory depend on which of the two was mutated last.
|
|
114
|
+
labels: new Set(s.labels),
|
|
115
|
+
});
|
|
79
116
|
// ---- thin adapters (GAIA-236 AC-3) -----------------------------------------
|
|
80
117
|
//
|
|
81
118
|
// The popup itself is driven by `keyToIntent` + `CREATE_FORM.apply` in the kernel.
|
|
82
119
|
// These per-transition functions stay because they ARE this module's tested
|
|
83
120
|
// contract: keeping them is what lets `tests/Service/create-form.test.ts` prove the
|
|
84
121
|
// re-cut changed no behaviour instead of being rewritten to match it.
|
|
85
|
-
export const initialForm = (opts) => CREATE_FORM.initial(opts);
|
|
122
|
+
export const initialForm = (opts, prefill = {}) => CREATE_FORM.initial(opts, prefill);
|
|
86
123
|
export const activeField = (s) => CREATE_FORM.activeField(s);
|
|
87
124
|
export const moveField = (s, delta) => CREATE_FORM.move(s, delta);
|
|
88
125
|
/** Append typed text to the description (only meaningful on the text field). */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OptionClients } from '../../transport.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type EditFormOptions } from './edit.js';
|
|
3
3
|
/** The two clients an edit-options load may use, by role. */
|
|
4
4
|
export type EditFormClients = OptionClients;
|
|
5
5
|
export interface EditFormHints {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// once, here, beside the create adapter it differs from.
|
|
9
9
|
import { listConductorsPreferringWrite, } from '../../conductor/Data/conductors.js';
|
|
10
10
|
import { listUsersPreferringWrite } from '../../user/Data/users.js';
|
|
11
|
+
import { STATE_TARGET_OPTIONS } from './edit.js';
|
|
11
12
|
/**
|
|
12
13
|
* uid 0 is Drupal's Anonymous and never a valid human assignee. A row whose uid never
|
|
13
14
|
* loaded is `undefined` — NOT Anonymous — and stays.
|
|
@@ -34,5 +35,8 @@ export async function loadEditFormOptions(clients, hints = {}) {
|
|
|
34
35
|
return {
|
|
35
36
|
users: excludeAnonymous(users).map((u) => ({ id: u.id, label: u.label })),
|
|
36
37
|
conductors: inProject(conductors, hints.projectId).map(withStatusSuffix),
|
|
38
|
+
// A constant, not a read — the popup's State row needs its positions
|
|
39
|
+
// whether or not the two collection reads succeeded (D8).
|
|
40
|
+
stateTargets: STATE_TARGET_OPTIONS,
|
|
37
41
|
};
|
|
38
42
|
}
|
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
import { type FormState } from '../../../Form/index.js';
|
|
2
2
|
import type { TicketDetailDocument } from '../../../types.js';
|
|
3
3
|
import type { FieldDef, Option } from './create.js';
|
|
4
|
-
export type EditFieldId = 'assignee' | 'conductor' | 'submit';
|
|
5
|
-
/**
|
|
4
|
+
export type EditFieldId = 'assignee' | 'conductor' | 'estimate' | 'spent' | 'stateTarget' | 'submit';
|
|
5
|
+
/**
|
|
6
|
+
* GAIA-313 — the ONE lifecycle move this popup offers, as a named constant.
|
|
7
|
+
*
|
|
8
|
+
* Not derived, and deliberately so: `state_flow` is a `gaia_dashboard` computed
|
|
9
|
+
* field rendering the happy path (`qualification … done`) and never contains
|
|
10
|
+
* `cancelled`, so no served fact carries the cancel edge. The precedent is
|
|
11
|
+
* `STATE_FILTER_VALUES` in `Data/query.ts`, hardcoded for the same reason.
|
|
12
|
+
*
|
|
13
|
+
* One constant serves every ticket workflow because in ALL of them
|
|
14
|
+
* (`gaia_feature`, `gaia_bug`, `gaia_chore`, `gaia_deployment`,
|
|
15
|
+
* `gaia_verification`) `cancel.from` is exactly the set of non-terminal states —
|
|
16
|
+
* so "non-terminal" and "the workflow permits cancelling" are the same
|
|
17
|
+
* predicate, and no workflow id is needed to tell them apart.
|
|
18
|
+
*/
|
|
19
|
+
export declare const CANCELLED_STATE = "cancelled";
|
|
20
|
+
/** The position that writes nothing. Not `null`: the shared `select` widget
|
|
21
|
+
* renders a clearable null as `— none —`, which is not what "keep" means. */
|
|
22
|
+
export declare const KEEP_STATE = "keep";
|
|
23
|
+
/** The declared field's id. Never `cancel` — `intent.kind === 'cancel'` already
|
|
24
|
+
* means "close the popup", and two meanings for one word is a defect waiting. */
|
|
25
|
+
export declare const STATE_FIELD_ID = "stateTarget";
|
|
26
|
+
/** The two positions of the State row, in ring order. */
|
|
27
|
+
export declare const STATE_TARGET_OPTIONS: Option[];
|
|
28
|
+
/**
|
|
29
|
+
* The states no transition leaves, in every ticket workflow.
|
|
30
|
+
*
|
|
31
|
+
* The server's own definition (`TypeWorkflows::isTerminal` — "a state no
|
|
32
|
+
* transition leaves") evaluates to exactly this pair for all five workflows.
|
|
33
|
+
*/
|
|
34
|
+
export declare const TERMINAL_TICKET_STATES: ReadonlySet<string>;
|
|
35
|
+
/** The option lists the popup offers (loaded by `loadEditFormOptions`). */
|
|
6
36
|
export interface EditFormOptions {
|
|
7
37
|
users: Option[];
|
|
8
38
|
conductors: Option[];
|
|
39
|
+
/** A CONSTANT list, not a read — it rides in the bag because `from` names a
|
|
40
|
+
* bag key and that is the only channel a declared select has (D8). */
|
|
41
|
+
stateTargets: Option[];
|
|
9
42
|
}
|
|
10
43
|
/** What the form HOLDS. `ticketId` is a NON-field value: the kernel carries it
|
|
11
44
|
* verbatim so submit needs no ambient state, and never transitions it. */
|
|
@@ -13,9 +46,17 @@ interface EditValues {
|
|
|
13
46
|
ticketId: string;
|
|
14
47
|
assignee: string | null;
|
|
15
48
|
conductor: string | null;
|
|
49
|
+
/** The human duration as TYPED — seconds only exist at `toObject` (D5). */
|
|
50
|
+
estimate: string;
|
|
51
|
+
spent: string;
|
|
52
|
+
/** `KEEP_STATE` or `CANCELLED_STATE`. Nullable because `SelectKey` requires
|
|
53
|
+
* it; the control cannot produce a null (non-clearable ring, inert Del). */
|
|
54
|
+
stateTarget: string | null;
|
|
16
55
|
}
|
|
17
|
-
/** The
|
|
56
|
+
/** The payload of the PATCH (GAIA-233 Decision 2's `ResourceWriteBody`, which
|
|
57
|
+
* already declares `attributes?`, so the writer needs no change). */
|
|
18
58
|
export interface EditFormPayload {
|
|
59
|
+
attributes: Record<string, number | string | null>;
|
|
19
60
|
relationships: {
|
|
20
61
|
assignee_user_id: {
|
|
21
62
|
data: {
|
|
@@ -31,14 +72,7 @@ export interface EditFormPayload {
|
|
|
31
72
|
};
|
|
32
73
|
};
|
|
33
74
|
}
|
|
34
|
-
/**
|
|
35
|
-
* The declaration. BOTH selects are `clearable`: AC-4 requires REMOVING an
|
|
36
|
-
* assignment, not only changing it, so `null` is a real position in each cycle.
|
|
37
|
-
*
|
|
38
|
-
* `toObject` always emits BOTH relationships, even when neither changed: "only send
|
|
39
|
-
* what changed" is a rule the operator cannot read off the screen, and an explicit
|
|
40
|
-
* no-op PATCH is what the popup visibly promises.
|
|
41
|
-
*/
|
|
75
|
+
/** The ordinary popup: a non-terminal ticket can be cancelled from it. */
|
|
42
76
|
export declare const EDIT_FORM: {
|
|
43
77
|
fields: import("../../../Form/field.js").FieldDecl[];
|
|
44
78
|
activeField: (s: FormState<EditValues>) => import("../../../Form/field.js").FieldDecl;
|
|
@@ -47,9 +81,42 @@ export declare const EDIT_FORM: {
|
|
|
47
81
|
apply(s: FormState<EditValues>, options: EditFormOptions, intent: import("../../../Form/form.js").Intent): FormState<EditValues>;
|
|
48
82
|
toObject: (s: FormState<EditValues>, options: EditFormOptions) => EditFormPayload;
|
|
49
83
|
};
|
|
50
|
-
/**
|
|
84
|
+
/** A terminal ticket's popup: assignment and time only. */
|
|
85
|
+
export declare const EDIT_FORM_TERMINAL: {
|
|
86
|
+
fields: import("../../../Form/field.js").FieldDecl[];
|
|
87
|
+
activeField: (s: FormState<EditValues>) => import("../../../Form/field.js").FieldDecl;
|
|
88
|
+
move: (s: FormState<EditValues>, d: number) => FormState<EditValues>;
|
|
89
|
+
initial(options: EditFormOptions, prefill?: Partial<EditValues>): FormState<EditValues>;
|
|
90
|
+
apply(s: FormState<EditValues>, options: EditFormOptions, intent: import("../../../Form/form.js").Intent): FormState<EditValues>;
|
|
91
|
+
toObject: (s: FormState<EditValues>, options: EditFormOptions) => EditFormPayload;
|
|
92
|
+
};
|
|
93
|
+
export type EditFormDeclaration = typeof EDIT_FORM;
|
|
94
|
+
/** The ticket's state as the popup sees it; `''` when the read carried none. */
|
|
95
|
+
export declare const ticketState: (document: TicketDetailDocument) => string;
|
|
96
|
+
/**
|
|
97
|
+
* Which declaration a ticket gets (AC-4).
|
|
98
|
+
*
|
|
99
|
+
* An unknown state fails OPEN (D7): `loadTicketDetail` requests no fieldset, so
|
|
100
|
+
* a missing `state` means genuinely unknown rather than not-requested. Hiding
|
|
101
|
+
* the row on a thin read would cost a legitimate affordance; offering it costs
|
|
102
|
+
* at worst the server's 422, which the popup already renders in full.
|
|
103
|
+
*/
|
|
104
|
+
export declare const editFormFor: (document: TicketDetailDocument) => EditFormDeclaration;
|
|
105
|
+
/** Field order in the popup, per declaration. */
|
|
51
106
|
export declare const EDIT_FIELDS: FieldDef<EditFieldId>[];
|
|
107
|
+
export declare const EDIT_FIELDS_TERMINAL: FieldDef<EditFieldId>[];
|
|
52
108
|
export type EditFormState = FormState<EditValues>;
|
|
109
|
+
/**
|
|
110
|
+
* No lists to OFFER — the two server-read bags are empty; the constant one is
|
|
111
|
+
* not, because it is not read from anywhere.
|
|
112
|
+
*
|
|
113
|
+
* The prefill below names every value, and `clear`/`toObject` read no option
|
|
114
|
+
* list, so the adapters that have no options to pass hand over this bag.
|
|
115
|
+
* Exported so the screen's options-failure path can reuse it: a failed
|
|
116
|
+
* user/conductor read must not silently take the State row's positions away
|
|
117
|
+
* (D8).
|
|
118
|
+
*/
|
|
119
|
+
export declare const EMPTY_EDIT_FORM_OPTIONS: EditFormOptions;
|
|
53
120
|
/** Prefill from the ticket's CURRENT values (AC-1) — never from `opts[0]`. The
|
|
54
121
|
* kernel applies a prefill with `Object.hasOwn`, so an unassigned ticket's
|
|
55
122
|
* explicit `null` STICKS instead of falling back to the first option. */
|
|
@@ -1,53 +1,200 @@
|
|
|
1
|
-
// The "Edit
|
|
2
|
-
// (GAIA-236): two `clearable` selects
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// The "Edit ticket" popup (GAIA-233, widened by GAIA-308 and GAIA-313) as a
|
|
2
|
+
// DECLARATION over the form kernel (GAIA-236): two `clearable` selects, two text
|
|
3
|
+
// fields carrying the accounted durations, one non-clearable State select, plus
|
|
4
|
+
// an action; and one `toObject` that yields the JSON:API payload of the PATCH.
|
|
5
|
+
// Its own field list, its own prefill source (the ticket's own relationships and
|
|
6
|
+
// attributes, never `opts[0]`) and its own output — the mechanics are the
|
|
7
|
+
// kernel's. No I/O here.
|
|
8
|
+
//
|
|
9
|
+
// GAIA-313 makes it TWO declarations from one body: a terminal ticket's popup
|
|
10
|
+
// simply does not carry the State row (D3), and `editFormFor` picks between them.
|
|
6
11
|
import { declareForm } from '../../../Form/index.js';
|
|
12
|
+
import { formatDuration, parseDuration } from '../../../lib/format.js';
|
|
13
|
+
/**
|
|
14
|
+
* GAIA-313 — the ONE lifecycle move this popup offers, as a named constant.
|
|
15
|
+
*
|
|
16
|
+
* Not derived, and deliberately so: `state_flow` is a `gaia_dashboard` computed
|
|
17
|
+
* field rendering the happy path (`qualification … done`) and never contains
|
|
18
|
+
* `cancelled`, so no served fact carries the cancel edge. The precedent is
|
|
19
|
+
* `STATE_FILTER_VALUES` in `Data/query.ts`, hardcoded for the same reason.
|
|
20
|
+
*
|
|
21
|
+
* One constant serves every ticket workflow because in ALL of them
|
|
22
|
+
* (`gaia_feature`, `gaia_bug`, `gaia_chore`, `gaia_deployment`,
|
|
23
|
+
* `gaia_verification`) `cancel.from` is exactly the set of non-terminal states —
|
|
24
|
+
* so "non-terminal" and "the workflow permits cancelling" are the same
|
|
25
|
+
* predicate, and no workflow id is needed to tell them apart.
|
|
26
|
+
*/
|
|
27
|
+
export const CANCELLED_STATE = 'cancelled';
|
|
28
|
+
/** The position that writes nothing. Not `null`: the shared `select` widget
|
|
29
|
+
* renders a clearable null as `— none —`, which is not what "keep" means. */
|
|
30
|
+
export const KEEP_STATE = 'keep';
|
|
31
|
+
/** The declared field's id. Never `cancel` — `intent.kind === 'cancel'` already
|
|
32
|
+
* means "close the popup", and two meanings for one word is a defect waiting. */
|
|
33
|
+
export const STATE_FIELD_ID = 'stateTarget';
|
|
34
|
+
/** The two positions of the State row, in ring order. */
|
|
35
|
+
export const STATE_TARGET_OPTIONS = [
|
|
36
|
+
{ id: KEEP_STATE, label: 'keep current state' },
|
|
37
|
+
{ id: CANCELLED_STATE, label: 'cancelled — abandon this ticket' },
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* The states no transition leaves, in every ticket workflow.
|
|
41
|
+
*
|
|
42
|
+
* The server's own definition (`TypeWorkflows::isTerminal` — "a state no
|
|
43
|
+
* transition leaves") evaluates to exactly this pair for all five workflows.
|
|
44
|
+
*/
|
|
45
|
+
export const TERMINAL_TICKET_STATES = new Set([
|
|
46
|
+
'done',
|
|
47
|
+
CANCELLED_STATE,
|
|
48
|
+
]);
|
|
7
49
|
/** A JSON:API to-one linkage, or an explicit `null` for "remove it". */
|
|
8
50
|
const linkage = (type, id) => ({
|
|
9
51
|
data: id === null ? null : { type, id },
|
|
10
52
|
});
|
|
11
53
|
/**
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
54
|
+
* The two time attributes of the PATCH, in seconds.
|
|
55
|
+
*
|
|
56
|
+
* Three outcomes per field, and the third is the reason this is not a one-liner:
|
|
57
|
+
* parsed value → the number;
|
|
58
|
+
* empty input → `null`, an explicit REMOVE (the selects are clearable for the
|
|
59
|
+
* same reason — AC-4 requires removing a value, not only
|
|
60
|
+
* changing it);
|
|
61
|
+
* unparseable → the key is OMITTED. Writing 0 for "soon" would silently
|
|
62
|
+
* destroy a real estimate, and the popup has no validation
|
|
63
|
+
* channel of its own; leaving the key out means the server
|
|
64
|
+
* keeps what it had and the operator sees their typo survive
|
|
65
|
+
* the save.
|
|
66
|
+
*
|
|
67
|
+
* This is why `toObject` cannot emit both keys unconditionally the way the two
|
|
68
|
+
* relationships do.
|
|
69
|
+
*/
|
|
70
|
+
const timeAttributes = (v) => {
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const [key, field] of [
|
|
73
|
+
['time_estimate', 'estimate'],
|
|
74
|
+
['time_spent', 'spent'],
|
|
75
|
+
]) {
|
|
76
|
+
const raw = v[field];
|
|
77
|
+
const seconds = parseDuration(raw);
|
|
78
|
+
if (seconds === null)
|
|
79
|
+
continue;
|
|
80
|
+
out[key] = raw.trim() === '' ? null : seconds;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The ONE lifecycle attribute this popup may write, and only on the position
|
|
86
|
+
* that means it (AC-2/AC-3).
|
|
87
|
+
*
|
|
88
|
+
* On every other position the key is ABSENT rather than sent as the ticket's
|
|
89
|
+
* current value: a self-transition (`coding → coding`) is not a declared
|
|
90
|
+
* transition in any workflow, so echoing the current state would turn every
|
|
91
|
+
* ordinary assignee edit into a 422.
|
|
92
|
+
*/
|
|
93
|
+
const stateAttribute = (v) => v.stateTarget === CANCELLED_STATE ? { state: CANCELLED_STATE } : {};
|
|
94
|
+
/**
|
|
95
|
+
* One body, two declarations (D3).
|
|
96
|
+
*
|
|
97
|
+
* BOTH assignment selects are `clearable`: AC-4 requires REMOVING an assignment,
|
|
98
|
+
* not only changing it, so `null` is a real position in each cycle.
|
|
14
99
|
*
|
|
15
100
|
* `toObject` always emits BOTH relationships, even when neither changed: "only send
|
|
16
101
|
* what changed" is a rule the operator cannot read off the screen, and an explicit
|
|
17
|
-
* no-op PATCH is what the popup visibly promises.
|
|
102
|
+
* no-op PATCH is what the popup visibly promises. The two time attributes are the
|
|
103
|
+
* documented exception — see `timeAttributes`.
|
|
104
|
+
*
|
|
105
|
+
* The kernel builds its field list once, at module load, and every consumer —
|
|
106
|
+
* the renderer's `▸` marker, `keyToIntent`, `apply` — indexes that array by
|
|
107
|
+
* `state.active`. So a row cannot be hidden at render time without shifting
|
|
108
|
+
* every index below it; absence has to be a property of WHICH DECLARATION is in
|
|
109
|
+
* force. The State row sits last, above the action, so indices 0–3 never move.
|
|
18
110
|
*/
|
|
19
|
-
|
|
111
|
+
const declareEditForm = (cancellable) => declareForm({
|
|
20
112
|
fields: (f) => [
|
|
21
113
|
f.select('assignee', 'Assignee', { from: 'users', clearable: true }),
|
|
22
|
-
f.select('conductor', 'Conductor', {
|
|
23
|
-
|
|
114
|
+
f.select('conductor', 'Conductor', {
|
|
115
|
+
from: 'conductors',
|
|
116
|
+
clearable: true,
|
|
117
|
+
}),
|
|
118
|
+
f.text('estimate', 'Estimate'),
|
|
119
|
+
f.text('spent', 'Spent'),
|
|
120
|
+
// NOT clearable: that is what makes "exactly two positions" provable
|
|
121
|
+
// rather than observed — `keyToIntent` returns null for Del/Backspace on
|
|
122
|
+
// a non-clearable select, and `cycleValue` never puts a null in the ring.
|
|
123
|
+
...(cancellable
|
|
124
|
+
? [f.select('stateTarget', 'State', { from: 'stateTargets' })]
|
|
125
|
+
: []),
|
|
126
|
+
f.action('Save'),
|
|
24
127
|
],
|
|
25
128
|
toObject: (v) => ({
|
|
129
|
+
attributes: { ...timeAttributes(v), ...stateAttribute(v) },
|
|
26
130
|
relationships: {
|
|
27
131
|
assignee_user_id: linkage('user--user', v.assignee),
|
|
28
132
|
conductor_id: linkage('gaia_conductor--gaia_conductor', v.conductor),
|
|
29
133
|
},
|
|
30
134
|
}),
|
|
31
135
|
});
|
|
32
|
-
/**
|
|
136
|
+
/** The ordinary popup: a non-terminal ticket can be cancelled from it. */
|
|
137
|
+
export const EDIT_FORM = declareEditForm(true);
|
|
138
|
+
/** A terminal ticket's popup: assignment and time only. */
|
|
139
|
+
export const EDIT_FORM_TERMINAL = declareEditForm(false);
|
|
140
|
+
/** The ticket's state as the popup sees it; `''` when the read carried none. */
|
|
141
|
+
export const ticketState = (document) => {
|
|
142
|
+
const value = document.data?.attributes?.state;
|
|
143
|
+
return typeof value === 'string' ? value : '';
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Which declaration a ticket gets (AC-4).
|
|
147
|
+
*
|
|
148
|
+
* An unknown state fails OPEN (D7): `loadTicketDetail` requests no fieldset, so
|
|
149
|
+
* a missing `state` means genuinely unknown rather than not-requested. Hiding
|
|
150
|
+
* the row on a thin read would cost a legitimate affordance; offering it costs
|
|
151
|
+
* at worst the server's 422, which the popup already renders in full.
|
|
152
|
+
*/
|
|
153
|
+
export const editFormFor = (document) => TERMINAL_TICKET_STATES.has(ticketState(document))
|
|
154
|
+
? EDIT_FORM_TERMINAL
|
|
155
|
+
: EDIT_FORM;
|
|
156
|
+
/** Field order in the popup, per declaration. */
|
|
33
157
|
export const EDIT_FIELDS = EDIT_FORM.fields;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
158
|
+
export const EDIT_FIELDS_TERMINAL = EDIT_FORM_TERMINAL.fields;
|
|
159
|
+
/**
|
|
160
|
+
* No lists to OFFER — the two server-read bags are empty; the constant one is
|
|
161
|
+
* not, because it is not read from anywhere.
|
|
162
|
+
*
|
|
163
|
+
* The prefill below names every value, and `clear`/`toObject` read no option
|
|
164
|
+
* list, so the adapters that have no options to pass hand over this bag.
|
|
165
|
+
* Exported so the screen's options-failure path can reuse it: a failed
|
|
166
|
+
* user/conductor read must not silently take the State row's positions away
|
|
167
|
+
* (D8).
|
|
168
|
+
*/
|
|
169
|
+
export const EMPTY_EDIT_FORM_OPTIONS = {
|
|
170
|
+
users: [],
|
|
171
|
+
conductors: [],
|
|
172
|
+
stateTargets: STATE_TARGET_OPTIONS,
|
|
173
|
+
};
|
|
37
174
|
const relId = (document, name) => {
|
|
38
175
|
const data = document.data?.relationships?.[name]?.data;
|
|
39
176
|
if (!data)
|
|
40
177
|
return null;
|
|
41
178
|
return Array.isArray(data) ? (data[0]?.id ?? null) : data.id;
|
|
42
179
|
};
|
|
180
|
+
const timeValue = (document, name) => {
|
|
181
|
+
const value = document.data?.attributes?.[name];
|
|
182
|
+
// An unset field prefills EMPTY, never `0m`: the popup must not turn "nobody
|
|
183
|
+
// estimated this" into a typed zero the operator then saves by accident.
|
|
184
|
+
return typeof value === 'number' ? formatDuration(value) : '';
|
|
185
|
+
};
|
|
43
186
|
/** Prefill from the ticket's CURRENT values (AC-1) — never from `opts[0]`. The
|
|
44
187
|
* kernel applies a prefill with `Object.hasOwn`, so an unassigned ticket's
|
|
45
188
|
* explicit `null` STICKS instead of falling back to the first option. */
|
|
46
189
|
export function initialEditForm(document) {
|
|
47
|
-
return EDIT_FORM.initial(
|
|
190
|
+
return EDIT_FORM.initial(EMPTY_EDIT_FORM_OPTIONS, {
|
|
48
191
|
ticketId: document.data?.id ?? '',
|
|
49
192
|
assignee: relId(document, 'assignee_user_id'),
|
|
50
193
|
conductor: relId(document, 'conductor_id'),
|
|
194
|
+
estimate: timeValue(document, 'time_estimate'),
|
|
195
|
+
spent: timeValue(document, 'time_spent'),
|
|
196
|
+
// Always KEEP, on both declarations: the popup never opens armed to cancel.
|
|
197
|
+
stateTarget: KEEP_STATE,
|
|
51
198
|
});
|
|
52
199
|
}
|
|
53
200
|
// ---- thin adapters over the kernel (the popup itself runs on keyToIntent) ----
|
|
@@ -57,5 +204,5 @@ export const moveEditField = (s, delta) => EDIT_FORM.move(s, delta);
|
|
|
57
204
|
export const cycleEdit = (s, opts, dir) => EDIT_FORM.apply(s, opts, { kind: 'cycle', dir });
|
|
58
205
|
/** Clear the active select outright (the Del/Backspace shortcut). Cycling to `null`
|
|
59
206
|
* already works; a long conductor list just makes it tedious. */
|
|
60
|
-
export const clearEditValue = (s) => EDIT_FORM.apply(s,
|
|
61
|
-
export const serializeEdit = (s) => EDIT_FORM.toObject(s,
|
|
207
|
+
export const clearEditValue = (s) => EDIT_FORM.apply(s, EMPTY_EDIT_FORM_OPTIONS, { kind: 'clear' });
|
|
208
|
+
export const serializeEdit = (s) => EDIT_FORM.toObject(s, EMPTY_EDIT_FORM_OPTIONS);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket-form adapter for translating terminal input into form intents.
|
|
3
|
+
*
|
|
4
|
+
* The generic form kernel deliberately has no knowledge of terminal screens;
|
|
5
|
+
* consumers outside the kernel reach it through their entity form adapter.
|
|
6
|
+
*/
|
|
7
|
+
export { keyToIntent } from '../../../Form/index.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket-form adapter for translating terminal input into form intents.
|
|
3
|
+
*
|
|
4
|
+
* The generic form kernel deliberately has no knowledge of terminal screens;
|
|
5
|
+
* consumers outside the kernel reach it through their entity form adapter.
|
|
6
|
+
*/
|
|
7
|
+
export { keyToIntent } from '../../../Form/index.js';
|