@bakers-build/addon-bakers-ui 0.1.2-next.88 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Entity/ticket/Component/edit-form/index.js +1 -1
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +25 -2
- package/dist/src/Entity/ticket/Data/detail.d.ts +4 -2
- package/dist/src/Entity/ticket/Data/detail.js +6 -2
- package/dist/src/Entity/ticket/Data/query.js +3 -4
- package/dist/src/Entity/ticket/Data/store.js +8 -3
- package/dist/src/Entity/ticket/Data/workflow.js +3 -3
- package/dist/src/Entity/ticket/Data/write.d.ts +1 -1
- package/dist/src/Entity/ticket/Data/write.js +8 -2
- package/dist/src/Entity/ticket/Form/create-data.js +17 -8
- package/dist/src/Entity/ticket/Form/create.d.ts +18 -7
- package/dist/src/Entity/ticket/Form/create.js +11 -3
- package/dist/src/Entity/ticket/Form/edit-data.js +15 -7
- package/dist/src/Entity/ticket/Form/edit.d.ts +23 -6
- package/dist/src/Entity/ticket/Form/edit.js +15 -7
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +3 -2
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +7 -2
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +2 -0
- package/dist/src/Entity/ticket/Screen/ticket-filter-bar.js +2 -3
- package/package.json +3 -3
|
@@ -58,7 +58,7 @@ const WARNING_INDENT = 2;
|
|
|
58
58
|
* silently left unwritten, so the accepted spelling has to be on screen. */
|
|
59
59
|
const LEGEND = 'Assignee = human ownership · Reviewers = named reviewers · Conductor = who works it next · ' +
|
|
60
60
|
'Estimate/Spent = accounted time, not the run footprint (1d 2h 30m, 90m, or a bare number as minutes) · ' +
|
|
61
|
-
'Parent = optional parent ticket · Labels = multi-select from
|
|
61
|
+
'Parent = optional parent ticket · Labels = multi-select from label · ' +
|
|
62
62
|
'Cancel ticket abandons a non-terminal ticket (Esc closes and writes nothing)';
|
|
63
63
|
/**
|
|
64
64
|
* A block of chrome prose, wrapped so the frame never has to clip it.
|
|
@@ -43,12 +43,15 @@ function headerLineNodes(id, content, fg) {
|
|
|
43
43
|
}));
|
|
44
44
|
}
|
|
45
45
|
export function ticketHeaderTree(document, identifier, title, lastRow, labels) {
|
|
46
|
-
// One compact meta line — state · assignee · labels — under
|
|
46
|
+
// One compact meta line — state · assignee · type · recipe · labels — under
|
|
47
|
+
// the title. The
|
|
47
48
|
// full set (created by, branch, …) lives in the Overview sidebar, so the
|
|
48
49
|
// header stays two lines rather than five. Empty fields drop out.
|
|
49
50
|
const meta = [
|
|
50
51
|
headerState(document),
|
|
51
52
|
headerAssignee(document, labels),
|
|
53
|
+
termName(document, 'ticket_type', 'bb_term--type'),
|
|
54
|
+
termName(document, 'recipe', 'bb_term--recipe'),
|
|
52
55
|
headerLabels(document),
|
|
53
56
|
]
|
|
54
57
|
.filter((part) => part !== '' && part !== '—')
|
|
@@ -513,7 +516,18 @@ function typedCommentLines(document, type) {
|
|
|
513
516
|
}
|
|
514
517
|
/** The `bb_term` label names of the ticket, resolved include → uuid (D4/D8). */
|
|
515
518
|
function labelNames(document) {
|
|
516
|
-
return toManyIds(document, 'labels').map((id) => asString(included(document, 'bb_term--
|
|
519
|
+
return toManyIds(document, 'labels').map((id) => asString(included(document, 'bb_term--label', id)?.attributes?.name) ??
|
|
520
|
+
id);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* A single-valued `bb_term` relationship's name (BAKERS-17 `ticket_type` /
|
|
524
|
+
* `recipe`), resolved include → uuid like the labels; `''` when unset.
|
|
525
|
+
*/
|
|
526
|
+
function termName(document, name, type) {
|
|
527
|
+
const id = relId(document, name);
|
|
528
|
+
if (id === null)
|
|
529
|
+
return '';
|
|
530
|
+
return asString(included(document, type, id)?.attributes?.name) ?? id;
|
|
517
531
|
}
|
|
518
532
|
/**
|
|
519
533
|
* The ticket's labels as ONE row of text — the header box's labels row (AC-8) and
|
|
@@ -669,6 +683,15 @@ export function detailSections(document, labels, nowSeconds = 0, baseUrl) {
|
|
|
669
683
|
heading: 'Labels',
|
|
670
684
|
lines: labelNames(document).length ? labelNames(document) : ['No labels.'],
|
|
671
685
|
});
|
|
686
|
+
// BAKERS-17: Type and Recipe sit beside Labels, for the same reason.
|
|
687
|
+
sections.push({
|
|
688
|
+
heading: 'Type',
|
|
689
|
+
lines: [termName(document, 'ticket_type', 'bb_term--type') || 'No type.'],
|
|
690
|
+
});
|
|
691
|
+
sections.push({
|
|
692
|
+
heading: 'Recipe',
|
|
693
|
+
lines: [termName(document, 'recipe', 'bb_term--recipe') || 'No recipe.'],
|
|
694
|
+
});
|
|
672
695
|
// BB-678: the upstream ticket this one was imported from — above `Parent`,
|
|
673
696
|
// matching the interactive Related pane's row order.
|
|
674
697
|
sections.push({ heading: 'Source', lines: sourceLines(document) });
|
|
@@ -16,8 +16,10 @@ import type { DropshClient, TicketDetailDocument } from '../../../types.js';
|
|
|
16
16
|
* BB-255 added `parent_id,labels` for the same reason: the Parent row (AC-3)
|
|
17
17
|
* and the header box's labels row (AC-8) would otherwise be raw uuids. Both were
|
|
18
18
|
* re-measured live on BB-255 — `labels` sideloads
|
|
19
|
-
* `bb_term--
|
|
20
|
-
* a parentless ticket.
|
|
19
|
+
* `bb_term--label` carrying `name`, and `parent_id` is `{data: null}` on
|
|
20
|
+
* a parentless ticket. BAKERS-17 added `ticket_type,recipe` for the header's
|
|
21
|
+
* type and recipe names, sideloading `bb_term--type` / `bb_term--recipe`.
|
|
22
|
+
* The `runs` include already ships each run's FULL attribute
|
|
21
23
|
* set, which is why the Runs tab and the run detail screen add no request at all.
|
|
22
24
|
*
|
|
23
25
|
* `parent_id` serves BB-252 as well: after a save from the ticket list, this
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
* BB-255 added `parent_id,labels` for the same reason: the Parent row (AC-3)
|
|
16
16
|
* and the header box's labels row (AC-8) would otherwise be raw uuids. Both were
|
|
17
17
|
* re-measured live on BB-255 — `labels` sideloads
|
|
18
|
-
* `bb_term--
|
|
19
|
-
* a parentless ticket.
|
|
18
|
+
* `bb_term--label` carrying `name`, and `parent_id` is `{data: null}` on
|
|
19
|
+
* a parentless ticket. BAKERS-17 added `ticket_type,recipe` for the header's
|
|
20
|
+
* type and recipe names, sideloading `bb_term--type` / `bb_term--recipe`.
|
|
21
|
+
* The `runs` include already ships each run's FULL attribute
|
|
20
22
|
* set, which is why the Runs tab and the run detail screen add no request at all.
|
|
21
23
|
*
|
|
22
24
|
* `parent_id` serves BB-252 as well: after a save from the ticket list, this
|
|
@@ -52,6 +54,8 @@ export function loadTicketDetail(client, ticketId, includes = [
|
|
|
52
54
|
'assignee_user_id',
|
|
53
55
|
'parent_id',
|
|
54
56
|
'labels',
|
|
57
|
+
'ticket_type',
|
|
58
|
+
'recipe',
|
|
55
59
|
'blocked_by',
|
|
56
60
|
'owner_user_id',
|
|
57
61
|
'reviewers',
|
|
@@ -21,15 +21,14 @@ export { CLOSED_FILTER_VALUES, cycleValue } from '../../cycle.js';
|
|
|
21
21
|
import { LIST_PAGE_SIZE as PAGE_SIZE } from '../../cycle.js';
|
|
22
22
|
import { TICKET_CONDUCTOR_INCLUDE_FIELDS, TICKET_LIST_FIELDS, } from './tickets.js';
|
|
23
23
|
// Ticket states from web/modules/custom/bb_core/bb_core.workflows.yml
|
|
24
|
-
// (
|
|
24
|
+
// (the `bb_work` workflow) plus the import/backlog states seen on tickets.
|
|
25
25
|
// Hardcoded so the cycle is deterministic and independent of the loaded page.
|
|
26
26
|
export const STATE_FILTER_VALUES = [
|
|
27
27
|
'triage',
|
|
28
28
|
'backlog',
|
|
29
29
|
'todo',
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'coding',
|
|
30
|
+
'plan',
|
|
31
|
+
'working',
|
|
33
32
|
'review',
|
|
34
33
|
'done',
|
|
35
34
|
'cancelled',
|
|
@@ -19,8 +19,11 @@ const ticketKey = (id) => dataKey(`ticket/${id}`);
|
|
|
19
19
|
* `includes` (`ticket-detail.ts:23`) without joining this list. Every include
|
|
20
20
|
* a screen's `includes` can name belongs here too.
|
|
21
21
|
*/
|
|
22
|
+
// What the header box reads on every tab (labels, and BAKERS-17's type and
|
|
23
|
+
// recipe), so every ticket view loads it whatever its own includes.
|
|
24
|
+
const headerIncludes = ['labels', 'ticket_type', 'recipe'];
|
|
22
25
|
const allIncludes = [
|
|
23
|
-
|
|
26
|
+
...headerIncludes,
|
|
24
27
|
'parent_id',
|
|
25
28
|
'blocked_by',
|
|
26
29
|
'conductor_id',
|
|
@@ -88,7 +91,9 @@ export function readTicket(context, fallback = {}) {
|
|
|
88
91
|
}
|
|
89
92
|
export function ticketLoaded(context, includes) {
|
|
90
93
|
const id = context.params?.ticketId;
|
|
91
|
-
return (!!id &&
|
|
94
|
+
return (!!id &&
|
|
95
|
+
(context.store?.has(ticketKey(id), [...includes, ...headerIncludes]) ??
|
|
96
|
+
true));
|
|
92
97
|
}
|
|
93
98
|
export function loadTicketView(context, includes) {
|
|
94
99
|
const supplied = context.contexts?.has(ticketDetailInputContext)
|
|
@@ -108,7 +113,7 @@ export function loadTicketView(context, includes) {
|
|
|
108
113
|
const key = ticketKey(id);
|
|
109
114
|
if (supplied && !store.get(key))
|
|
110
115
|
store.set(key, supplied, allIncludes);
|
|
111
|
-
return store.load(key, [...includes,
|
|
116
|
+
return store.load(key, [...includes, ...headerIncludes], async (missing) => {
|
|
112
117
|
if (!context.services) {
|
|
113
118
|
if (supplied)
|
|
114
119
|
return supplied;
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// The declaration stays because the `workflow` ATTRIBUTE stays (spec F4).
|
|
8
8
|
/** The single source of truth, in the create form picker's order. */
|
|
9
9
|
export const WORKFLOW_KINDS = [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{ id: '
|
|
10
|
+
// BAKERS-17: one workflow for every work ticket; feature/bug/chore is the
|
|
11
|
+
// ticket's `ticket_type` term now, not its workflow.
|
|
12
|
+
{ id: 'bb_work', label: 'work' },
|
|
13
13
|
];
|
|
14
14
|
/** The picker's `{id, label}` list — what the create form's `workflow` field offers. */
|
|
15
15
|
export const WORKFLOW_OPTIONS = WORKFLOW_KINDS.map((k) => ({
|
|
@@ -33,7 +33,7 @@ export declare const TICKET_DESCRIPTION_FORMAT = "bb_rich";
|
|
|
33
33
|
* 1. NOTHING server-owned is written — no `state` (the server lands the
|
|
34
34
|
* ticket in the workflow's first state), no `identifier`, no `number`, no
|
|
35
35
|
* `branch_name`, no `base_branch`. Acceptance criterion 5 is satisfied by
|
|
36
|
-
* OMISSION, which is also what `@bakers-build/create-ticket` does.
|
|
36
|
+
* OMISSION, which is also what `@bakers-build/bb-create-ticket` does.
|
|
37
37
|
* 2. A relationship with nothing picked is OMITTED, not sent as an explicit
|
|
38
38
|
* `null`. A create has nothing to unset, so `null` would carry no
|
|
39
39
|
* information a missing key does not — the exact opposite of the PATCH in
|
|
@@ -42,7 +42,7 @@ const one = (type, id) => id ? { data: { type, id } } : undefined;
|
|
|
42
42
|
* 1. NOTHING server-owned is written — no `state` (the server lands the
|
|
43
43
|
* ticket in the workflow's first state), no `identifier`, no `number`, no
|
|
44
44
|
* `branch_name`, no `base_branch`. Acceptance criterion 5 is satisfied by
|
|
45
|
-
* OMISSION, which is also what `@bakers-build/create-ticket` does.
|
|
45
|
+
* OMISSION, which is also what `@bakers-build/bb-create-ticket` does.
|
|
46
46
|
* 2. A relationship with nothing picked is OMITTED, not sent as an explicit
|
|
47
47
|
* `null`. A create has nothing to unset, so `null` would carry no
|
|
48
48
|
* information a missing key does not — the exact opposite of the PATCH in
|
|
@@ -61,10 +61,16 @@ export function buildTicketCreateBody(payload, projectId) {
|
|
|
61
61
|
const conductor = one('bb_conductor--bb_conductor', payload.conductor?.id);
|
|
62
62
|
if (conductor)
|
|
63
63
|
relationships.conductor_id = conductor;
|
|
64
|
+
const ticketType = one('bb_term--type', payload.ticketType?.id);
|
|
65
|
+
if (ticketType)
|
|
66
|
+
relationships.ticket_type = ticketType;
|
|
67
|
+
const recipe = one('bb_term--recipe', payload.recipe?.id);
|
|
68
|
+
if (recipe)
|
|
69
|
+
relationships.recipe = recipe;
|
|
64
70
|
if (payload.labels.length > 0) {
|
|
65
71
|
relationships.labels = {
|
|
66
72
|
data: payload.labels.map((l) => ({
|
|
67
|
-
type: 'bb_term--
|
|
73
|
+
type: 'bb_term--label',
|
|
68
74
|
id: l.id,
|
|
69
75
|
})),
|
|
70
76
|
};
|
|
@@ -49,7 +49,18 @@ const toOption = (row) => ({ id: row.id, label: row.label });
|
|
|
49
49
|
* the data service returns terms for whatever purpose, and which of the entity
|
|
50
50
|
* type's vocabularies a given widget wants is the widget's business.
|
|
51
51
|
*/
|
|
52
|
-
const LABEL_VOCABULARY = '
|
|
52
|
+
const LABEL_VOCABULARY = 'label';
|
|
53
|
+
/** The vocabularies of the single-valued `ticket_type` / `recipe` (BAKERS-17). */
|
|
54
|
+
const TYPE_VOCABULARY = 'type';
|
|
55
|
+
const RECIPE_VOCABULARY = 'recipe';
|
|
56
|
+
/** A vocabulary's terms as one option bag, under the bag key `bag`. */
|
|
57
|
+
const termSource = (client, bag, vocabulary) => ({
|
|
58
|
+
bag,
|
|
59
|
+
async load() {
|
|
60
|
+
const outcome = await listTerms(client, vocabulary);
|
|
61
|
+
return { options: outcome.rows.map(toOption), error: outcome.error };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
53
64
|
/**
|
|
54
65
|
* The `conductors` option source for one (possibly absent) project.
|
|
55
66
|
*
|
|
@@ -145,13 +156,9 @@ export function createFormSources(client, hints = {}) {
|
|
|
145
156
|
ownerUserId,
|
|
146
157
|
ownConductorMachineId,
|
|
147
158
|
}),
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const outcome = await listTerms(client, LABEL_VOCABULARY);
|
|
152
|
-
return { options: outcome.rows.map(toOption), error: outcome.error };
|
|
153
|
-
},
|
|
154
|
-
},
|
|
159
|
+
ticketType: termSource(client, 'ticketTypes', TYPE_VOCABULARY),
|
|
160
|
+
recipe: termSource(client, 'recipes', RECIPE_VOCABULARY),
|
|
161
|
+
labels: termSource(client, 'labels', LABEL_VOCABULARY),
|
|
155
162
|
};
|
|
156
163
|
// No project in scope ⇒ NO source, so the bag stays absent rather than empty:
|
|
157
164
|
// "nothing can be offered" and "searched, nothing matched" are different
|
|
@@ -183,6 +190,8 @@ export async function composeCreateFormOptions(sources) {
|
|
|
183
190
|
users: [],
|
|
184
191
|
reviewers: [],
|
|
185
192
|
conductors: [],
|
|
193
|
+
ticketTypes: [],
|
|
194
|
+
recipes: [],
|
|
186
195
|
labels: [],
|
|
187
196
|
workflows: WORKFLOW_OPTIONS,
|
|
188
197
|
// Spread over the defaults, so a bag with no source stays ABSENT (D8) while
|
|
@@ -14,6 +14,8 @@ export interface CreateOptionErrors {
|
|
|
14
14
|
users?: string | undefined;
|
|
15
15
|
reviewers?: string | undefined;
|
|
16
16
|
conductors?: string | undefined;
|
|
17
|
+
ticketTypes?: string | undefined;
|
|
18
|
+
recipes?: string | undefined;
|
|
17
19
|
labels?: string | undefined;
|
|
18
20
|
parents?: string | undefined;
|
|
19
21
|
workflows?: string | undefined;
|
|
@@ -45,6 +47,10 @@ export interface CreateFormOptions {
|
|
|
45
47
|
users: Option[];
|
|
46
48
|
reviewers: Option[];
|
|
47
49
|
conductors: Option[];
|
|
50
|
+
/** The `type` vocabulary, for the single-valued `ticket_type` (BAKERS-17). */
|
|
51
|
+
ticketTypes: Option[];
|
|
52
|
+
/** The `recipe` vocabulary, for the single-valued `recipe` (BAKERS-17). */
|
|
53
|
+
recipes: Option[];
|
|
48
54
|
labels: Option[];
|
|
49
55
|
workflows: Option[];
|
|
50
56
|
/**
|
|
@@ -85,7 +91,7 @@ export type FieldDef<Id extends string = string> = Omit<FieldDecl, 'id'> & {
|
|
|
85
91
|
*/
|
|
86
92
|
export declare const AGENT_ACTION_ID = "submit";
|
|
87
93
|
export declare const DIRECT_ACTION_ID = "direct";
|
|
88
|
-
export type CreateFieldId = 'project' | 'title' | 'description' | 'parent' | 'assignee' | 'reviewers' | 'conductor' | 'labels' | 'workflow' | 'template' | 'submit' | 'direct';
|
|
94
|
+
export type CreateFieldId = 'project' | 'title' | 'description' | 'parent' | 'assignee' | 'reviewers' | 'conductor' | 'ticketType' | 'recipe' | 'labels' | 'workflow' | 'template' | 'submit' | 'direct';
|
|
89
95
|
/** What the form HOLDS. `active`/`cursor` are the kernel's and must not appear. */
|
|
90
96
|
export interface CreateValues {
|
|
91
97
|
/**
|
|
@@ -114,6 +120,8 @@ export interface CreateValues {
|
|
|
114
120
|
assignee: string | null;
|
|
115
121
|
reviewers: ReadonlySet<string>;
|
|
116
122
|
conductor: string | null;
|
|
123
|
+
ticketType: string | null;
|
|
124
|
+
recipe: string | null;
|
|
117
125
|
labels: ReadonlySet<string>;
|
|
118
126
|
workflow: string | null;
|
|
119
127
|
/** The picked ticket template's uuid, or null. Never remembered. */
|
|
@@ -134,6 +142,8 @@ export interface CreateFormPayload {
|
|
|
134
142
|
assignee: Option | null;
|
|
135
143
|
reviewers: Option[];
|
|
136
144
|
conductor: Option | null;
|
|
145
|
+
ticketType: Option | null;
|
|
146
|
+
recipe: Option | null;
|
|
137
147
|
labels: Option[];
|
|
138
148
|
/**
|
|
139
149
|
* The ticket the new one goes under, or null when none was picked (AC-7).
|
|
@@ -146,9 +156,9 @@ export interface CreateFormPayload {
|
|
|
146
156
|
export declare const optionFor: (list: Option[], id: string | null) => Option | null;
|
|
147
157
|
/**
|
|
148
158
|
* The declaration used whenever a project is already in scope (unchanged,
|
|
149
|
-
* BB-664 AC-8). No field carries a function.
|
|
150
|
-
*
|
|
151
|
-
* options only, exactly as before BB-233.
|
|
159
|
+
* BB-664 AC-8). No field carries a function. The `clearable` selects are the
|
|
160
|
+
* conductor (BB-309 D2) and the template, type and recipe, which open empty;
|
|
161
|
+
* every other one cycles over real options only, exactly as before BB-233.
|
|
152
162
|
*/
|
|
153
163
|
export declare const CREATE_FORM: {
|
|
154
164
|
fields: FieldDecl[];
|
|
@@ -192,9 +202,10 @@ export type CreateFormState = FormState<CreateValues>;
|
|
|
192
202
|
* value shape, so renaming a field breaks the build instead of silently
|
|
193
203
|
* dropping it out of the memory.
|
|
194
204
|
*
|
|
195
|
-
* `workflow`
|
|
196
|
-
* and a silently retained `
|
|
197
|
-
* value nobody looks at (D5). The parent contributes BOTH
|
|
205
|
+
* `workflow`, `ticketType` and `recipe` are NOT here, deliberately. They are
|
|
206
|
+
* per-ticket decisions, and a silently retained `bug` type on the next feature
|
|
207
|
+
* ticket is a wrong value nobody looks at (D5). The parent contributes BOTH
|
|
208
|
+
* of its keys, because
|
|
198
209
|
* an autocomplete owns two and `apply` never produces one without the other
|
|
199
210
|
* (D4).
|
|
200
211
|
*/
|
|
@@ -85,6 +85,10 @@ const sharedFields = (f) => [
|
|
|
85
85
|
// from `listOf(options)[0]` regardless of `clearable`, and `ownFirst` in
|
|
86
86
|
// `create-data.ts` puts the machine's own conductor there.
|
|
87
87
|
f.select('conductor', 'Conductor', { from: 'conductors', clearable: true }),
|
|
88
|
+
// Clearable and opened EMPTY by the screen, like the template: a type or
|
|
89
|
+
// recipe is chosen, never preselected.
|
|
90
|
+
f.select('ticketType', 'Type', { from: 'ticketTypes', clearable: true }),
|
|
91
|
+
f.select('recipe', 'Recipe', { from: 'recipes', clearable: true }),
|
|
88
92
|
f.multi('labels', 'Labels', { from: 'labels' }),
|
|
89
93
|
f.select('workflow', 'Workflow', { from: 'workflows' }),
|
|
90
94
|
// A template replaces the ordinary create, so it sits right above
|
|
@@ -103,14 +107,16 @@ const toCreatePayload = (v, o) => ({
|
|
|
103
107
|
assignee: optionFor(o.users, v.assignee),
|
|
104
108
|
reviewers: (o.reviewers ?? []).filter((x) => v.reviewers.has(x.id)),
|
|
105
109
|
conductor: optionFor(o.conductors, v.conductor),
|
|
110
|
+
ticketType: optionFor(o.ticketTypes, v.ticketType),
|
|
111
|
+
recipe: optionFor(o.recipes, v.recipe),
|
|
106
112
|
labels: o.labels.filter((x) => v.labels.has(x.id)),
|
|
107
113
|
parent: parentFor(o.parents ?? [], v.parent),
|
|
108
114
|
});
|
|
109
115
|
/**
|
|
110
116
|
* The declaration used whenever a project is already in scope (unchanged,
|
|
111
|
-
* BB-664 AC-8). No field carries a function.
|
|
112
|
-
*
|
|
113
|
-
* options only, exactly as before BB-233.
|
|
117
|
+
* BB-664 AC-8). No field carries a function. The `clearable` selects are the
|
|
118
|
+
* conductor (BB-309 D2) and the template, type and recipe, which open empty;
|
|
119
|
+
* every other one cycles over real options only, exactly as before BB-233.
|
|
114
120
|
*/
|
|
115
121
|
export const CREATE_FORM = declareForm({
|
|
116
122
|
fields: sharedFields,
|
|
@@ -177,6 +183,8 @@ const NO_OPTIONS = {
|
|
|
177
183
|
users: [],
|
|
178
184
|
reviewers: [],
|
|
179
185
|
conductors: [],
|
|
186
|
+
ticketTypes: [],
|
|
187
|
+
recipes: [],
|
|
180
188
|
labels: [],
|
|
181
189
|
workflows: [],
|
|
182
190
|
};
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// this module knows both sides and therefore owns the mapping to `Option` and this
|
|
5
5
|
// popup's policy: read write-client-first for users/conductors, drop Anonymous, filter
|
|
6
6
|
// conductors to the ticket's project, put liveness in the label, load the
|
|
7
|
-
// `
|
|
7
|
+
// `label` vocabulary for the Labels multi-select (and `type`/`recipe` for the
|
|
8
|
+
// Type and Recipe selects), and reorder ONLY the
|
|
8
9
|
// conductor ring owner-first (BB-460) — the assignee ring is unordered because
|
|
9
10
|
// the popup preselects the ticket's current assignee. Each rule has exactly one
|
|
10
11
|
// user, so each is written once, here, beside the create adapter it differs from.
|
|
@@ -17,7 +18,10 @@ import { listUsersPreferringWrite } from '../../user/Data/users.js';
|
|
|
17
18
|
import { ownFirst } from './create-data.js';
|
|
18
19
|
import { loadReviewerOptions } from './reviewer-options.js';
|
|
19
20
|
/** The vocabulary this popup offers as its labels (BB-360 D9 / D11). */
|
|
20
|
-
const LABEL_VOCABULARY = '
|
|
21
|
+
const LABEL_VOCABULARY = 'label';
|
|
22
|
+
/** The vocabularies of the single-valued `ticket_type` / `recipe` (BAKERS-17). */
|
|
23
|
+
const TYPE_VOCABULARY = 'type';
|
|
24
|
+
const RECIPE_VOCABULARY = 'recipe';
|
|
21
25
|
/**
|
|
22
26
|
* uid 0 is Drupal's Anonymous and never a valid human assignee. A row whose uid never
|
|
23
27
|
* loaded is `undefined` — NOT Anonymous — and stays.
|
|
@@ -37,11 +41,11 @@ const withStatusSuffix = (c) => ({
|
|
|
37
41
|
label: c.status ? `${c.label} (${c.status})` : c.label,
|
|
38
42
|
});
|
|
39
43
|
export async function loadEditFormOptions(clients, hints = {}) {
|
|
40
|
-
//
|
|
44
|
+
// Terms have no write-preferring helper: they are not write-gated the way
|
|
41
45
|
// assignees/conductors are. Fail-open to [] when the read client is absent or
|
|
42
46
|
// the vocabulary throws (D11).
|
|
43
|
-
const
|
|
44
|
-
? listTerms(clients.read,
|
|
47
|
+
const termsPromise = (vocabulary) => clients.read
|
|
48
|
+
? listTerms(clients.read, vocabulary).then((outcome) => outcome.rows.map((t) => ({ id: t.id, label: t.label })), () => [])
|
|
45
49
|
: Promise.resolve([]);
|
|
46
50
|
const reviewerClient = clients.write ?? clients.read;
|
|
47
51
|
const reviewersPromise = reviewerClient
|
|
@@ -50,16 +54,20 @@ export async function loadEditFormOptions(clients, hints = {}) {
|
|
|
50
54
|
options: [],
|
|
51
55
|
error: 'no client — reviewers cannot be loaded',
|
|
52
56
|
});
|
|
53
|
-
const [users, conductors, labels, reviewers] = await Promise.all([
|
|
57
|
+
const [users, conductors, ticketTypes, recipes, labels, reviewers] = await Promise.all([
|
|
54
58
|
listUsersPreferringWrite(clients),
|
|
55
59
|
listConductorsPreferringWrite(clients),
|
|
56
|
-
|
|
60
|
+
termsPromise(TYPE_VOCABULARY),
|
|
61
|
+
termsPromise(RECIPE_VOCABULARY),
|
|
62
|
+
termsPromise(LABEL_VOCABULARY),
|
|
57
63
|
reviewersPromise,
|
|
58
64
|
]);
|
|
59
65
|
return {
|
|
60
66
|
users: excludeAnonymous(users).map((u) => ({ id: u.id, label: u.label })),
|
|
61
67
|
reviewers: reviewers.options,
|
|
62
68
|
conductors: ownFirst(inProject(conductors, hints.projectId).filter((c) => c.active), (c) => hints.ownerUserId !== undefined && c.ownerUserId === hints.ownerUserId).map(withStatusSuffix),
|
|
69
|
+
ticketTypes,
|
|
70
|
+
recipes,
|
|
63
71
|
labels,
|
|
64
72
|
...(reviewers.error === undefined
|
|
65
73
|
? {}
|
|
@@ -1,7 +1,7 @@
|
|
|
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' | 'reviewers' | 'conductor' | 'estimate' | 'spent' | 'parent' | 'labels' | 'submit' | 'cancelTicket';
|
|
4
|
+
export type EditFieldId = 'assignee' | 'reviewers' | 'conductor' | 'estimate' | 'spent' | 'parent' | 'ticketType' | 'recipe' | 'labels' | 'submit' | 'cancelTicket';
|
|
5
5
|
/**
|
|
6
6
|
* BB-313 / BB-360 — the ONE lifecycle move this popup offers, as a named
|
|
7
7
|
* constant.
|
|
@@ -11,10 +11,9 @@ export type EditFieldId = 'assignee' | 'reviewers' | 'conductor' | 'estimate' |
|
|
|
11
11
|
* `cancelled`, so no served fact carries the cancel edge. The precedent is
|
|
12
12
|
* `STATE_FILTER_VALUES` in `Data/query.ts`, hardcoded for the same reason.
|
|
13
13
|
*
|
|
14
|
-
* One constant serves every ticket workflow because in
|
|
15
|
-
* (`
|
|
16
|
-
*
|
|
17
|
-
* so "non-terminal" and "the workflow permits cancelling" are the same
|
|
14
|
+
* One constant serves every ticket workflow because in the ticket workflow
|
|
15
|
+
* (`bb_work`) `cancel.from` is exactly the set of non-terminal states — so
|
|
16
|
+
* "non-terminal" and "the workflow permits cancelling" are the same
|
|
18
17
|
* predicate, and no workflow id is needed to tell them apart.
|
|
19
18
|
*/
|
|
20
19
|
export declare const CANCELLED_STATE = "cancelled";
|
|
@@ -29,7 +28,7 @@ export declare const CANCEL_TICKET_ACTION_ID = "cancelTicket";
|
|
|
29
28
|
* The states no transition leaves, in every ticket workflow.
|
|
30
29
|
*
|
|
31
30
|
* The server's own definition (`TypeWorkflows::isTerminal` — "a state no
|
|
32
|
-
* transition leaves") evaluates to exactly this pair for
|
|
31
|
+
* transition leaves") evaluates to exactly this pair for `bb_work`.
|
|
33
32
|
*/
|
|
34
33
|
export declare const TERMINAL_TICKET_STATES: ReadonlySet<string>;
|
|
35
34
|
/** The option lists the popup offers (loaded by `loadEditFormOptions`). */
|
|
@@ -37,6 +36,10 @@ export interface EditFormOptions {
|
|
|
37
36
|
users: Option[];
|
|
38
37
|
reviewers: Option[];
|
|
39
38
|
conductors: Option[];
|
|
39
|
+
/** The `type` vocabulary, for the single-valued `ticket_type` (BAKERS-17). */
|
|
40
|
+
ticketTypes: Option[];
|
|
41
|
+
/** The `recipe` vocabulary, for the single-valued `recipe` (BAKERS-17). */
|
|
42
|
+
recipes: Option[];
|
|
40
43
|
labels: Option[];
|
|
41
44
|
errors?: {
|
|
42
45
|
reviewers?: string | undefined;
|
|
@@ -63,6 +66,8 @@ interface EditValues {
|
|
|
63
66
|
/** The PICKED parent's uuid, or null. `parentQuery` holds what was typed. */
|
|
64
67
|
parent: string | null;
|
|
65
68
|
parentQuery: string;
|
|
69
|
+
ticketType: string | null;
|
|
70
|
+
recipe: string | null;
|
|
66
71
|
labels: ReadonlySet<string>;
|
|
67
72
|
}
|
|
68
73
|
/** The payload of the ordinary Save PATCH (BB-233 Decision 2). */
|
|
@@ -81,6 +86,18 @@ export interface EditFormPayload {
|
|
|
81
86
|
id: string;
|
|
82
87
|
} | null;
|
|
83
88
|
};
|
|
89
|
+
ticket_type: {
|
|
90
|
+
data: {
|
|
91
|
+
type: string;
|
|
92
|
+
id: string;
|
|
93
|
+
} | null;
|
|
94
|
+
};
|
|
95
|
+
recipe: {
|
|
96
|
+
data: {
|
|
97
|
+
type: string;
|
|
98
|
+
id: string;
|
|
99
|
+
} | null;
|
|
100
|
+
};
|
|
84
101
|
labels: {
|
|
85
102
|
data: {
|
|
86
103
|
type: string;
|
|
@@ -21,10 +21,9 @@ import { PARENT_PAGE_SIZE } from '../Data/parents.js';
|
|
|
21
21
|
* `cancelled`, so no served fact carries the cancel edge. The precedent is
|
|
22
22
|
* `STATE_FILTER_VALUES` in `Data/query.ts`, hardcoded for the same reason.
|
|
23
23
|
*
|
|
24
|
-
* One constant serves every ticket workflow because in
|
|
25
|
-
* (`
|
|
26
|
-
*
|
|
27
|
-
* so "non-terminal" and "the workflow permits cancelling" are the same
|
|
24
|
+
* One constant serves every ticket workflow because in the ticket workflow
|
|
25
|
+
* (`bb_work`) `cancel.from` is exactly the set of non-terminal states — so
|
|
26
|
+
* "non-terminal" and "the workflow permits cancelling" are the same
|
|
28
27
|
* predicate, and no workflow id is needed to tell them apart.
|
|
29
28
|
*/
|
|
30
29
|
export const CANCELLED_STATE = 'cancelled';
|
|
@@ -39,7 +38,7 @@ export const CANCEL_TICKET_ACTION_ID = 'cancelTicket';
|
|
|
39
38
|
* The states no transition leaves, in every ticket workflow.
|
|
40
39
|
*
|
|
41
40
|
* The server's own definition (`TypeWorkflows::isTerminal` — "a state no
|
|
42
|
-
* transition leaves") evaluates to exactly this pair for
|
|
41
|
+
* transition leaves") evaluates to exactly this pair for `bb_work`.
|
|
43
42
|
*/
|
|
44
43
|
export const TERMINAL_TICKET_STATES = new Set([
|
|
45
44
|
'done',
|
|
@@ -51,7 +50,7 @@ const linkage = (type, id) => ({
|
|
|
51
50
|
});
|
|
52
51
|
/** Multi-value term linkage for labels (always present on ordinary Save). */
|
|
53
52
|
const labelsLinkage = (ids) => ({
|
|
54
|
-
data: [...ids].map((id) => ({ type: 'bb_term--
|
|
53
|
+
data: [...ids].map((id) => ({ type: 'bb_term--label', id })),
|
|
55
54
|
});
|
|
56
55
|
/**
|
|
57
56
|
* The two time attributes of the PATCH, in seconds.
|
|
@@ -89,7 +88,8 @@ const timeAttributes = (v) => {
|
|
|
89
88
|
*
|
|
90
89
|
* BOTH assignment selects are `clearable`: AC-4 requires REMOVING an assignment,
|
|
91
90
|
* not only changing it, so `null` is a real position in each cycle. Parent is
|
|
92
|
-
* clearable for the same reason (D7)
|
|
91
|
+
* clearable for the same reason (D7), and so are Type and Recipe. Labels
|
|
92
|
+
* always PATCH the full selected set,
|
|
93
93
|
* including empty.
|
|
94
94
|
*
|
|
95
95
|
* Ordinary Save NEVER includes `attributes.state` — a self-transition is not a
|
|
@@ -119,6 +119,8 @@ const declareEditForm = (savable, cancellable) => declareForm({
|
|
|
119
119
|
absentNote: '(no project context — open a project to pick a parent)',
|
|
120
120
|
pageSize: PARENT_PAGE_SIZE,
|
|
121
121
|
}),
|
|
122
|
+
f.select('ticketType', 'Type', { from: 'ticketTypes', clearable: true }),
|
|
123
|
+
f.select('recipe', 'Recipe', { from: 'recipes', clearable: true }),
|
|
122
124
|
f.multi('labels', 'Labels', { from: 'labels' }),
|
|
123
125
|
...(savable ? [f.action('Save', 'submit')] : []),
|
|
124
126
|
...(cancellable
|
|
@@ -131,6 +133,8 @@ const declareEditForm = (savable, cancellable) => declareForm({
|
|
|
131
133
|
relationships: {
|
|
132
134
|
assignee_user_id: linkage('user--user', v.assignee),
|
|
133
135
|
conductor_id: linkage('bb_conductor--bb_conductor', v.conductor),
|
|
136
|
+
ticket_type: linkage('bb_term--type', v.ticketType),
|
|
137
|
+
recipe: linkage('bb_term--recipe', v.recipe),
|
|
134
138
|
labels: labelsLinkage(v.labels),
|
|
135
139
|
parent_id: linkage('bb_ticket--bb_ticket', v.parent),
|
|
136
140
|
...(v.reviewersLoaded || v.reviewers.size > 0
|
|
@@ -191,6 +195,8 @@ export const EMPTY_EDIT_FORM_OPTIONS = {
|
|
|
191
195
|
users: [],
|
|
192
196
|
reviewers: [],
|
|
193
197
|
conductors: [],
|
|
198
|
+
ticketTypes: [],
|
|
199
|
+
recipes: [],
|
|
194
200
|
labels: [],
|
|
195
201
|
};
|
|
196
202
|
const relId = (document, name) => {
|
|
@@ -266,6 +272,8 @@ export function initialEditForm(document) {
|
|
|
266
272
|
spent: timeValue(document, 'time_spent'),
|
|
267
273
|
parent,
|
|
268
274
|
parentQuery: parentQueryFromDocument(document, parent),
|
|
275
|
+
ticketType: relId(document, 'ticket_type'),
|
|
276
|
+
recipe: relId(document, 'recipe'),
|
|
269
277
|
labels: labelIds(document),
|
|
270
278
|
});
|
|
271
279
|
}
|
|
@@ -73,8 +73,9 @@ export declare class CreateTicketFormScreen extends AsyncModalScreen<CreateFormO
|
|
|
73
73
|
enter(context: ScreenContext): void;
|
|
74
74
|
protected didLoad(options: CreateFormOptions): void;
|
|
75
75
|
/**
|
|
76
|
-
* A new ticket's form. The template
|
|
77
|
-
* first option, and
|
|
76
|
+
* A new ticket's form. The template, type and recipe are forced to none: a
|
|
77
|
+
* select opens on its first option, and each must be chosen, never
|
|
78
|
+
* preselected.
|
|
78
79
|
*/
|
|
79
80
|
private freshForm;
|
|
80
81
|
/** The template picked in the form, or null. */
|
|
@@ -124,6 +124,8 @@ export class CreateTicketFormScreen extends AsyncModalScreen {
|
|
|
124
124
|
users: [],
|
|
125
125
|
reviewers: [],
|
|
126
126
|
conductors: [],
|
|
127
|
+
ticketTypes: [],
|
|
128
|
+
recipes: [],
|
|
127
129
|
labels: [],
|
|
128
130
|
workflows: [],
|
|
129
131
|
errors: {
|
|
@@ -171,13 +173,16 @@ export class CreateTicketFormScreen extends AsyncModalScreen {
|
|
|
171
173
|
this.startOptionLoader();
|
|
172
174
|
}
|
|
173
175
|
/**
|
|
174
|
-
* A new ticket's form. The template
|
|
175
|
-
* first option, and
|
|
176
|
+
* A new ticket's form. The template, type and recipe are forced to none: a
|
|
177
|
+
* select opens on its first option, and each must be chosen, never
|
|
178
|
+
* preselected.
|
|
176
179
|
*/
|
|
177
180
|
freshForm(options) {
|
|
178
181
|
return this.declaration().initial(options, {
|
|
179
182
|
...(this.remembered ?? {}),
|
|
180
183
|
template: null,
|
|
184
|
+
ticketType: null,
|
|
185
|
+
recipe: null,
|
|
181
186
|
});
|
|
182
187
|
}
|
|
183
188
|
/** The template picked in the form, or null. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakers-build/addon-bakers-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "bakers.build TUI: interactive terminal UI (dashboard, projects, tickets) as a dropsh `tui` renderer plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"zustand": "^5.0.15"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@bakers-build/core": "^0.
|
|
33
|
-
"@bakers-build/message": "^0.
|
|
32
|
+
"@bakers-build/core": "^0.2.0",
|
|
33
|
+
"@bakers-build/message": "^0.2.0"
|
|
34
34
|
}
|
|
35
35
|
}
|