@aquera/mcp-ui-render 0.0.1
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/CHANGELOG.md +437 -0
- package/CONFIGURATION.md +178 -0
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/RELEASE-NOTES.md +77 -0
- package/TEST-PLAN.md +72 -0
- package/USAGE.md +100 -0
- package/package.json +52 -0
- package/src/controls.d.ts +1 -0
- package/src/controls.js +572 -0
- package/src/controls.js.map +1 -0
- package/src/elements/aq-mcp-config.d.ts +289 -0
- package/src/elements/aq-mcp-config.js +673 -0
- package/src/elements/aq-mcp-config.js.map +1 -0
- package/src/elements/aq-mcp-field.d.ts +92 -0
- package/src/elements/aq-mcp-field.js +275 -0
- package/src/elements/aq-mcp-field.js.map +1 -0
- package/src/elements/aq-mcp-section.d.ts +101 -0
- package/src/elements/aq-mcp-section.js +261 -0
- package/src/elements/aq-mcp-section.js.map +1 -0
- package/src/engine/status.d.ts +31 -0
- package/src/engine/status.js +44 -0
- package/src/engine/status.js.map +1 -0
- package/src/engine/submit.d.ts +26 -0
- package/src/engine/submit.js +37 -0
- package/src/engine/submit.js.map +1 -0
- package/src/engine/validate.d.ts +24 -0
- package/src/engine/validate.js +144 -0
- package/src/engine/validate.js.map +1 -0
- package/src/engine/values.d.ts +20 -0
- package/src/engine/values.js +42 -0
- package/src/engine/values.js.map +1 -0
- package/src/index.d.ts +18 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/mcp-ui-render.css +811 -0
- package/src/registry.d.ts +63 -0
- package/src/registry.js +39 -0
- package/src/registry.js.map +1 -0
- package/src/types.d.ts +239 -0
- package/src/types.js +8 -0
- package/src/types.js.map +1 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
var AqMcpConfig_1;
|
|
2
|
+
import { __decorate, __metadata } from "tslib";
|
|
3
|
+
/**
|
|
4
|
+
* `<aq-mcp-config>` — root element. Owns the descriptor + values + validation
|
|
5
|
+
* state, renders ordered sections (§12), and emits intent for the host:
|
|
6
|
+
* - `aq-mcp-change` — on any field edit `{ attribute, value, values }`
|
|
7
|
+
* - `aq-mcp-submit` — per-section save `{ sectionKey, submitTool, values }` (§13/§14)
|
|
8
|
+
* - `aq-mcp-action` — button/action `{ tool, args, confirm, field, action }` (§5).
|
|
9
|
+
* `action` is the specific `FieldAction` clicked for a multi-action control's
|
|
10
|
+
* own toolbar (e.g. `properties-table`'s "Download"/"Edit Configuration"),
|
|
11
|
+
* undefined for the classic single-action `button` control.
|
|
12
|
+
* - `aq-mcp-options-request` — a `select`/`radio` field with `tool` set has no
|
|
13
|
+
* options yet `{ attribute, tool, args }` (§9.6); host resolves + feeds them
|
|
14
|
+
* back via the `dynamicOptions` property (see below), one request per attribute.
|
|
15
|
+
* It performs NO network I/O and holds no secrets (D-4): it renders, validates,
|
|
16
|
+
* and emits; the host executes tools. A host driving its OWN "submit
|
|
17
|
+
* everything" button (bypassing per-section footers) can call the public
|
|
18
|
+
* `validateAll()` method directly on the element to get the library's real
|
|
19
|
+
* validation + inline field-error display instead of just the raw `values`.
|
|
20
|
+
* Every field is also validated live on its own edits (`onFieldInput`), not
|
|
21
|
+
* just on submit — its error updates the instant its value changes.
|
|
22
|
+
* `sectionsLayout` ('stacked' default, or 'tabs' with `tabsPlacement` for
|
|
23
|
+
* orientation) controls how sections are arranged — everything else
|
|
24
|
+
* (validation, submit, footers) behaves identically in either layout.
|
|
25
|
+
* `tabsShowErrorIndicator` (default true, tabs mode only) puts a red dot on a
|
|
26
|
+
* tab whose section has a required-but-empty or invalid field, computed live
|
|
27
|
+
* — not just after a failed submit.
|
|
28
|
+
*
|
|
29
|
+
* Renders into LIGHT DOM (`createRenderRoot()` returns `this`, no shadow
|
|
30
|
+
* root) on all three elements in this package, deliberately: it lets Nile's
|
|
31
|
+
* CSS custom-property tokens (`--ng-*`/`--nile-*`) and `mcp-ui-render.css`
|
|
32
|
+
* cascade in from the host's own stylesheet the same way any other page
|
|
33
|
+
* content does, with no per-shadow-root style injection needed. The
|
|
34
|
+
* trade-off is zero style encapsulation — a host's global CSS can affect
|
|
35
|
+
* `.aq-mcp-*` classes and vice versa — and it means `mcp-ui-render.css` MUST
|
|
36
|
+
* be loaded globally by the host; without it these elements render
|
|
37
|
+
* structurally correct but entirely unstyled, with no error or warning
|
|
38
|
+
* (see USAGE.md "Styling & light DOM").
|
|
39
|
+
*/
|
|
40
|
+
import { LitElement, html } from 'lit';
|
|
41
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
42
|
+
import { fieldApplies, seedDefaults } from '../engine/values.js';
|
|
43
|
+
import { validateField } from '../engine/validate.js';
|
|
44
|
+
import { collectSubmit } from '../engine/submit.js';
|
|
45
|
+
import { sectionStatus } from '../engine/status.js';
|
|
46
|
+
import './aq-mcp-section.js';
|
|
47
|
+
/**
|
|
48
|
+
* Header glyphs for the section keys the bootstrap contract itself defines
|
|
49
|
+
* (§2): `authorization` is who the connector signs in AS, `tenantAttributes`
|
|
50
|
+
* is where it reads and writes. Any other key falls through to
|
|
51
|
+
* `FALLBACK_SECTION_ICON`, so an unknown section is never iconless, and a
|
|
52
|
+
* host can override any of it via `sectionIcons`.
|
|
53
|
+
*/
|
|
54
|
+
const DEFAULT_SECTION_ICONS = {
|
|
55
|
+
authorization: 'ng-user-01',
|
|
56
|
+
tenantAttributes: 'ng-server',
|
|
57
|
+
};
|
|
58
|
+
const FALLBACK_SECTION_ICON = 'ng-settings-01';
|
|
59
|
+
/** Highest descriptor contract MAJOR this build understands (§9.5). */
|
|
60
|
+
const SUPPORTED_MAJOR = 1;
|
|
61
|
+
/** Parse the MAJOR from a `MAJOR.MINOR` version string; NaN when malformed. */
|
|
62
|
+
function majorOf(version) {
|
|
63
|
+
return Number.parseInt(String(version ?? '').split('.')[0], 10);
|
|
64
|
+
}
|
|
65
|
+
let AqMcpConfig = AqMcpConfig_1 = class AqMcpConfig extends LitElement {
|
|
66
|
+
constructor() {
|
|
67
|
+
super(...arguments);
|
|
68
|
+
/** The `bootstrap` response to render. */
|
|
69
|
+
this.descriptor = null;
|
|
70
|
+
/** Current values keyed by `attribute`. */
|
|
71
|
+
this.values = {};
|
|
72
|
+
/**
|
|
73
|
+
* Host-fed options for `select`/`radio` fields whose list is resolved dynamically
|
|
74
|
+
* (`field.tool` set, e.g. `listFtpServers`) rather than declared statically as
|
|
75
|
+
* `field.options` (§9.6) — keyed by `field.attribute`. This library performs no
|
|
76
|
+
* network I/O itself (D-4): it only ASKS, via `aq-mcp-options-request`; the host
|
|
77
|
+
* calls the tool and sets the result back in here (immutably — a new object, not
|
|
78
|
+
* a mutated one, so Lit's property-changed check fires):
|
|
79
|
+
*
|
|
80
|
+
* el.addEventListener('aq-mcp-options-request', async (e) => {
|
|
81
|
+
* const { attribute, tool, args } = e.detail;
|
|
82
|
+
* const options = await host.callTool(tool, args); // e.g. FTP server list
|
|
83
|
+
* el.dynamicOptions = { ...el.dynamicOptions, [attribute]: options };
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
* Takes priority over `field.options` when both are present for the same
|
|
87
|
+
* attribute. A field is requested at most once per attribute for this
|
|
88
|
+
* element's lifetime — a host that needs to force a refresh re-triggers it
|
|
89
|
+
* itself (out of scope here, same "host decides" split as `validateAll()`).
|
|
90
|
+
*/
|
|
91
|
+
this.dynamicOptions = {};
|
|
92
|
+
/** Master switch for every section's footer (§13/§14) — the default "Save
|
|
93
|
+
* <label>" button, AND any `registerSectionFooter` override. Default true;
|
|
94
|
+
* set false to hide every section's footer at once, e.g. when the host
|
|
95
|
+
* drives submission itself via its own button(s). */
|
|
96
|
+
this.showSectionFooters = true;
|
|
97
|
+
/** How sections are laid out. `'stacked'` (default) is today's behavior —
|
|
98
|
+
* every section as its own card, one after another. `'tabs'` renders one
|
|
99
|
+
* `nile-nav-tab` per section (nav) with the section's own card as that
|
|
100
|
+
* tab's panel — orientation controlled by `tabsPlacement`. `'accordion'`
|
|
101
|
+
* renders one `nile-accordion` per section, single-open, with a completion
|
|
102
|
+
* summary in each collapsed header (field count + status chip) and a
|
|
103
|
+
* two-column, label-above-control body — see `renderAccordion`. */
|
|
104
|
+
this.sectionsLayout = 'stacked';
|
|
105
|
+
/** Tab orientation when `sectionsLayout === 'tabs'` — passed straight
|
|
106
|
+
* through to `nile-nav-tab-group`'s own `placement`. Ignored in
|
|
107
|
+
* `'stacked'` mode. `'start'` gives a left-hand vertical tab list. */
|
|
108
|
+
this.tabsPlacement = 'top';
|
|
109
|
+
/** In `'tabs'` mode, show a small red dot next to a tab's label when that
|
|
110
|
+
* section has a required-but-empty field or a live validation error — lets
|
|
111
|
+
* the user spot which tab needs attention without opening it. Default
|
|
112
|
+
* true (this is a pure addition with no prior equivalent, so there's
|
|
113
|
+
* nothing to stay backward-compatible with); set false to opt out.
|
|
114
|
+
* Ignored in `'stacked'` mode, where inline field errors are already
|
|
115
|
+
* visible without needing a tab-level summary. */
|
|
116
|
+
this.tabsShowErrorIndicator = true;
|
|
117
|
+
/** Which section starts expanded in `'accordion'` mode. Left unset
|
|
118
|
+
* (`null`), the library picks: the first section with a required-but-empty
|
|
119
|
+
* field, else the first visible section — so a form always opens on the
|
|
120
|
+
* part that still needs work. A section `key` pins that section instead;
|
|
121
|
+
* the empty string starts every section collapsed. Only consulted when the
|
|
122
|
+
* descriptor (or this property) changes — never re-evaluated on a value
|
|
123
|
+
* edit, which would slam the card the user is typing in shut. Ignored in
|
|
124
|
+
* the other layouts. */
|
|
125
|
+
this.accordionOpenSection = null;
|
|
126
|
+
/** Show the open section's save footer in `'accordion'` mode. Default
|
|
127
|
+
* FALSE, unlike the other two layouts: an accordion is a whole-form
|
|
128
|
+
* overview, and the host normally drives one submit for all of it via
|
|
129
|
+
* `validateAll()`. Set true to get the per-section "Save <label>" button
|
|
130
|
+
* here too; `showSectionFooters` still overrides both to off. Ignored in
|
|
131
|
+
* the other layouts. */
|
|
132
|
+
this.accordionShowFooters = false;
|
|
133
|
+
/** How many columns the open section's field grid uses in `'accordion'`
|
|
134
|
+
* mode. `2` (default) is the side-by-side layout; `1` stacks every field
|
|
135
|
+
* full width, which suits a narrow container, a long-label descriptor, or
|
|
136
|
+
* simply a host that prefers a single reading column.
|
|
137
|
+
*
|
|
138
|
+
* This is a CEILING, not a fixed count: at `2` the grid still collapses to
|
|
139
|
+
* one column when the container is too narrow to give each cell a usable
|
|
140
|
+
* width (see `.aq-mcp-section__body--dense`), so `1` means "never two",
|
|
141
|
+
* not "always exactly one regardless of space". Ignored in the other
|
|
142
|
+
* layouts, whose rows are single-column by construction. */
|
|
143
|
+
this.accordionColumns = 2;
|
|
144
|
+
/** Host override for section header icons in `'accordion'` mode, keyed by
|
|
145
|
+
* `section.key` → `nile-glyph` name. Highest priority in a three-step
|
|
146
|
+
* fallback: this map, then the descriptor's own `section.icon`, then
|
|
147
|
+
* `DEFAULT_SECTION_ICONS` below. The host wins because it knows the
|
|
148
|
+
* product's iconography; the descriptor only knows the connector. */
|
|
149
|
+
this.sectionIcons = {};
|
|
150
|
+
this.errors = {};
|
|
151
|
+
/** The single expanded section's `key` in `'accordion'` mode, or null when
|
|
152
|
+
* all are collapsed. Holding ONE key is what makes the accordion
|
|
153
|
+
* single-open: setting it re-renders every sibling with `open=false`. */
|
|
154
|
+
this.openKey = null;
|
|
155
|
+
this.pendingAction = null;
|
|
156
|
+
/** Attributes already asked about via `aq-mcp-options-request` — a plain dedupe
|
|
157
|
+
* guard, not rendered state, so mutated in place rather than reassigned (no
|
|
158
|
+
* need to trigger a Lit update for it). */
|
|
159
|
+
this.requestedOptions = new Set();
|
|
160
|
+
}
|
|
161
|
+
createRenderRoot() {
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
willUpdate(changed) {
|
|
165
|
+
// Re-seed on a `values` change too, not just `descriptor` — a field that was
|
|
166
|
+
// inapplicable at initial load (e.g. gated behind `dependencyAttribute`) only
|
|
167
|
+
// gets its own `defaultValue` seeded once the user's edit makes it apply, and
|
|
168
|
+
// that edit lands as a `values` change, never a `descriptor` one. `seedDefaults`
|
|
169
|
+
// only ever fills in a currently-`undefined` attribute, so comparing key counts
|
|
170
|
+
// is enough to tell "did this seeding pass actually add anything" — guards
|
|
171
|
+
// against reassigning (and re-triggering `willUpdate`) every single time.
|
|
172
|
+
if ((changed.has('descriptor') || changed.has('values')) && this.descriptor) {
|
|
173
|
+
const seeded = seedDefaults(this.descriptor, this.values);
|
|
174
|
+
if (Object.keys(seeded).length !== Object.keys(this.values).length) {
|
|
175
|
+
this.values = seeded;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Which accordion card starts open is decided from the descriptor, NOT
|
|
179
|
+
// re-decided on every edit — see `accordionOpenSection`'s doc comment.
|
|
180
|
+
// Runs after the seeding above so `resolveOpenKey`'s completeness check
|
|
181
|
+
// sees defaults that were just filled in, not the pre-seed blanks.
|
|
182
|
+
if (changed.has('descriptor') || changed.has('accordionOpenSection') || changed.has('sectionsLayout')) {
|
|
183
|
+
this.openKey = this.resolveOpenKey();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** The section to expand first: an explicit `accordionOpenSection` wins
|
|
187
|
+
* (including `''` for "all collapsed"), else the first section still
|
|
188
|
+
* missing a required value, else the first visible one. */
|
|
189
|
+
resolveOpenKey() {
|
|
190
|
+
if (this.accordionOpenSection != null)
|
|
191
|
+
return this.accordionOpenSection || null;
|
|
192
|
+
const visible = this.visibleSections(this.sortedSections());
|
|
193
|
+
const needsAttention = visible.find((s) => sectionStatus(s, this.values).state === 'incomplete');
|
|
194
|
+
return (needsAttention ?? visible[0])?.key ?? null;
|
|
195
|
+
}
|
|
196
|
+
/** Sections in render order (§12) — `order` ascending, missing treated as 0. */
|
|
197
|
+
sortedSections() {
|
|
198
|
+
return [...(this.descriptor?.sections ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
199
|
+
}
|
|
200
|
+
updated(changed) {
|
|
201
|
+
if (changed.has('descriptor') || changed.has('values') || changed.has('dynamicOptions')) {
|
|
202
|
+
this.requestPendingOptions();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Emits `aq-mcp-options-request` for every currently-applicable `select`/`radio`
|
|
206
|
+
* field that declares `tool` but has no options yet — neither a static
|
|
207
|
+
* `field.options` nor an already-fed `dynamicOptions` entry — and hasn't been
|
|
208
|
+
* asked about before. Skipped for a field that's statically hidden or whose
|
|
209
|
+
* `dependencyAttribute` condition isn't currently met (§12) — no point asking
|
|
210
|
+
* for FTP servers before the user has even chosen SFTP storage. */
|
|
211
|
+
requestPendingOptions() {
|
|
212
|
+
for (const section of this.descriptor?.sections ?? []) {
|
|
213
|
+
for (const field of section.fields ?? []) {
|
|
214
|
+
if (!field.tool)
|
|
215
|
+
continue;
|
|
216
|
+
if (field['ui-component'] !== 'select' && field['ui-component'] !== 'radio')
|
|
217
|
+
continue;
|
|
218
|
+
if (field.hidden || !fieldApplies(field, this.values))
|
|
219
|
+
continue;
|
|
220
|
+
if (this.requestedOptions.has(field.attribute))
|
|
221
|
+
continue;
|
|
222
|
+
const hasOptions = (this.dynamicOptions[field.attribute] ?? field.options ?? []).length > 0;
|
|
223
|
+
if (hasOptions)
|
|
224
|
+
continue;
|
|
225
|
+
this.requestedOptions.add(field.attribute);
|
|
226
|
+
this.dispatchEvent(new CustomEvent('aq-mcp-options-request', {
|
|
227
|
+
detail: { attribute: field.attribute, tool: field.tool, args: field.args ?? {} },
|
|
228
|
+
bubbles: true,
|
|
229
|
+
composed: true,
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Look up a field's own schema by `attribute` — needed to re-validate it
|
|
235
|
+
* live on every edit (`onFieldInput`), since the event only carries the
|
|
236
|
+
* new value, not the field definition itself. */
|
|
237
|
+
fieldByAttribute(attribute) {
|
|
238
|
+
for (const s of this.descriptor?.sections ?? []) {
|
|
239
|
+
const f = (s.fields ?? []).find((field) => field.attribute === attribute);
|
|
240
|
+
if (f)
|
|
241
|
+
return f;
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
/** Whether a field is currently in play for validation — skips
|
|
246
|
+
* `datatype: "none"` (buttons/reports never hold a value), statically
|
|
247
|
+
* hidden fields, and fields whose `dependencyAttribute` condition isn't
|
|
248
|
+
* currently met. Shared by `computeErrors`, live on-change validation, and
|
|
249
|
+
* the tab error-indicator, so all three agree on what "applicable" means. */
|
|
250
|
+
fieldNeedsValidation(f) {
|
|
251
|
+
return f.datatype !== 'none' && !f.hidden && fieldApplies(f, this.values);
|
|
252
|
+
}
|
|
253
|
+
// ---- field intent ----
|
|
254
|
+
onFieldInput(e) {
|
|
255
|
+
const { attribute, value } = e.detail;
|
|
256
|
+
this.values = { ...this.values, [attribute]: value };
|
|
257
|
+
// Live-validate on every change (not just clear-and-wait-for-submit): an
|
|
258
|
+
// edited field's error updates immediately to reflect its NEW value —
|
|
259
|
+
// shows the instant it's typed, clears the instant it's actually fixed.
|
|
260
|
+
const field = this.fieldByAttribute(attribute);
|
|
261
|
+
const msg = field && this.fieldNeedsValidation(field) ? validateField(field, value) : null;
|
|
262
|
+
this.errors = { ...this.errors, [attribute]: msg };
|
|
263
|
+
this.dispatchEvent(new CustomEvent('aq-mcp-change', {
|
|
264
|
+
detail: { attribute, value, values: this.values },
|
|
265
|
+
bubbles: true,
|
|
266
|
+
composed: true,
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
onFieldAction(e) {
|
|
270
|
+
const { field, action } = e.detail;
|
|
271
|
+
const confirm = action?.confirm ?? field.confirm;
|
|
272
|
+
if (confirm) {
|
|
273
|
+
this.pendingAction = { field, action }; // honour confirm before invoking (§5)
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
this.emitAction(field, action);
|
|
277
|
+
}
|
|
278
|
+
onSectionSubmit(e) {
|
|
279
|
+
const { sectionKey } = e.detail;
|
|
280
|
+
const section = (this.descriptor?.sections ?? []).find((s) => s.key === sectionKey);
|
|
281
|
+
if (section)
|
|
282
|
+
this.submitSection(section);
|
|
283
|
+
}
|
|
284
|
+
emitAction(field, action) {
|
|
285
|
+
this.dispatchEvent(new CustomEvent('aq-mcp-action', {
|
|
286
|
+
detail: {
|
|
287
|
+
tool: action?.tool ?? field.tool,
|
|
288
|
+
args: action?.args ?? field.args ?? {},
|
|
289
|
+
confirm: action?.confirm ?? field.confirm,
|
|
290
|
+
field,
|
|
291
|
+
action,
|
|
292
|
+
},
|
|
293
|
+
bubbles: true,
|
|
294
|
+
composed: true,
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
// ---- submit ----
|
|
298
|
+
/** Validate one batch of fields against the current `values` — shared by
|
|
299
|
+
* `validateSection` (one section) and the public `validateAll` (every
|
|
300
|
+
* section), so both paths compute errors identically. */
|
|
301
|
+
computeErrors(fields) {
|
|
302
|
+
const errs = {};
|
|
303
|
+
let ok = true;
|
|
304
|
+
for (const f of fields) {
|
|
305
|
+
if (!this.fieldNeedsValidation(f))
|
|
306
|
+
continue;
|
|
307
|
+
const msg = validateField(f, this.values[f.attribute]);
|
|
308
|
+
errs[f.attribute] = msg;
|
|
309
|
+
if (msg)
|
|
310
|
+
ok = false;
|
|
311
|
+
}
|
|
312
|
+
return { errs, ok };
|
|
313
|
+
}
|
|
314
|
+
/** Does this section have an actual, currently-recorded validation error —
|
|
315
|
+
* i.e. a field present in `this.errors` with a message. Deliberately reads
|
|
316
|
+
* `this.errors`, NOT a fresh re-validation of every field: a required field
|
|
317
|
+
* the user simply hasn't reached yet is incomplete, not invalid, and
|
|
318
|
+
* shouldn't flag its tab before the user has done anything. An error only
|
|
319
|
+
* lands in `this.errors` once a field has actually been edited (live
|
|
320
|
+
* validation) or the section/form has been submitted at least once —
|
|
321
|
+
* exactly the same "has this field's error actually surfaced" signal the
|
|
322
|
+
* inline per-field error display already uses. */
|
|
323
|
+
sectionHasError(section) {
|
|
324
|
+
return (section.fields ?? []).some((f) => !!this.errors[f.attribute]);
|
|
325
|
+
}
|
|
326
|
+
validateSection(section) {
|
|
327
|
+
const { errs, ok } = this.computeErrors(section.fields ?? []);
|
|
328
|
+
this.errors = { ...this.errors, ...errs };
|
|
329
|
+
return ok;
|
|
330
|
+
}
|
|
331
|
+
submitSection(section) {
|
|
332
|
+
if (!this.validateSection(section))
|
|
333
|
+
return;
|
|
334
|
+
this.dispatchEvent(new CustomEvent('aq-mcp-submit', {
|
|
335
|
+
detail: collectSubmit(section, this.values),
|
|
336
|
+
bubbles: true,
|
|
337
|
+
composed: true,
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
/** Validate every section's applicable fields at once and return the full
|
|
341
|
+
* form state. Pushes results into this element's own `errors` state — the
|
|
342
|
+
* same thing a section's Save button does via `validateSection`, just
|
|
343
|
+
* across ALL sections at once — so a host-driven "submit everything"
|
|
344
|
+
* button (bypassing per-section footers entirely) still gets the
|
|
345
|
+
* library's real validation and inline field-error display, not just the
|
|
346
|
+
* raw `values`. Call it imperatively on the element (e.g. an Angular
|
|
347
|
+
* template ref: `<aq-mcp-config #cfg>` then `cfg.validateAll()`) — there's
|
|
348
|
+
* no natural event to hang this off, since the host decides what "submit
|
|
349
|
+
* everything" even means for its own data shape. */
|
|
350
|
+
validateAll() {
|
|
351
|
+
const allFields = (this.descriptor?.sections ?? []).flatMap((s) => s.fields ?? []);
|
|
352
|
+
const { errs, ok } = this.computeErrors(allFields);
|
|
353
|
+
this.errors = errs;
|
|
354
|
+
return { valid: ok, values: this.values };
|
|
355
|
+
}
|
|
356
|
+
render() {
|
|
357
|
+
const d = this.descriptor;
|
|
358
|
+
if (!d)
|
|
359
|
+
return html ``;
|
|
360
|
+
const sections = this.sortedSections();
|
|
361
|
+
const major = majorOf(d.version);
|
|
362
|
+
// MAJOR ahead of what we support → best-effort render on known keys + a notice (§9.5).
|
|
363
|
+
const versionAhead = Number.isFinite(major) && major > SUPPORTED_MAJOR;
|
|
364
|
+
return html `
|
|
365
|
+
<div
|
|
366
|
+
class="aq-mcp ${this.sectionsLayout === 'accordion' ? 'aq-mcp--accordion' : ''}"
|
|
367
|
+
@field-input=${this.onFieldInput}
|
|
368
|
+
@field-action=${this.onFieldAction}
|
|
369
|
+
@section-submit=${this.onSectionSubmit}
|
|
370
|
+
>
|
|
371
|
+
${versionAhead
|
|
372
|
+
? html `<nile-section-message
|
|
373
|
+
variant="warning"
|
|
374
|
+
heading="Unsupported configuration version"
|
|
375
|
+
description="This form was built for an older version (v${SUPPORTED_MAJOR}.x). Some fields may not render as intended."
|
|
376
|
+
></nile-section-message>`
|
|
377
|
+
: html ``}
|
|
378
|
+
${this.renderSections(sections)}
|
|
379
|
+
${this.renderConfirm()}
|
|
380
|
+
</div>
|
|
381
|
+
`;
|
|
382
|
+
}
|
|
383
|
+
/** Dispatch to the layout the host asked for. `'stacked'` — today's
|
|
384
|
+
* default — is the fallback for any unrecognised value, so a typo'd
|
|
385
|
+
* attribute renders a usable form rather than nothing. */
|
|
386
|
+
renderSections(sections) {
|
|
387
|
+
if (this.sectionsLayout === 'tabs')
|
|
388
|
+
return this.renderTabs(sections);
|
|
389
|
+
if (this.sectionsLayout === 'accordion')
|
|
390
|
+
return this.renderAccordion(sections);
|
|
391
|
+
return sections.map((s) => this.renderSection(s));
|
|
392
|
+
}
|
|
393
|
+
/** A section whose every field is statically hidden has nothing to show
|
|
394
|
+
* (§7, §12) — in 'stacked' mode `renderSection` just renders nothing for
|
|
395
|
+
* it; in 'tabs' mode it must not get a tab at all (an empty tab is worse
|
|
396
|
+
* than no tab), so this filters the nav list up front. */
|
|
397
|
+
visibleSections(sections) {
|
|
398
|
+
return sections.filter((s) => {
|
|
399
|
+
const fields = s.fields ?? [];
|
|
400
|
+
return !(fields.length > 0 && fields.every((f) => f.hidden));
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
renderTabs(sections) {
|
|
404
|
+
const visible = this.visibleSections(sections);
|
|
405
|
+
if (visible.length === 0)
|
|
406
|
+
return html ``;
|
|
407
|
+
// 'start'/'end' are the vertical placements — nile-nav-tab-group's own
|
|
408
|
+
// `width` attribute fixes each tab item's width (its documented purpose:
|
|
409
|
+
// preventing layout shift when switching tabs), which for a vertical nav
|
|
410
|
+
// list is exactly the nav column's width; `indicator-placement="left"`
|
|
411
|
+
// is Nile's own documented pairing for vertical placement.
|
|
412
|
+
const vertical = this.tabsPlacement === 'start' || this.tabsPlacement === 'end';
|
|
413
|
+
return html `
|
|
414
|
+
<nile-nav-tab-group
|
|
415
|
+
class="aq-mcp-tabs"
|
|
416
|
+
placement=${this.tabsPlacement}
|
|
417
|
+
indicator-placement=${vertical ? 'left' : ''}
|
|
418
|
+
width=${vertical ? '220px' : ''}
|
|
419
|
+
>
|
|
420
|
+
${visible.map((s) => html `<nile-nav-tab slot="nav" panel=${s.key}>
|
|
421
|
+
<span class="aq-mcp-tab-label" title=${s.label}>${s.label}</span>
|
|
422
|
+
${this.tabsShowErrorIndicator && this.sectionHasError(s)
|
|
423
|
+
? html `<span
|
|
424
|
+
slot="suffix"
|
|
425
|
+
class="aq-mcp-tab-error-dot"
|
|
426
|
+
role="img"
|
|
427
|
+
aria-label="${s.label} has a validation error"
|
|
428
|
+
></span>`
|
|
429
|
+
: ''}
|
|
430
|
+
</nile-nav-tab>`)}
|
|
431
|
+
${visible.map((s) => html `<nile-nav-tab-panel name=${s.key}>${this.renderSection(s, true)}</nile-nav-tab-panel>`)}
|
|
432
|
+
</nile-nav-tab-group>
|
|
433
|
+
`;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Is this `nile-show`/`nile-hide` the ACCORDION's own, or one that bubbled up
|
|
437
|
+
* from a descendant?
|
|
438
|
+
*
|
|
439
|
+
* Every Nile component emits with `bubbles: true, composed: true`
|
|
440
|
+
* (`internal/nile-element.ts`), and `nile-tooltip` emits the same
|
|
441
|
+
* `nile-show`/`nile-hide` names an accordion does. A field's help tooltip
|
|
442
|
+
* lives inside the open card, so without this guard simply hovering an info
|
|
443
|
+
* icon opened the section and dismissing the tooltip COLLAPSED it — the
|
|
444
|
+
* card slamming shut under the user's cursor.
|
|
445
|
+
*
|
|
446
|
+
* `currentTarget` is the `nile-accordion` the listener sits on;
|
|
447
|
+
* `target` is whatever emitted. Comparing them keeps a descendant's
|
|
448
|
+
* identically-named event from driving this element's open state, and stays
|
|
449
|
+
* correct for any future nested Nile component that reuses these names.
|
|
450
|
+
*/
|
|
451
|
+
static isOwnDisclosure(e) {
|
|
452
|
+
return e.target === e.currentTarget;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* `sectionsLayout: 'accordion'` — one `nile-accordion` per visible section,
|
|
456
|
+
* single-open, in §12 order.
|
|
457
|
+
*
|
|
458
|
+
* Single-open falls out of holding ONE key: `nile-accordion` owns its own
|
|
459
|
+
* `open` flag, so both directions are wired — `?open` down from `openKey`,
|
|
460
|
+
* `nile-show`/`nile-hide` back up. Setting `openKey` on a show re-renders
|
|
461
|
+
* every sibling with `open=false`, which closes them; there's no need to
|
|
462
|
+
* imperatively call `hide()` on the others.
|
|
463
|
+
*/
|
|
464
|
+
/** Host map first, then the built-in map for the contract's own section
|
|
465
|
+
* keys, then a generic glyph. The descriptor has no icon field and its
|
|
466
|
+
* shape is fixed, so the host is the only place a product can override
|
|
467
|
+
* this — which is the right place anyway: iconography is the app's
|
|
468
|
+
* decision, not the connector's. */
|
|
469
|
+
iconFor(section) {
|
|
470
|
+
return this.sectionIcons[section.key] ?? DEFAULT_SECTION_ICONS[section.key] ?? FALLBACK_SECTION_ICON;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* The header's completion readout, for a section the user may never open.
|
|
474
|
+
*
|
|
475
|
+
* A mark and a NUMBER, no sentence. The count is descriptor-derived (how
|
|
476
|
+
* many `isRequired` fields still hold no value); the check and the dot are
|
|
477
|
+
* symbols. An earlier version read "Complete" and "N required left", which
|
|
478
|
+
* is the renderer narrating state in words the descriptor never supplied.
|
|
479
|
+
*
|
|
480
|
+
* The trade-off is real and worth naming: a bare dot-and-count is terser
|
|
481
|
+
* than "2 required left", and leans on the reader connecting it to the `*`
|
|
482
|
+
* marks inside the card. `title` carries the count on hover and to assistive
|
|
483
|
+
* tech using the descriptor's own key name rather than invented prose.
|
|
484
|
+
*/
|
|
485
|
+
renderSectionStatus(status) {
|
|
486
|
+
if (status.state === 'neutral')
|
|
487
|
+
return html ``;
|
|
488
|
+
if (status.state === 'incomplete') {
|
|
489
|
+
return html `<span
|
|
490
|
+
class="aq-mcp-acc__state aq-mcp-acc__state--incomplete"
|
|
491
|
+
title="isRequired: ${status.requiredEmpty}"
|
|
492
|
+
>
|
|
493
|
+
<span class="aq-mcp-acc__dot"></span>${status.requiredEmpty}
|
|
494
|
+
</span>`;
|
|
495
|
+
}
|
|
496
|
+
return html `<span class="aq-mcp-acc__state aq-mcp-acc__state--complete">
|
|
497
|
+
<nile-glyph name="ng-check" method="stroke" size="18" color="currentColor"></nile-glyph>
|
|
498
|
+
</span>`;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* `sectionsLayout: 'accordion'` — one `nile-accordion` per visible section,
|
|
502
|
+
* single-open, in §12 order. Each header is an icon tile, the section
|
|
503
|
+
* `label`, its `authType` badge, and a completion status beside the chevron,
|
|
504
|
+
* so a collapsed section says what it is and whether it still needs work
|
|
505
|
+
* without being opened.
|
|
506
|
+
*
|
|
507
|
+
* Every word of connector-describing text here is a descriptor VALUE
|
|
508
|
+
* rendered verbatim — `section.label` and `section.authType`. An earlier
|
|
509
|
+
* version composed a sentence around them ("Basic authentication · saved by
|
|
510
|
+
* the platform"), which read as the connector's own description of itself
|
|
511
|
+
* but was the renderer putting words in its mouth: only "Basic" came from
|
|
512
|
+
* the descriptor, and "saved by the platform" was inferred from a key being
|
|
513
|
+
* ABSENT. The `bootstrap` format carries no prose field, so rather than
|
|
514
|
+
* invent one, nothing is written here that the descriptor did not supply.
|
|
515
|
+
*
|
|
516
|
+
* Single-open falls out of holding ONE key: `nile-accordion` owns its own
|
|
517
|
+
* `open` flag, so both directions are wired — `?open` down from `openKey`,
|
|
518
|
+
* `nile-show`/`nile-hide` back up. Setting `openKey` on a show re-renders
|
|
519
|
+
* every sibling with `open=false`, which closes them; there's no need to
|
|
520
|
+
* imperatively call `hide()` on the others.
|
|
521
|
+
*/
|
|
522
|
+
renderAccordion(sections) {
|
|
523
|
+
const visible = this.visibleSections(sections);
|
|
524
|
+
if (visible.length === 0)
|
|
525
|
+
return html ``;
|
|
526
|
+
return html `<div class="aq-mcp-acc-list">
|
|
527
|
+
${visible.map((s) => {
|
|
528
|
+
const status = sectionStatus(s, this.values);
|
|
529
|
+
return html `<nile-accordion
|
|
530
|
+
class="aq-mcp-acc"
|
|
531
|
+
data-section=${s.key}
|
|
532
|
+
data-state=${status.state}
|
|
533
|
+
expandIconPlacement="right"
|
|
534
|
+
size="lg"
|
|
535
|
+
?open=${this.openKey === s.key}
|
|
536
|
+
@nile-show=${(e) => {
|
|
537
|
+
if (!AqMcpConfig_1.isOwnDisclosure(e))
|
|
538
|
+
return;
|
|
539
|
+
this.openKey = s.key;
|
|
540
|
+
}}
|
|
541
|
+
@nile-hide=${(e) => {
|
|
542
|
+
if (!AqMcpConfig_1.isOwnDisclosure(e))
|
|
543
|
+
return;
|
|
544
|
+
// Only clear if THIS section is the one recorded as open — a hide
|
|
545
|
+
// fired by a sibling being re-rendered closed must not wipe the
|
|
546
|
+
// key that was just set by the section the user actually opened.
|
|
547
|
+
if (this.openKey === s.key)
|
|
548
|
+
this.openKey = null;
|
|
549
|
+
}}
|
|
550
|
+
>
|
|
551
|
+
<div slot="summary" class="aq-mcp-acc__summary">
|
|
552
|
+
<span class="aq-mcp-acc__icon" aria-hidden="true">
|
|
553
|
+
<nile-glyph name=${this.iconFor(s)} method="stroke" size="20" color="currentColor"></nile-glyph>
|
|
554
|
+
</span>
|
|
555
|
+
<span class="aq-mcp-acc__heading">
|
|
556
|
+
<span class="aq-mcp-acc__label">${s.label}</span>
|
|
557
|
+
${s.authType ? html `<nile-badge variant="info">${s.authType}</nile-badge>` : ''}
|
|
558
|
+
</span>
|
|
559
|
+
${this.renderSectionStatus(status)}
|
|
560
|
+
</div>
|
|
561
|
+
${this.renderSection(s, false, true)}
|
|
562
|
+
</nile-accordion>`;
|
|
563
|
+
})}
|
|
564
|
+
</div>`;
|
|
565
|
+
}
|
|
566
|
+
/** `compact` drops the per-section card's own border + title (the tab nav
|
|
567
|
+
* already shows the label) when nested inside a tab panel — the authType
|
|
568
|
+
* badge and save-target caption still show either way. `dense` is the
|
|
569
|
+
* accordion equivalent: the card AND the header go (the accordion's own
|
|
570
|
+
* summary carries all of it), and the body switches to the two-column,
|
|
571
|
+
* label-above-control grid. They're separate flags, not one enum, because
|
|
572
|
+
* each drops a different amount of chrome. */
|
|
573
|
+
renderSection(section, compact = false, dense = false) {
|
|
574
|
+
const fields = section.fields ?? [];
|
|
575
|
+
// Section whose every field is hidden renders nothing (§7, §12).
|
|
576
|
+
if (fields.length > 0 && fields.every((f) => f.hidden))
|
|
577
|
+
return html ``;
|
|
578
|
+
// Accordion mode hides the per-section footer unless the host opts back
|
|
579
|
+
// in (`accordionShowFooters`) — `showSectionFooters` still vetoes both.
|
|
580
|
+
const showFooter = this.showSectionFooters && (!dense || this.accordionShowFooters);
|
|
581
|
+
return html `<aq-mcp-section
|
|
582
|
+
.section=${section}
|
|
583
|
+
.values=${this.values}
|
|
584
|
+
.errors=${this.errors}
|
|
585
|
+
.showFooter=${showFooter}
|
|
586
|
+
.compact=${compact}
|
|
587
|
+
.dense=${dense}
|
|
588
|
+
.columns=${this.accordionColumns}
|
|
589
|
+
.dynamicOptions=${this.dynamicOptions}
|
|
590
|
+
></aq-mcp-section>`;
|
|
591
|
+
}
|
|
592
|
+
renderConfirm() {
|
|
593
|
+
const pending = this.pendingAction;
|
|
594
|
+
if (!pending)
|
|
595
|
+
return html ``;
|
|
596
|
+
const { field, action } = pending;
|
|
597
|
+
const cancel = () => {
|
|
598
|
+
this.pendingAction = null;
|
|
599
|
+
};
|
|
600
|
+
const proceed = () => {
|
|
601
|
+
this.pendingAction = null;
|
|
602
|
+
this.emitAction(field, action);
|
|
603
|
+
};
|
|
604
|
+
return html `
|
|
605
|
+
<nile-dialog open label="Please confirm" @nile-close=${cancel} @nile-hide=${cancel}>
|
|
606
|
+
<p>${action?.confirm ?? field.confirm}</p>
|
|
607
|
+
<nile-button slot="footer" variant="ghost" @click=${cancel}>Cancel</nile-button>
|
|
608
|
+
<nile-button slot="footer" variant="primary" @click=${proceed}>Continue</nile-button>
|
|
609
|
+
</nile-dialog>
|
|
610
|
+
`;
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
__decorate([
|
|
614
|
+
property({ attribute: false }),
|
|
615
|
+
__metadata("design:type", Object)
|
|
616
|
+
], AqMcpConfig.prototype, "descriptor", void 0);
|
|
617
|
+
__decorate([
|
|
618
|
+
property({ attribute: false }),
|
|
619
|
+
__metadata("design:type", Object)
|
|
620
|
+
], AqMcpConfig.prototype, "values", void 0);
|
|
621
|
+
__decorate([
|
|
622
|
+
property({ attribute: false }),
|
|
623
|
+
__metadata("design:type", Object)
|
|
624
|
+
], AqMcpConfig.prototype, "dynamicOptions", void 0);
|
|
625
|
+
__decorate([
|
|
626
|
+
property({ type: Boolean }),
|
|
627
|
+
__metadata("design:type", Object)
|
|
628
|
+
], AqMcpConfig.prototype, "showSectionFooters", void 0);
|
|
629
|
+
__decorate([
|
|
630
|
+
property({ type: String }),
|
|
631
|
+
__metadata("design:type", String)
|
|
632
|
+
], AqMcpConfig.prototype, "sectionsLayout", void 0);
|
|
633
|
+
__decorate([
|
|
634
|
+
property({ type: String }),
|
|
635
|
+
__metadata("design:type", String)
|
|
636
|
+
], AqMcpConfig.prototype, "tabsPlacement", void 0);
|
|
637
|
+
__decorate([
|
|
638
|
+
property({ type: Boolean }),
|
|
639
|
+
__metadata("design:type", Object)
|
|
640
|
+
], AqMcpConfig.prototype, "tabsShowErrorIndicator", void 0);
|
|
641
|
+
__decorate([
|
|
642
|
+
property({ type: String }),
|
|
643
|
+
__metadata("design:type", Object)
|
|
644
|
+
], AqMcpConfig.prototype, "accordionOpenSection", void 0);
|
|
645
|
+
__decorate([
|
|
646
|
+
property({ type: Boolean }),
|
|
647
|
+
__metadata("design:type", Object)
|
|
648
|
+
], AqMcpConfig.prototype, "accordionShowFooters", void 0);
|
|
649
|
+
__decorate([
|
|
650
|
+
property({ type: Number }),
|
|
651
|
+
__metadata("design:type", Number)
|
|
652
|
+
], AqMcpConfig.prototype, "accordionColumns", void 0);
|
|
653
|
+
__decorate([
|
|
654
|
+
property({ attribute: false }),
|
|
655
|
+
__metadata("design:type", Object)
|
|
656
|
+
], AqMcpConfig.prototype, "sectionIcons", void 0);
|
|
657
|
+
__decorate([
|
|
658
|
+
state(),
|
|
659
|
+
__metadata("design:type", Object)
|
|
660
|
+
], AqMcpConfig.prototype, "errors", void 0);
|
|
661
|
+
__decorate([
|
|
662
|
+
state(),
|
|
663
|
+
__metadata("design:type", Object)
|
|
664
|
+
], AqMcpConfig.prototype, "openKey", void 0);
|
|
665
|
+
__decorate([
|
|
666
|
+
state(),
|
|
667
|
+
__metadata("design:type", Object)
|
|
668
|
+
], AqMcpConfig.prototype, "pendingAction", void 0);
|
|
669
|
+
AqMcpConfig = AqMcpConfig_1 = __decorate([
|
|
670
|
+
customElement('aq-mcp-config')
|
|
671
|
+
], AqMcpConfig);
|
|
672
|
+
export { AqMcpConfig };
|
|
673
|
+
//# sourceMappingURL=aq-mcp-config.js.map
|