@gaia-ai/addon-gaia-ui 0.9.0 → 0.9.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.
@@ -1,7 +1,7 @@
1
1
  import type { VNode } from '@opentui/core';
2
2
  import type { ActiveRunInfo } from '../../../run/Data/active-run.js';
3
3
  import type { FieldDef } from '../../Form/create.js';
4
- import { type EditFieldId, type EditFormOptions, type EditFormState } from '../../Form/edit.js';
4
+ import type { EditFieldId, EditFormOptions, EditFormState } from '../../Form/edit.js';
5
5
  /** The chrome around the fields: who we write as, what went wrong, who is live. */
6
6
  export interface EditFormChrome {
7
7
  /** The auth profile the write will actually use (AC-5 — never implied). */
@@ -1,6 +1,6 @@
1
- // The "Edit ticket" popup's body (GAIA-233, widened by GAIA-308), as CHROME
2
- // around its declaration — a genuine sibling of `Component/create-form` now, not
3
- // a second implementation of it.
1
+ // The "Edit ticket" popup's body (GAIA-233, widened by GAIA-308 / GAIA-360), as
2
+ // CHROME around its declaration — a genuine sibling of `Component/create-form`
3
+ // now, not a second implementation of it.
4
4
  //
5
5
  // GAIA-257 (re-entry) AC-13/AC-14. What this file used to carry, and why each
6
6
  // piece had to go:
@@ -22,10 +22,13 @@
22
22
  // things that really are this popup's own — around one call.
23
23
  //
24
24
  // GAIA-313 (D4) finally makes the claim above literally true: the field list is
25
- // HANDED IN rather than imported. There are two declarations now — one with the
26
- // State row, one without — so importing either here would make this file a
25
+ // HANDED IN rather than imported. There are two declarations now — with and
26
+ // without Cancel ticket — so importing either here would make this file a
27
27
  // second decider that can drift from the screen's. It names no field; it renders
28
- // the list it is given, and keys its one conditional legend on that list.
28
+ // the list it is given.
29
+ //
30
+ // GAIA-360 removes the State select legend. Parent and Labels are ordinary
31
+ // declared fields drawn by the shared widget layer.
29
32
  //
30
33
  // GAIA-340 Task 9 — and it returns a RENDERABLE TREE. The rows are gone with the
31
34
  // caret they carried: this popup's two `text` fields hold their insertion point
@@ -35,7 +38,6 @@
35
38
  import { columnNode, indentedColumn, textNode } from '../../../../lib/vnode.js';
36
39
  import { wrapToWidth } from '../../../../lib/width.js';
37
40
  import { findOption, renderDeclaredFieldsTree, } from '../../../../Widget/index.js';
38
- import { STATE_FIELD_ID, } from '../../Form/edit.js';
39
41
  const DEFAULT_WIDTH = 58;
40
42
  /** The id the popup's body column is rooted at. */
41
43
  export const EDIT_FORM_REGION = 'edit-form';
@@ -48,24 +50,16 @@ export const EDIT_FORM_REGION = 'edit-form';
48
50
  * again. One number, spent once, in the layout.
49
51
  */
50
52
  const WARNING_INDENT = 2;
51
- /** Four semantically different things in one form, so the form says which is
52
- * which: `assignee_user_id` is human ownership, `conductor_id` is what
53
- * dispatches, and the two durations are the ACCOUNTED time (GAIA-308)not
54
- * the run footprint the Runs tab shows, which the machine measured and which
55
- * is allowed to differ (R6). The duration grammar is named because the popup
56
- * has no validation channel: an unparseable entry is silently left unwritten,
57
- * so the accepted spelling has to be on screen. */
53
+ /** Four assignment/time fields plus structure: `assignee_user_id` is human
54
+ * ownership, `conductor_id` is what dispatches, the two durations are the
55
+ * ACCOUNTED time (GAIA-308) — not the run footprint the Runs tab showsand
56
+ * Parent/Labels are structural (GAIA-360). The duration grammar is named
57
+ * because the popup has no validation channel: an unparseable entry is
58
+ * silently left unwritten, so the accepted spelling has to be on screen. */
58
59
  const LEGEND = 'Assignee = human ownership · Conductor = who works it next · ' +
59
- 'Estimate/Spent = accounted time, not the run footprint (1d 2h 30m, 90m, or a bare number as minutes)';
60
- /**
61
- * GAIA-313 D6 the popup now has two "cancels" and they mean opposite things:
62
- * `Esc` closes the popup, the State row abandons the ticket. The status line
63
- * says `Esc cancel`, so the disambiguation has to be on screen, next to the row
64
- * that caused it. Rendered only when that row is present — a terminal ticket's
65
- * popup would otherwise explain an affordance it does not offer.
66
- */
67
- const STATE_LEGEND = 'State = the only lifecycle move this popup offers is cancelling the ticket · ' +
68
- 'Esc closes the popup and writes nothing';
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 gaia_labels · ' +
62
+ 'Cancel ticket abandons a non-terminal ticket (Esc closes and writes nothing)';
69
63
  /**
70
64
  * A block of chrome prose, wrapped so the frame never has to clip it.
71
65
  *
@@ -109,8 +103,6 @@ export function editFormTree(fields, s, opts, chrome = {}) {
109
103
  rows.push(renderDeclaredFieldsTree(fields, s, opts, { width }));
110
104
  rows.push(textNode({ id: 'edit-form-gap', content: '' }));
111
105
  rows.push(...proseNodes(LEGEND, width, 'edit-form-legend'));
112
- if (fields.some((f) => f.id === STATE_FIELD_ID))
113
- rows.push(...proseNodes(STATE_LEGEND, width, 'edit-form-state-legend'));
114
106
  if (chrome.message !== undefined && chrome.message !== '') {
115
107
  rows.push(...proseNodes(chrome.message, width, 'edit-form-message'));
116
108
  }
@@ -1,5 +1,5 @@
1
1
  import type { OptionClients } from '../../transport.js';
2
- import { type EditFormOptions } from './edit.js';
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 {
@@ -1,14 +1,20 @@
1
1
  // GAIA-236 — the ADAPTER between the data services and the EDIT popup, and a sibling
2
2
  // of `loadCreateFormOptions` rather than a parameterisation of it. A data service
3
- // (`users.ts`, `conductors.ts`) returns data and knows nothing about forms; this
4
- // module knows both sides and therefore owns the mapping to `Option` and this popup's
5
- // policy: read write-client-first, drop Anonymous, filter conductors to the ticket's
6
- // project, put liveness in the label, and do NOT reorder — the popup preselects the
7
- // ticket's current assignee. Each rule has exactly one user, so each is written
8
- // once, here, beside the create adapter it differs from.
3
+ // (`users.ts`, `conductors.ts`, `terms.ts`) returns data and knows nothing about forms;
4
+ // this module knows both sides and therefore owns the mapping to `Option` and this
5
+ // popup's policy: read write-client-first for users/conductors, drop Anonymous, filter
6
+ // conductors to the ticket's project, put liveness in the label, load the
7
+ // `gaia_labels` vocabulary for the Labels multi-select, and do NOT reorder the
8
+ // popup preselects the ticket's current assignee. Each rule has exactly one user,
9
+ // so each is written once, here, beside the create adapter it differs from.
10
+ //
11
+ // GAIA-360: labels bag replaces the constant State select targets. Parent typeahead
12
+ // is an OptionSource on the screen, not a batch bag here.
9
13
  import { listConductorsPreferringWrite, } from '../../conductor/Data/conductors.js';
14
+ import { listTerms } from '../../term/Data/terms.js';
10
15
  import { listUsersPreferringWrite } from '../../user/Data/users.js';
11
- import { STATE_TARGET_OPTIONS } from './edit.js';
16
+ /** The vocabulary this popup offers as its labels (GAIA-360 D9 / D11). */
17
+ const LABEL_VOCABULARY = 'gaia_labels';
12
18
  /**
13
19
  * uid 0 is Drupal's Anonymous and never a valid human assignee. A row whose uid never
14
20
  * loaded is `undefined` — NOT Anonymous — and stays.
@@ -28,15 +34,20 @@ const withStatusSuffix = (c) => ({
28
34
  label: c.status ? `${c.label} (${c.status})` : c.label,
29
35
  });
30
36
  export async function loadEditFormOptions(clients, hints = {}) {
31
- const [users, conductors] = await Promise.all([
37
+ // Labels have no write-preferring helper: terms are not write-gated the way
38
+ // assignees/conductors are. Fail-open to [] when the read client is absent or
39
+ // the vocabulary throws (D11).
40
+ const labelsPromise = clients.read
41
+ ? listTerms(clients.read, LABEL_VOCABULARY).then((outcome) => outcome.rows.map((t) => ({ id: t.id, label: t.label })), () => [])
42
+ : Promise.resolve([]);
43
+ const [users, conductors, labels] = await Promise.all([
32
44
  listUsersPreferringWrite(clients),
33
45
  listConductorsPreferringWrite(clients),
46
+ labelsPromise,
34
47
  ]);
35
48
  return {
36
49
  users: excludeAnonymous(users).map((u) => ({ id: u.id, label: u.label })),
37
50
  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,
51
+ labels,
41
52
  };
42
53
  }
@@ -0,0 +1,8 @@
1
+ import type { DropshClient } from '../../../types.js';
2
+ import type { OptionSource } from '../../../Widget/index.js';
3
+ import type { ParentOption } from './create.js';
4
+ /**
5
+ * Parent candidates for the edit popup: project-scoped, identifier+title match,
6
+ * excluding `ticketId` so an operator cannot parent a ticket to itself.
7
+ */
8
+ export declare function editParentOptionSource(client: DropshClient, projectId: string, ticketId: string): OptionSource<ParentOption>;
@@ -0,0 +1,23 @@
1
+ // GAIA-360 D5/D9 — edit's Parent autocomplete source: the shared
2
+ // `parentOptionSource` (identifier+title match, project scope) with the open
3
+ // ticket filtered out of every load. Self-parent is a client-side footgun;
4
+ // deeper cycle detection stays server-side.
5
+ import { parentOptionSource } from './parent-source.js';
6
+ /**
7
+ * Parent candidates for the edit popup: project-scoped, identifier+title match,
8
+ * excluding `ticketId` so an operator cannot parent a ticket to itself.
9
+ */
10
+ export function editParentOptionSource(client, projectId, ticketId) {
11
+ const base = parentOptionSource(client, projectId, 'identifier+title');
12
+ return {
13
+ bag: base.bag,
14
+ ...(base.debounceMs === undefined ? {} : { debounceMs: base.debounceMs }),
15
+ async load(query) {
16
+ const outcome = await base.load(query);
17
+ return {
18
+ options: outcome.options.filter((row) => row.id !== ticketId),
19
+ ...(outcome.error === undefined ? {} : { error: outcome.error }),
20
+ };
21
+ },
22
+ };
23
+ }
@@ -1,9 +1,10 @@
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' | 'estimate' | 'spent' | 'stateTarget' | 'submit';
4
+ export type EditFieldId = 'assignee' | 'conductor' | 'estimate' | 'spent' | 'parent' | 'labels' | 'submit' | 'cancelTicket';
5
5
  /**
6
- * GAIA-313 — the ONE lifecycle move this popup offers, as a named constant.
6
+ * GAIA-313 / GAIA-360 — the ONE lifecycle move this popup offers, as a named
7
+ * constant.
7
8
  *
8
9
  * Not derived, and deliberately so: `state_flow` is a `gaia_dashboard` computed
9
10
  * field rendering the happy path (`qualification … done`) and never contains
@@ -17,14 +18,13 @@ export type EditFieldId = 'assignee' | 'conductor' | 'estimate' | 'spent' | 'sta
17
18
  * predicate, and no workflow id is needed to tell them apart.
18
19
  */
19
20
  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[];
21
+ /**
22
+ * The Cancel ticket action's field id (GAIA-360 D4).
23
+ *
24
+ * Never `'cancel'` — `intent.kind === 'cancel'` already means "close the
25
+ * popup", and two meanings for one word is a defect waiting.
26
+ */
27
+ export declare const CANCEL_TICKET_ACTION_ID = "cancelTicket";
28
28
  /**
29
29
  * The states no transition leaves, in every ticket workflow.
30
30
  *
@@ -36,9 +36,13 @@ export declare const TERMINAL_TICKET_STATES: ReadonlySet<string>;
36
36
  export interface EditFormOptions {
37
37
  users: Option[];
38
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[];
39
+ labels: Option[];
40
+ /**
41
+ * Parent candidates from the option loader (project-scoped typeahead). Absent
42
+ * until the screen primes a source; an empty array means "searched, nothing
43
+ * matched" once a source is live.
44
+ */
45
+ parents?: Option[] | undefined;
42
46
  }
43
47
  /** What the form HOLDS. `ticketId` is a NON-field value: the kernel carries it
44
48
  * verbatim so submit needs no ambient state, and never transitions it. */
@@ -49,12 +53,12 @@ interface EditValues {
49
53
  /** The human duration as TYPED — seconds only exist at `toObject` (D5). */
50
54
  estimate: string;
51
55
  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;
56
+ /** The PICKED parent's uuid, or null. `parentQuery` holds what was typed. */
57
+ parent: string | null;
58
+ parentQuery: string;
59
+ labels: ReadonlySet<string>;
55
60
  }
56
- /** The payload of the PATCH (GAIA-233 Decision 2's `ResourceWriteBody`, which
57
- * already declares `attributes?`, so the writer needs no change). */
61
+ /** The payload of the ordinary Save PATCH (GAIA-233 Decision 2). */
58
62
  export interface EditFormPayload {
59
63
  attributes: Record<string, number | string | null>;
60
64
  relationships: {
@@ -70,6 +74,24 @@ export interface EditFormPayload {
70
74
  id: string;
71
75
  } | null;
72
76
  };
77
+ labels: {
78
+ data: {
79
+ type: string;
80
+ id: string;
81
+ }[];
82
+ };
83
+ parent_id: {
84
+ data: {
85
+ type: string;
86
+ id: string;
87
+ } | null;
88
+ };
89
+ };
90
+ }
91
+ /** Cancel ticket writes ONLY the lifecycle attribute (GAIA-360 D1). */
92
+ export interface CancelEditPayload {
93
+ attributes: {
94
+ state: typeof CANCELLED_STATE;
73
95
  };
74
96
  }
75
97
  /** The ordinary popup: a non-terminal ticket can be cancelled from it. */
@@ -81,7 +103,7 @@ export declare const EDIT_FORM: {
81
103
  apply(s: FormState<EditValues>, options: EditFormOptions, intent: import("../../../Form/form.js").Intent): FormState<EditValues>;
82
104
  toObject: (s: FormState<EditValues>, options: EditFormOptions) => EditFormPayload;
83
105
  };
84
- /** A terminal ticket's popup: assignment and time only. */
106
+ /** A terminal ticket's popup: assignment, times, structure — no Cancel ticket. */
85
107
  export declare const EDIT_FORM_TERMINAL: {
86
108
  fields: import("../../../Form/field.js").FieldDecl[];
87
109
  activeField: (s: FormState<EditValues>) => import("../../../Form/field.js").FieldDecl;
@@ -94,12 +116,12 @@ export type EditFormDeclaration = typeof EDIT_FORM;
94
116
  /** The ticket's state as the popup sees it; `''` when the read carried none. */
95
117
  export declare const ticketState: (document: TicketDetailDocument) => string;
96
118
  /**
97
- * Which declaration a ticket gets (AC-4).
119
+ * Which declaration a ticket gets (AC-4 / D13).
98
120
  *
99
- * An unknown state fails OPEN (D7): `loadTicketDetail` requests no fieldset, so
121
+ * An unknown state fails OPEN (D13): `loadTicketDetail` requests no fieldset, so
100
122
  * 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.
123
+ * Cancel ticket on a thin read would cost a legitimate affordance; offering it
124
+ * costs at worst the server's 422, which the popup already renders in full.
103
125
  */
104
126
  export declare const editFormFor: (document: TicketDetailDocument) => EditFormDeclaration;
105
127
  /** Field order in the popup, per declaration. */
@@ -107,26 +129,30 @@ export declare const EDIT_FIELDS: FieldDef<EditFieldId>[];
107
129
  export declare const EDIT_FIELDS_TERMINAL: FieldDef<EditFieldId>[];
108
130
  export type EditFormState = FormState<EditValues>;
109
131
  /**
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).
132
+ * No lists to OFFER — bags start empty until the adapter / option loader fills
133
+ * them. Prefill names every value, and `clear`/`toObject` read no option list
134
+ * for the required fields, so adapters that have no options hand over this bag.
135
+ * Exported so the screen's options-failure path can reuse it (D11): a failed
136
+ * user/conductor/label read must not silently remove Cancel ticket (D8 keeps
137
+ * the action on the declaration, not in the bag).
118
138
  */
119
139
  export declare const EMPTY_EDIT_FORM_OPTIONS: EditFormOptions;
120
140
  /** Prefill from the ticket's CURRENT values (AC-1) — never from `opts[0]`. The
121
141
  * kernel applies a prefill with `Object.hasOwn`, so an unassigned ticket's
122
142
  * explicit `null` STICKS instead of falling back to the first option. */
123
143
  export declare function initialEditForm(document: TicketDetailDocument): EditFormState;
144
+ /**
145
+ * The Cancel ticket PATCH body (GAIA-360 D1): lifecycle only.
146
+ *
147
+ * Local field edits are discarded; after a successful write the detail reloads
148
+ * from the server (same as Save's reload path). No relationships.
149
+ */
150
+ export declare const cancelEditPayload: () => CancelEditPayload;
124
151
  export declare const activeEditField: (s: EditFormState) => FieldDef<EditFieldId>;
125
152
  export declare const moveEditField: (s: EditFormState, delta: number) => EditFormState;
126
153
  /** Cycle the active select by `dir`, `null` included (AC-4). */
127
154
  export declare const cycleEdit: (s: EditFormState, opts: EditFormOptions, dir: 1 | -1) => EditFormState;
128
- /** Clear the active select outright (the Del/Backspace shortcut). Cycling to `null`
129
- * already works; a long conductor list just makes it tedious. */
155
+ /** Clear the active select/autocomplete outright (the Del/Backspace shortcut). */
130
156
  export declare const clearEditValue: (s: EditFormState) => EditFormState;
131
157
  export declare const serializeEdit: (s: EditFormState) => EditFormPayload;
132
158
  export {};
@@ -1,17 +1,20 @@
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.
1
+ // The "Edit ticket" popup (GAIA-233, widened by GAIA-308 / GAIA-313 / GAIA-360)
2
+ // as a DECLARATION over the form kernel (GAIA-236): two `clearable` selects, two
3
+ // text fields for accounted durations, a clearable Parent autocomplete, a Labels
4
+ // multi-select, and dual actions (Save + Cancel ticket on non-terminal). One
5
+ // `toObject` yields the ordinary Save PATCH; Cancel ticket uses a separate pure
6
+ // payload helper. No I/O here.
8
7
  //
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.
8
+ // GAIA-360 removes the State select entirely (D8). Cancelling is a second
9
+ // `f.action` whose id is never `cancel` that intent closes the popup (Esc).
10
+ // Two declarations from one body: a terminal ticket's popup simply omits the
11
+ // Cancel ticket action (D3), and `editFormFor` picks between them.
11
12
  import { declareForm } from '../../../Form/index.js';
12
13
  import { formatDuration, parseDuration } from '../../../lib/format.js';
14
+ import { PARENT_PAGE_SIZE } from '../Data/parents.js';
13
15
  /**
14
- * GAIA-313 — the ONE lifecycle move this popup offers, as a named constant.
16
+ * GAIA-313 / GAIA-360 — the ONE lifecycle move this popup offers, as a named
17
+ * constant.
15
18
  *
16
19
  * Not derived, and deliberately so: `state_flow` is a `gaia_dashboard` computed
17
20
  * field rendering the happy path (`qualification … done`) and never contains
@@ -25,17 +28,13 @@ import { formatDuration, parseDuration } from '../../../lib/format.js';
25
28
  * predicate, and no workflow id is needed to tell them apart.
26
29
  */
27
30
  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
- ];
31
+ /**
32
+ * The Cancel ticket action's field id (GAIA-360 D4).
33
+ *
34
+ * Never `'cancel'` — `intent.kind === 'cancel'` already means "close the
35
+ * popup", and two meanings for one word is a defect waiting.
36
+ */
37
+ export const CANCEL_TICKET_ACTION_ID = 'cancelTicket';
39
38
  /**
40
39
  * The states no transition leaves, in every ticket workflow.
41
40
  *
@@ -50,6 +49,10 @@ export const TERMINAL_TICKET_STATES = new Set([
50
49
  const linkage = (type, id) => ({
51
50
  data: id === null ? null : { type, id },
52
51
  });
52
+ /** Multi-value term linkage for labels (always present on ordinary Save). */
53
+ const labelsLinkage = (ids) => ({
54
+ data: [...ids].map((id) => ({ type: 'gaia_term--gaia_labels', id })),
55
+ });
53
56
  /**
54
57
  * The two time attributes of the PATCH, in seconds.
55
58
  *
@@ -64,7 +67,7 @@ const linkage = (type, id) => ({
64
67
  * keeps what it had and the operator sees their typo survive
65
68
  * the save.
66
69
  *
67
- * This is why `toObject` cannot emit both keys unconditionally the way the two
70
+ * This is why `toObject` cannot emit both keys unconditionally the way the
68
71
  * relationships do.
69
72
  */
70
73
  const timeAttributes = (v) => {
@@ -82,31 +85,22 @@ const timeAttributes = (v) => {
82
85
  return out;
83
86
  };
84
87
  /**
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).
88
+ * One body, two declarations (D3 / D8).
96
89
  *
97
90
  * 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.
91
+ * not only changing it, so `null` is a real position in each cycle. Parent is
92
+ * clearable for the same reason (D7). Labels always PATCH the full selected set,
93
+ * including empty.
99
94
  *
100
- * `toObject` always emits BOTH relationships, even when neither changed: "only send
101
- * what changed" is a rule the operator cannot read off the screen, and an explicit
102
- * no-op PATCH is what the popup visibly promises. The two time attributes are the
103
- * documented exception — see `timeAttributes`.
95
+ * Ordinary Save NEVER includes `attributes.state` a self-transition is not a
96
+ * declared edge, so echoing the current state would turn every ordinary edit
97
+ * into a 422. Cancel ticket is a separate action that uses `cancelEditPayload`.
104
98
  *
105
99
  * The kernel builds its field list once, at module load, and every consumer —
106
100
  * the renderer's `▸` marker, `keyToIntent`, `apply` — indexes that array by
107
101
  * `state.active`. So a row cannot be hidden at render time without shifting
108
102
  * 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.
103
+ * force.
110
104
  */
111
105
  const declareEditForm = (cancellable) => declareForm({
112
106
  fields: (f) => [
@@ -117,25 +111,33 @@ const declareEditForm = (cancellable) => declareForm({
117
111
  }),
118
112
  f.text('estimate', 'Estimate'),
119
113
  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.
114
+ f.autocomplete('parent', 'Parent', {
115
+ from: 'parents',
116
+ query: 'parentQuery',
117
+ clearable: true,
118
+ absentNote: '(no project context — open a project to pick a parent)',
119
+ pageSize: PARENT_PAGE_SIZE,
120
+ }),
121
+ f.multi('labels', 'Labels', { from: 'labels' }),
122
+ f.action('Save', 'submit'),
123
123
  ...(cancellable
124
- ? [f.select('stateTarget', 'State', { from: 'stateTargets' })]
124
+ ? [f.action('Cancel ticket', CANCEL_TICKET_ACTION_ID)]
125
125
  : []),
126
- f.action('Save'),
127
126
  ],
128
127
  toObject: (v) => ({
129
- attributes: { ...timeAttributes(v), ...stateAttribute(v) },
128
+ // Ordinary Save never writes lifecycle state (D6/D7).
129
+ attributes: { ...timeAttributes(v) },
130
130
  relationships: {
131
131
  assignee_user_id: linkage('user--user', v.assignee),
132
132
  conductor_id: linkage('gaia_conductor--gaia_conductor', v.conductor),
133
+ labels: labelsLinkage(v.labels),
134
+ parent_id: linkage('gaia_ticket--gaia_ticket', v.parent),
133
135
  },
134
136
  }),
135
137
  });
136
138
  /** The ordinary popup: a non-terminal ticket can be cancelled from it. */
137
139
  export const EDIT_FORM = declareEditForm(true);
138
- /** A terminal ticket's popup: assignment and time only. */
140
+ /** A terminal ticket's popup: assignment, times, structure — no Cancel ticket. */
139
141
  export const EDIT_FORM_TERMINAL = declareEditForm(false);
140
142
  /** The ticket's state as the popup sees it; `''` when the read carried none. */
141
143
  export const ticketState = (document) => {
@@ -143,12 +145,12 @@ export const ticketState = (document) => {
143
145
  return typeof value === 'string' ? value : '';
144
146
  };
145
147
  /**
146
- * Which declaration a ticket gets (AC-4).
148
+ * Which declaration a ticket gets (AC-4 / D13).
147
149
  *
148
- * An unknown state fails OPEN (D7): `loadTicketDetail` requests no fieldset, so
150
+ * An unknown state fails OPEN (D13): `loadTicketDetail` requests no fieldset, so
149
151
  * 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
+ * Cancel ticket on a thin read would cost a legitimate affordance; offering it
153
+ * costs at worst the server's 422, which the popup already renders in full.
152
154
  */
153
155
  export const editFormFor = (document) => TERMINAL_TICKET_STATES.has(ticketState(document))
154
156
  ? EDIT_FORM_TERMINAL
@@ -157,19 +159,17 @@ export const editFormFor = (document) => TERMINAL_TICKET_STATES.has(ticketState(
157
159
  export const EDIT_FIELDS = EDIT_FORM.fields;
158
160
  export const EDIT_FIELDS_TERMINAL = EDIT_FORM_TERMINAL.fields;
159
161
  /**
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).
162
+ * No lists to OFFER — bags start empty until the adapter / option loader fills
163
+ * them. Prefill names every value, and `clear`/`toObject` read no option list
164
+ * for the required fields, so adapters that have no options hand over this bag.
165
+ * Exported so the screen's options-failure path can reuse it (D11): a failed
166
+ * user/conductor/label read must not silently remove Cancel ticket (D8 keeps
167
+ * the action on the declaration, not in the bag).
168
168
  */
169
169
  export const EMPTY_EDIT_FORM_OPTIONS = {
170
170
  users: [],
171
171
  conductors: [],
172
- stateTargets: STATE_TARGET_OPTIONS,
172
+ labels: [],
173
173
  };
174
174
  const relId = (document, name) => {
175
175
  const data = document.data?.relationships?.[name]?.data;
@@ -177,32 +177,71 @@ const relId = (document, name) => {
177
177
  return null;
178
178
  return Array.isArray(data) ? (data[0]?.id ?? null) : data.id;
179
179
  };
180
+ const labelIds = (document) => {
181
+ const data = document.data?.relationships?.labels?.data;
182
+ if (!data)
183
+ return new Set();
184
+ if (Array.isArray(data)) {
185
+ return new Set(data.map((row) => row.id).filter(Boolean));
186
+ }
187
+ return data.id ? new Set([data.id]) : new Set();
188
+ };
180
189
  const timeValue = (document, name) => {
181
190
  const value = document.data?.attributes?.[name];
182
191
  // An unset field prefills EMPTY, never `0m`: the popup must not turn "nobody
183
192
  // estimated this" into a typed zero the operator then saves by accident.
184
193
  return typeof value === 'number' ? formatDuration(value) : '';
185
194
  };
195
+ /**
196
+ * Parent query text from the detail include when present (D9).
197
+ *
198
+ * A known parent must not blank when absent from the first candidate page — the
199
+ * id is always set from the relationship; the query is best-effort display
200
+ * material from the sideload. Missing include ⇒ empty query, id still set.
201
+ */
202
+ const parentQueryFromDocument = (document, parentId) => {
203
+ if (!parentId)
204
+ return '';
205
+ const hit = document.included?.find((row) => row.id === parentId && row.type === 'gaia_ticket--gaia_ticket');
206
+ if (!hit)
207
+ return '';
208
+ const attrs = hit.attributes ?? {};
209
+ const identifier = typeof attrs.identifier === 'string' ? attrs.identifier : '';
210
+ const title = typeof attrs.title === 'string' ? attrs.title : '';
211
+ if (!identifier && !title)
212
+ return '';
213
+ return [identifier, title].filter(Boolean).join(' · ');
214
+ };
186
215
  /** Prefill from the ticket's CURRENT values (AC-1) — never from `opts[0]`. The
187
216
  * kernel applies a prefill with `Object.hasOwn`, so an unassigned ticket's
188
217
  * explicit `null` STICKS instead of falling back to the first option. */
189
218
  export function initialEditForm(document) {
219
+ const parent = relId(document, 'parent_id');
190
220
  return EDIT_FORM.initial(EMPTY_EDIT_FORM_OPTIONS, {
191
221
  ticketId: document.data?.id ?? '',
192
222
  assignee: relId(document, 'assignee_user_id'),
193
223
  conductor: relId(document, 'conductor_id'),
194
224
  estimate: timeValue(document, 'time_estimate'),
195
225
  spent: timeValue(document, 'time_spent'),
196
- // Always KEEP, on both declarations: the popup never opens armed to cancel.
197
- stateTarget: KEEP_STATE,
226
+ parent,
227
+ parentQuery: parentQueryFromDocument(document, parent),
228
+ labels: labelIds(document),
198
229
  });
199
230
  }
231
+ /**
232
+ * The Cancel ticket PATCH body (GAIA-360 D1): lifecycle only.
233
+ *
234
+ * Local field edits are discarded; after a successful write the detail reloads
235
+ * from the server (same as Save's reload path). No relationships.
236
+ */
237
+ export const cancelEditPayload = () => ({
238
+ attributes: { state: CANCELLED_STATE },
239
+ });
200
240
  // ---- thin adapters over the kernel (the popup itself runs on keyToIntent) ----
201
241
  export const activeEditField = (s) => EDIT_FORM.activeField(s);
202
242
  export const moveEditField = (s, delta) => EDIT_FORM.move(s, delta);
203
243
  /** Cycle the active select by `dir`, `null` included (AC-4). */
204
244
  export const cycleEdit = (s, opts, dir) => EDIT_FORM.apply(s, opts, { kind: 'cycle', dir });
205
- /** Clear the active select outright (the Del/Backspace shortcut). Cycling to `null`
206
- * already works; a long conductor list just makes it tedious. */
245
+ /** Clear the active select/autocomplete outright (the Del/Backspace shortcut). */
207
246
  export const clearEditValue = (s) => EDIT_FORM.apply(s, EMPTY_EDIT_FORM_OPTIONS, { kind: 'clear' });
208
247
  export const serializeEdit = (s) => EDIT_FORM.toObject(s, EMPTY_EDIT_FORM_OPTIONS);
@@ -27,7 +27,7 @@ export declare class EditAssignmentFormScreen extends AsyncScreen<EditAssignment
27
27
  };
28
28
  readonly id: string;
29
29
  /**
30
- * The declaration THIS ticket's popup runs on (GAIA-313 D3).
30
+ * The declaration THIS ticket's popup runs on (GAIA-313 D3 / GAIA-360 D8).
31
31
  *
32
32
  * Chosen once, in `didLoad`, and used for BOTH the rendered rows and the key
33
33
  * routing — the kernel indexes its field array by `state.active`, so two
@@ -41,6 +41,9 @@ export declare class EditAssignmentFormScreen extends AsyncScreen<EditAssignment
41
41
  private action;
42
42
  private message;
43
43
  private submitting;
44
+ private context;
45
+ private sources;
46
+ private optionLoader;
44
47
  get options(): EditFormOptions | undefined;
45
48
  protected load(context: ScreenContext): Promise<EditAssignmentData>;
46
49
  protected didLoad(data: EditAssignmentData): void;
@@ -68,22 +71,9 @@ export declare class EditAssignmentFormScreen extends AsyncScreen<EditAssignment
68
71
  * own input context is declared `role: 'input'`, which `scopeKey()` filters
69
72
  * out. So without this the second open reuses the first open's `form` and
70
73
  * `declaration` — and GAIA-313 put the first LIFECYCLE-MUTATING value into that
71
- * reused state: an armed `cancelled` survived `Esc` and rode the next Save, and
72
- * a popup reopened on a just-cancelled ticket still offered the transition the
73
- * server would now reject.
74
- *
75
- * Overridden HERE and not on `AsyncScreen`, deliberately: the sibling modals
76
- * (create-ticket, parent-filter, who-filter) share the caching shape but carry
77
- * no value that changes the ticket's lifecycle, and dropping every screen's
78
- * document on `leave` would re-read on every back-navigation in the cockpit.
79
- * The cost here is one detail read and one options read per open — what a modal
80
- * editing form should pay.
81
- *
82
- * Safe at every call site: `Router.navigate` calls `leave()` and then re-points
83
- * `location` before anything draws, so the reset screen (whose `renderLoading`
84
- * is `Loading assignment…`) is never rendered; and `submit()`'s trailing
85
- * `.finally` only re-writes `submitting = false`, which `reset()` has already
86
- * set, on a context whose `isActive()` is false by then.
74
+ * reused state. Overridden HERE and not on `AsyncScreen`, deliberately: the
75
+ * sibling modals share the caching shape but carry no lifecycle-mutating
76
+ * residual that must die with the popup.
87
77
  */
88
78
  leave(): void;
89
79
  reset(): void;
@@ -3,11 +3,13 @@ import { columnNode } from '../../../lib/vnode.js';
3
3
  import { defineContext } from '../../../Routing/context.js';
4
4
  import { AsyncScreen } from '../../../Screen/async-screen.js';
5
5
  import { presentation, } from '../../../Screen/screen.js';
6
+ import { createOptionLoader, } from '../../../Widget/option-source.js';
6
7
  import { activeRunInfo } from '../../run/Data/active-run.js';
7
8
  import { EDIT_FORM_REGION, editFormTree, } from '../Component/edit-form/index.js';
8
9
  import { loadTicketDetail } from '../Data/detail.js';
9
- import { EDIT_FORM_TERMINAL, EMPTY_EDIT_FORM_OPTIONS, editFormFor, initialEditForm, serializeEdit, } from '../Form/edit.js';
10
+ import { CANCEL_TICKET_ACTION_ID, cancelEditPayload, EDIT_FORM_TERMINAL, EMPTY_EDIT_FORM_OPTIONS, editFormFor, initialEditForm, serializeEdit, } from '../Form/edit.js';
10
11
  import { loadEditFormOptions } from '../Form/edit-data.js';
12
+ import { editParentOptionSource } from '../Form/edit-parent-source.js';
11
13
  import { keyToIntent } from '../Form/keys.js';
12
14
  export const editAssignmentInputContext = defineContext('edit-assignment-input', 'input');
13
15
  const relationshipId = (document, name) => {
@@ -35,7 +37,7 @@ export class EditAssignmentFormScreen extends AsyncScreen {
35
37
  };
36
38
  id = EditAssignmentFormScreen.route.id;
37
39
  /**
38
- * The declaration THIS ticket's popup runs on (GAIA-313 D3).
40
+ * The declaration THIS ticket's popup runs on (GAIA-313 D3 / GAIA-360 D8).
39
41
  *
40
42
  * Chosen once, in `didLoad`, and used for BOTH the rendered rows and the key
41
43
  * routing — the kernel indexes its field array by `state.active`, so two
@@ -49,10 +51,14 @@ export class EditAssignmentFormScreen extends AsyncScreen {
49
51
  action;
50
52
  message = '';
51
53
  submitting = false;
54
+ context;
55
+ sources = {};
56
+ optionLoader;
52
57
  get options() {
53
58
  return this.readyValue?.options;
54
59
  }
55
60
  async load(context) {
61
+ this.context = context;
56
62
  const ticketId = context.params?.ticketId;
57
63
  if (!context.services || !ticketId)
58
64
  throw new Error('Assignment form requires ticketId and services');
@@ -68,11 +74,38 @@ export class EditAssignmentFormScreen extends AsyncScreen {
68
74
  ? {}
69
75
  : { write: context.services.writeClient }),
70
76
  }, ...(projectId === undefined ? [] : [{ projectId }])).catch(() => EMPTY_EDIT_FORM_OPTIONS);
77
+ // Parent typeahead: project-scoped source with self-exclusion (D5/D11).
78
+ // No project ⇒ no source, so the bag stays absent and the field shows
79
+ // its absentNote. Built as one object: OptionSources is Readonly and
80
+ // does not permit property assignment after construction.
81
+ this.sources =
82
+ projectId === undefined
83
+ ? {}
84
+ : {
85
+ parent: editParentOptionSource(context.services.client, projectId, ticketId),
86
+ };
71
87
  return { document, options };
72
88
  }
73
89
  didLoad(data) {
74
90
  this.declaration = editFormFor(data.document);
75
91
  this.form = initialEditForm(data.document);
92
+ this.optionLoader = createOptionLoader({
93
+ sources: this.sources,
94
+ ...(this.context?.options?.debounce
95
+ ? { debounce: this.context.options.debounce }
96
+ : {}),
97
+ current: () => this.readyValue?.options ?? null,
98
+ onLoaded: (next) => {
99
+ const ready = this.readyValue;
100
+ if (!ready)
101
+ return;
102
+ this.replaceReady({ ...ready, options: next });
103
+ this.context?.requestDraw();
104
+ },
105
+ });
106
+ // Prime parent candidates with the empty query so ←/→ browses before typing.
107
+ if (this.sources.parent)
108
+ this.optionLoader.reload('parent', '');
76
109
  }
77
110
  renderLoading() {
78
111
  return ['Loading assignment…'];
@@ -107,14 +140,22 @@ export class EditAssignmentFormScreen extends AsyncScreen {
107
140
  this.action = undefined;
108
141
  return action;
109
142
  }
110
- submit(context, data) {
143
+ submit(context, data,
144
+ /** When true, Cancel ticket path (lifecycle-only). */
145
+ cancelTicket) {
111
146
  if (this.submitting || !this.form || !context.services)
112
147
  return;
113
148
  this.submitting = true;
114
149
  this.message = '';
115
150
  const ticketId = data.document.data?.id ?? context.params?.ticketId ?? '';
116
- const payload = serializeEdit(this.form);
117
- const assigneeId = payload.relationships.assignee_user_id.data?.id ?? '';
151
+ const payload = cancelTicket
152
+ ? cancelEditPayload()
153
+ : serializeEdit(this.form);
154
+ const assigneeId = cancelTicket
155
+ ? (relationshipId(data.document, 'assignee_user_id') ?? '')
156
+ : 'relationships' in payload
157
+ ? (payload.relationships.assignee_user_id.data?.id ?? '')
158
+ : '';
118
159
  void (context.services.writeClient ?? context.services.client)
119
160
  .update('gaia_ticket--gaia_ticket', ticketId, payload)
120
161
  .then(async () => {
@@ -147,20 +188,37 @@ export class EditAssignmentFormScreen extends AsyncScreen {
147
188
  const data = this.readyValue;
148
189
  if (!data || !this.form)
149
190
  return false;
191
+ const field = this.declaration.activeField(this.form);
150
192
  // GAIA-300 D3 — the field count is passed so the kernel can scope ↑/↓
151
193
  // suggestion-browsing to single-field forms; this form has several, so
152
194
  // its ↑/↓ behaviour is unchanged.
153
- const intent = keyToIntent(input, this.declaration.activeField(this.form), {
195
+ const intent = keyToIntent(input, field, {
154
196
  fieldCount: this.declaration.fields.length,
197
+ ...(field.query !== undefined
198
+ ? {
199
+ query: String(this.form[field.query] ??
200
+ ''),
201
+ }
202
+ : {}),
155
203
  });
156
204
  if (!intent)
157
205
  return true;
158
206
  if (intent.kind === 'cancel')
159
207
  this.action = { type: 'back' };
160
- else if (intent.kind === 'submit')
161
- this.submit(context, data);
162
- else
208
+ else if (intent.kind === 'submit') {
209
+ // D10: Ctrl+S ALWAYS Save, even when the cursor is on Cancel ticket.
210
+ // Only Enter/submit while standing on the cancelTicket action cancels.
211
+ const cancelTicket = input !== 'ctrl+s' && field.id === CANCEL_TICKET_ACTION_ID;
212
+ this.submit(context, data, cancelTicket);
213
+ }
214
+ else {
163
215
  this.form = this.declaration.apply(this.form, data.options, intent);
216
+ if (field.query !== undefined &&
217
+ (intent.kind === 'insert' || intent.kind === 'backspace')) {
218
+ const query = this.form[field.query];
219
+ this.optionLoader?.reload(field.id, typeof query === 'string' ? query : '');
220
+ }
221
+ }
164
222
  context.requestDraw();
165
223
  return true;
166
224
  }
@@ -173,22 +231,9 @@ export class EditAssignmentFormScreen extends AsyncScreen {
173
231
  * own input context is declared `role: 'input'`, which `scopeKey()` filters
174
232
  * out. So without this the second open reuses the first open's `form` and
175
233
  * `declaration` — and GAIA-313 put the first LIFECYCLE-MUTATING value into that
176
- * reused state: an armed `cancelled` survived `Esc` and rode the next Save, and
177
- * a popup reopened on a just-cancelled ticket still offered the transition the
178
- * server would now reject.
179
- *
180
- * Overridden HERE and not on `AsyncScreen`, deliberately: the sibling modals
181
- * (create-ticket, parent-filter, who-filter) share the caching shape but carry
182
- * no value that changes the ticket's lifecycle, and dropping every screen's
183
- * document on `leave` would re-read on every back-navigation in the cockpit.
184
- * The cost here is one detail read and one options read per open — what a modal
185
- * editing form should pay.
186
- *
187
- * Safe at every call site: `Router.navigate` calls `leave()` and then re-points
188
- * `location` before anything draws, so the reset screen (whose `renderLoading`
189
- * is `Loading assignment…`) is never rendered; and `submit()`'s trailing
190
- * `.finally` only re-writes `submitting = false`, which `reset()` has already
191
- * set, on a context whose `isActive()` is false by then.
234
+ * reused state. Overridden HERE and not on `AsyncScreen`, deliberately: the
235
+ * sibling modals share the caching shape but carry no lifecycle-mutating
236
+ * residual that must die with the popup.
192
237
  */
193
238
  leave() {
194
239
  this.reset();
@@ -200,6 +245,10 @@ export class EditAssignmentFormScreen extends AsyncScreen {
200
245
  this.action = undefined;
201
246
  this.message = '';
202
247
  this.submitting = false;
248
+ this.optionLoader?.cancel();
249
+ this.optionLoader = undefined;
250
+ this.sources = {};
251
+ this.context = undefined;
203
252
  super.reset();
204
253
  }
205
254
  present(context) {
@@ -210,7 +259,9 @@ export class EditAssignmentFormScreen extends AsyncScreen {
210
259
  meta: String(this.readyValue?.document.data?.attributes?.identifier ??
211
260
  context.params?.ticketId ??
212
261
  ''),
213
- status: ' ↑↓ field · ←→ change · Ctrl+S save · Esc cancel ',
262
+ // GAIA-360 D12 Esc closes the popup; Cancel ticket is the lifecycle
263
+ // action. Status must not say "Esc cancel".
264
+ status: ' ↑↓ field · ←→ change · Ctrl+S save · Esc close ',
214
265
  // The popup's box supplies its own border and padding, so the body it
215
266
  // holds must not also carry the body window's inset — see
216
267
  // `presentation()`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaia-ai/addon-gaia-ui",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "GAIA TUI: interactive terminal UI (dashboard, projects, tickets) as a dropsh `tui` renderer plugin.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,6 +25,6 @@
25
25
  "drupal-jsonapi-params": "^3.0.1"
26
26
  },
27
27
  "peerDependencies": {
28
- "@gaia-ai/core": "^0.9.0"
28
+ "@gaia-ai/core": "^0.9.2"
29
29
  }
30
30
  }