@ethisyscore/plugin-ui 1.93.0 → 1.95.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/README.md +2 -1
- package/dist/components/connectors/index.cjs +656 -2
- package/dist/components/connectors/index.cjs.map +1 -1
- package/dist/components/connectors/index.d.cts +135 -373
- package/dist/components/connectors/index.d.ts +135 -373
- package/dist/components/connectors/index.js +652 -5
- package/dist/components/connectors/index.js.map +1 -1
- package/dist/components/ui/index.cjs +4 -0
- package/dist/components/ui/index.d.cts +31 -1
- package/dist/components/ui/index.d.ts +31 -1
- package/dist/components/ui/index.js +1 -1
- package/dist/connector-mappings/index.cjs +1061 -0
- package/dist/connector-mappings/index.cjs.map +1 -0
- package/dist/connector-mappings/index.d.cts +1274 -0
- package/dist/connector-mappings/index.d.ts +1274 -0
- package/dist/connector-mappings/index.js +1020 -0
- package/dist/connector-mappings/index.js.map +1 -0
- package/dist/mappingQueryKeys-BcbA0TGy.d.cts +473 -0
- package/dist/mappingQueryKeys-BcbA0TGy.d.ts +473 -0
- package/dist/mcpService-4h3sKDpW.d.cts +35 -0
- package/dist/mcpService-4h3sKDpW.d.ts +35 -0
- package/dist/platform-react/index.cjs +52 -4
- package/dist/platform-react/index.cjs.map +1 -1
- package/dist/platform-react/index.d.cts +30 -35
- package/dist/platform-react/index.d.ts +30 -35
- package/dist/platform-react/index.js +53 -6
- package/dist/platform-react/index.js.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-operation mapping completeness for one connector + module, as returned by the host's
|
|
5
|
+
* `connectors:mappings:completeness` tool.
|
|
6
|
+
*
|
|
7
|
+
* Declared here rather than imported from a plugin's generated contracts because this package sits
|
|
8
|
+
* BELOW the plugins: every plugin that shows a Tool Mappings page had its own copy of this shape,
|
|
9
|
+
* and the copies are what let the surfaces drift apart.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* The distinction that matters, and the one every copy of the banner got wrong at least once:
|
|
13
|
+
* `isToolMapped` and `unmappedRequiredCanonicalFields` are INDEPENDENT. An operation can have a tool
|
|
14
|
+
* mapped and still be incomplete, because the tool's schema does not accept a field the operation
|
|
15
|
+
* requires. Copy that says "not mapped" when it means "fields not mapped" sends the operator to
|
|
16
|
+
* auto-match, which maps tools and cannot close a field gap.
|
|
17
|
+
*/
|
|
18
|
+
interface MappingCompletenessItem {
|
|
19
|
+
/** Canonical operation key, e.g. `list-commits`. */
|
|
20
|
+
operationKey: string;
|
|
21
|
+
/** Human label for the operation, as declared by the module's catalogue. */
|
|
22
|
+
label: string;
|
|
23
|
+
/**
|
|
24
|
+
* Whether the module declares this operation as required. Required gaps block the module from
|
|
25
|
+
* working; optional ones only leave an extra capability switched off.
|
|
26
|
+
*/
|
|
27
|
+
isRequired: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Whether ANY tool is mapped to the operation. False means the operator has not chosen a tool
|
|
30
|
+
* yet; true says nothing about whether the mapping is complete.
|
|
31
|
+
*/
|
|
32
|
+
isToolMapped: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Canonical request fields the operation requires that do not map onto a property the live tool
|
|
35
|
+
* actually accepts. Non-empty means incomplete, whether or not a tool is mapped.
|
|
36
|
+
*/
|
|
37
|
+
unmappedRequiredCanonicalFields: string[];
|
|
38
|
+
}
|
|
39
|
+
/** Response envelope for the completeness read, post-unwrap. */
|
|
40
|
+
interface MappingCompletenessResult {
|
|
41
|
+
operations: MappingCompletenessItem[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Type discriminator for a canonical field. Mirrors the backend `CanonicalFieldKind` enum.
|
|
45
|
+
*
|
|
46
|
+
* Wire format is the enum member name (e.g. `"String"`, `"StringArray"`); the editor renders the
|
|
47
|
+
* value verbatim as the field-kind chip, so an unrecognised kind degrades to a label rather than
|
|
48
|
+
* a blank.
|
|
49
|
+
*/
|
|
50
|
+
type ConnectorCanonicalFieldKind = "String" | "Integer" | "Boolean" | "DateTime" | "Object" | "StringArray";
|
|
51
|
+
/**
|
|
52
|
+
* A single canonical field on an operation's request or response schema. Carried on the mapping row
|
|
53
|
+
* so the editor can render an input per field without a separate schema fetch.
|
|
54
|
+
*/
|
|
55
|
+
interface ConnectorCanonicalField {
|
|
56
|
+
name: string;
|
|
57
|
+
kind: ConnectorCanonicalFieldKind;
|
|
58
|
+
isRequired: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Arguments the editor hands to the consumer-supplied `onTestMapping` callback. The consumer builds
|
|
62
|
+
* the full service request from these plus its own `connectorId` / `operationKey` / `toolName` — the
|
|
63
|
+
* editor owns no network call.
|
|
64
|
+
*/
|
|
65
|
+
interface TestToolMappingRequest {
|
|
66
|
+
/** Sample values the operator typed — one entry per canonical request field. */
|
|
67
|
+
sampleArgs: Record<string, unknown>;
|
|
68
|
+
/** Current unsaved request-field map (empty inputs stripped; `null` if all blank). */
|
|
69
|
+
requestFieldMap: Record<string, string> | null;
|
|
70
|
+
/** Current unsaved response-field paths (empty inputs stripped; `null` if all blank). */
|
|
71
|
+
responseFieldPaths: Record<string, string> | null;
|
|
72
|
+
/**
|
|
73
|
+
* Current unsaved static request parameters (blank-key rows dropped; `null` if none). Optional and
|
|
74
|
+
* backward-compatible: it lets the dry-run exercise the DRAFT static params — a consolidated
|
|
75
|
+
* tool's `method` discriminator, say — that the operator typed but has not saved yet. A consumer
|
|
76
|
+
* whose test endpoint does not accept static fields simply ignores it.
|
|
77
|
+
*/
|
|
78
|
+
staticRequestFields?: Record<string, string> | null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Result of extracting one canonical response field from the tool's raw response. Mirrors the
|
|
82
|
+
* backend `ExtractedFieldResult` DTO.
|
|
83
|
+
*/
|
|
84
|
+
interface TestToolMappingExtractedField {
|
|
85
|
+
canonicalField: string;
|
|
86
|
+
/** Extracted value, or `null` when the path matched nothing. */
|
|
87
|
+
value: string | null;
|
|
88
|
+
/** `true` when the JSON path resolved to a node in the response. */
|
|
89
|
+
present: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* `true` when the extracted value looks like Base64 content (long string, charset
|
|
92
|
+
* `[A-Za-z0-9+/=]`, no whitespace). The editor shows a "prefix with base64:" hint so the operator
|
|
93
|
+
* adjusts the JSON path rather than concluding the mapping is broken.
|
|
94
|
+
*/
|
|
95
|
+
looksBase64: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** Result the consumer's `onTestMapping` callback resolves with, after the dry-run. */
|
|
98
|
+
interface TestToolMappingResult {
|
|
99
|
+
success: boolean;
|
|
100
|
+
/** Error message when `success` is false. */
|
|
101
|
+
error: string | null;
|
|
102
|
+
/**
|
|
103
|
+
* Raw JSON string of the tool's response, shown in a collapsible so the operator can find the
|
|
104
|
+
* right JSON path.
|
|
105
|
+
*/
|
|
106
|
+
rawResponseJson: string | null;
|
|
107
|
+
/** Per-field extraction result. */
|
|
108
|
+
extractedFields: TestToolMappingExtractedField[];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Result the consumer's `onSuggestTranslation` callback resolves with. When `success` is true, the
|
|
112
|
+
* editor merges the two dictionaries over its current inputs so the operator can review the
|
|
113
|
+
* AI proposal before saving.
|
|
114
|
+
*/
|
|
115
|
+
interface ConnectorMappingSuggestionResult {
|
|
116
|
+
success: boolean;
|
|
117
|
+
/** Error message when `success` is false. `null` when `success` is true. */
|
|
118
|
+
error: string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Proposed canonical-request → tool-field-name rewrite. Empty object `{}` when the model found no
|
|
121
|
+
* request-side rewrites to apply.
|
|
122
|
+
*/
|
|
123
|
+
requestFieldMap: Record<string, string>;
|
|
124
|
+
/**
|
|
125
|
+
* Proposed canonical-response → JSON-path rewrite (e.g. `{ "content": "base64:$.content" }`).
|
|
126
|
+
* Empty object `{}` when the model found no response-side rewrites.
|
|
127
|
+
*/
|
|
128
|
+
responseFieldPaths: Record<string, string>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface MappingCompletenessBannerProps {
|
|
132
|
+
/**
|
|
133
|
+
* Per-operation completeness for the selected connector + module. The banner splits these into
|
|
134
|
+
* REQUIRED operations with gaps (a blocking warning) and OPTIONAL ones (an informational note);
|
|
135
|
+
* it renders nothing when neither group has gaps.
|
|
136
|
+
*/
|
|
137
|
+
operations: MappingCompletenessItem[];
|
|
138
|
+
/**
|
|
139
|
+
* Opt-in AI assist. Invoked with the operation the operator chose to fix. The consumer wires this
|
|
140
|
+
* to the suggest-translation mutation and decides what to do with the result (open the editor
|
|
141
|
+
* seeded with the suggestion, apply it, and so on).
|
|
142
|
+
*/
|
|
143
|
+
onSuggestWithAi: (operation: MappingCompletenessItem) => void;
|
|
144
|
+
/**
|
|
145
|
+
* Opens the manual mapping editor for an operation. The consumer owns the editor surface and
|
|
146
|
+
* routing; the banner only raises intent.
|
|
147
|
+
*
|
|
148
|
+
* Every known consumer wires this to the request/response translation editor, which is why the
|
|
149
|
+
* row control is labelled for what it opens rather than for this prop's name. Kept required
|
|
150
|
+
* because it always was: making it optional would let a consumer render rows with no action.
|
|
151
|
+
*/
|
|
152
|
+
onOpenMappingEditor: (operation: MappingCompletenessItem) => void;
|
|
153
|
+
/**
|
|
154
|
+
* OPTIONAL, and the preferred wiring. Jumps straight to the operation's REQUEST-FIELD
|
|
155
|
+
* TRANSLATION editor — the "Configure field translation" surface — rather than to whatever general
|
|
156
|
+
* mapping surface `onOpenMappingEditor` happens to open.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* Optional on purpose. This package is pinned independently by every plugin, so a required prop
|
|
160
|
+
* would break every consumer that has not been updated. When it is absent the row falls back to
|
|
161
|
+
* `onOpenMappingEditor` with identical rendering, so an un-updated consumer keeps exactly the
|
|
162
|
+
* behaviour it had and still gains the corrected copy.
|
|
163
|
+
*/
|
|
164
|
+
onConfigureTranslation?: (operation: MappingCompletenessItem) => void;
|
|
165
|
+
/**
|
|
166
|
+
* Operation key currently awaiting an AI suggestion, or `null`. Drives the per-row spinner and
|
|
167
|
+
* disabled state so a slow suggestion does not lock the whole banner.
|
|
168
|
+
*/
|
|
169
|
+
suggestingOperationKey?: string | null;
|
|
170
|
+
/** Disables the AI assist affordance entirely, e.g. AI not configured for the organisation. */
|
|
171
|
+
disableSuggest?: boolean;
|
|
172
|
+
/**
|
|
173
|
+
* `true` when the module has no PROVIDER chosen at all - nobody has said whether it is served by
|
|
174
|
+
* another installed app or by an external system.
|
|
175
|
+
*
|
|
176
|
+
* @remarks
|
|
177
|
+
* A different gap from everything else this banner reports, and the only one that makes the rest
|
|
178
|
+
* meaningless: with no provider there is no tool inventory to be complete or incomplete against,
|
|
179
|
+
* so the two alerts below correctly have nothing to say and the page would otherwise look
|
|
180
|
+
* configured. This is therefore the one condition that renders the banner when no operation has
|
|
181
|
+
* a gap.
|
|
182
|
+
*
|
|
183
|
+
* Optional, defaulting to `false`, for the reason {@link onConfigureTranslation} is: a consumer
|
|
184
|
+
* that has not adopted the provider surface keeps exactly its current behaviour.
|
|
185
|
+
*/
|
|
186
|
+
isProviderUnconfigured?: boolean;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Connector-setup "mapping completeness" banner, shared by every plugin with a Tool Mappings page.
|
|
190
|
+
*
|
|
191
|
+
* Presentational and props-driven: the consumer fetches completeness and owns the editor, so the
|
|
192
|
+
* same banner serves Repo Monitoring, Timesheets, Error Monitoring and anything added later.
|
|
193
|
+
*
|
|
194
|
+
* It separates two cases:
|
|
195
|
+
*
|
|
196
|
+
* - **Required** operations with unmapped required fields, a warning the operator must act on
|
|
197
|
+
* because the module cannot run them.
|
|
198
|
+
* - **Optional** operations with unmapped required fields, a calm note. These power extra
|
|
199
|
+
* capabilities that stay off until their fields are mapped, and the connector may legitimately
|
|
200
|
+
* expose no suitable tool, so they must never read as a blocking error.
|
|
201
|
+
*
|
|
202
|
+
* Renders nothing when neither group has gaps.
|
|
203
|
+
*
|
|
204
|
+
* @remarks
|
|
205
|
+
* **The wording is load-bearing, and was wrong in the copy this was promoted from.** Both groups are
|
|
206
|
+
* selected on `unmappedRequiredCanonicalFields`, which is about FIELD mapping. The original optional
|
|
207
|
+
* alert was titled "Optional operations not mapped" and said they "stay off until mapped", which
|
|
208
|
+
* describes TOOL mapping. Auto-match maps tools and cannot close a field gap, so an operator who
|
|
209
|
+
* pressed auto-match saw the count refuse to move and reported the banner as broken. It was
|
|
210
|
+
* accurate; the text was not. Every string here therefore says "fields", and both alerts say plainly
|
|
211
|
+
* that auto-match will not clear them.
|
|
212
|
+
*
|
|
213
|
+
* A row without the "No tool mapped" chip already HAS a tool and needs its fields mapped. That chip
|
|
214
|
+
* is the only thing distinguishing the two states, which is why the prose points at it.
|
|
215
|
+
*
|
|
216
|
+
* **The second wording fix, from a production report.** Saying "fields" was necessary but not
|
|
217
|
+
* sufficient. An operator read "fields that aren't mapped to this connector's tools", concluded the
|
|
218
|
+
* TOOL was unmapped (it was not), and had no idea which control closed the gap — the text named an
|
|
219
|
+
* abstraction, "the mapping editor", and never the affordance on screen. So the copy now names the
|
|
220
|
+
* thing that is missing (the REQUEST-FIELD TRANSLATION, canonical field name → the tool's own
|
|
221
|
+
* parameter name) and the control that supplies it ("Configure field translation"), and every
|
|
222
|
+
* listed operation carries that control inline. Their words: "it doesnt really suggest how to fix
|
|
223
|
+
* it".
|
|
224
|
+
*
|
|
225
|
+
* That label is not invented here: it is the verbatim tooltip and aria-label of the per-row gear in
|
|
226
|
+
* every consumer's `ConnectorMappingView` (host gogo-ui, self-healing, tech, timesheets, sales). The
|
|
227
|
+
* prose has to name a string the operator can actually find on the page, so a near-miss paraphrase
|
|
228
|
+
* would reintroduce the defect in a quieter form.
|
|
229
|
+
*/
|
|
230
|
+
declare function MappingCompletenessBanner({ operations, onSuggestWithAi, onOpenMappingEditor, onConfigureTranslation, suggestingOperationKey, disableSuggest, isProviderUnconfigured, }: MappingCompletenessBannerProps): react.JSX.Element | null;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The words the provider controls use, in one place, because two surfaces have to say the same ones.
|
|
234
|
+
*
|
|
235
|
+
* ## Why these are constants and not literals at each site
|
|
236
|
+
*
|
|
237
|
+
* A "not configured" message has to name the control that resolves it, in that control's own words.
|
|
238
|
+
* This surface has already paid for the alternative: the completeness banner described a gap in the
|
|
239
|
+
* abstract ("the mapping editor"), an operator could not find any such thing on the page, and the
|
|
240
|
+
* report that came back was "it doesnt really suggest how to fix it". The fix was to name the
|
|
241
|
+
* affordance verbatim - and a verbatim name held as two separate string literals is one rename away
|
|
242
|
+
* from pointing at nothing again, with both files still compiling and both tests still green.
|
|
243
|
+
*
|
|
244
|
+
* So the control renders {@link PROVIDER_CONTROL_LABEL} and the banner quotes it, from here. A test
|
|
245
|
+
* asserts the banner's text contains the label the view actually renders; it fails if either side
|
|
246
|
+
* is edited alone.
|
|
247
|
+
*
|
|
248
|
+
* Pure strings, no imports. That matters: the mapping HOOKS module value-imports this, and it must
|
|
249
|
+
* not drag `@mui/material` into a barrel whose consumers may not have installed it.
|
|
250
|
+
*/
|
|
251
|
+
/** Label of the control that chooses what serves a module. */
|
|
252
|
+
declare const PROVIDER_CONTROL_LABEL = "Served by";
|
|
253
|
+
/**
|
|
254
|
+
* Labels of the two things that can serve a module.
|
|
255
|
+
*
|
|
256
|
+
* Phrased from the operator's side rather than the schema's. "Plugin" and "Connector" are the
|
|
257
|
+
* platform's words for its own internals; an operator choosing where a module's data comes from is
|
|
258
|
+
* choosing between the applications they already run and something outside.
|
|
259
|
+
*/
|
|
260
|
+
declare const PROVIDER_OPTION_LABEL: {
|
|
261
|
+
readonly plugin: "This organisation's apps";
|
|
262
|
+
readonly connector: "External system";
|
|
263
|
+
};
|
|
264
|
+
/** Label of the control that picks WHICH installed application, once "this organisation's apps" is chosen. */
|
|
265
|
+
declare const PLUGIN_PROVIDER_CONTROL_LABEL = "App";
|
|
266
|
+
/** Label of the control that picks WHICH connector, once "external system" is chosen. */
|
|
267
|
+
declare const CONNECTOR_CONTROL_LABEL = "Connector";
|
|
268
|
+
|
|
269
|
+
interface ConnectorMappingTranslationEditorProps {
|
|
270
|
+
open: boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Human-readable operation label for the dialog title (e.g. "Get File").
|
|
273
|
+
*
|
|
274
|
+
* Optional because it is derived from a looked-up operation that may not be found: every caller
|
|
275
|
+
* passes `string | undefined`. The contract claimed required while reality was optional, so this
|
|
276
|
+
* states the truth rather than changing any runtime value.
|
|
277
|
+
*/
|
|
278
|
+
operationLabel?: string;
|
|
279
|
+
/**
|
|
280
|
+
* Tool name the mapping currently points at — shown in the dialog header for context.
|
|
281
|
+
*
|
|
282
|
+
* Optional and nullable because the editor opens for an operation that may not be mapped yet:
|
|
283
|
+
* every caller passes `string | null | undefined`.
|
|
284
|
+
*/
|
|
285
|
+
toolName?: string | null;
|
|
286
|
+
/**
|
|
287
|
+
* Canonical request fields the editor renders one input per.
|
|
288
|
+
*
|
|
289
|
+
* Optional and nullable: an operation may declare no canonical schema, and the editor already
|
|
290
|
+
* gates on their presence. See the coalescing note in the component body — this is not cosmetic.
|
|
291
|
+
*/
|
|
292
|
+
canonicalRequestFields?: ConnectorCanonicalField[] | null;
|
|
293
|
+
/** Canonical response fields the editor renders one JSON-path input per. Same nullability. */
|
|
294
|
+
canonicalResponseFields?: ConnectorCanonicalField[] | null;
|
|
295
|
+
/** Currently-persisted request-field translation, or `null` if the operator hasn't supplied one. */
|
|
296
|
+
requestFieldMap: Record<string, string> | null;
|
|
297
|
+
/** Currently-persisted response-field translation, or `null` if the operator hasn't supplied one. */
|
|
298
|
+
responseFieldPaths: Record<string, string> | null;
|
|
299
|
+
/**
|
|
300
|
+
* Currently-persisted static request parameters (tool field name → constant literal value), or
|
|
301
|
+
* `null` if the operator hasn't supplied any. Seeds the "Static request parameters" rows.
|
|
302
|
+
*/
|
|
303
|
+
staticRequestFields: Record<string, string> | null;
|
|
304
|
+
/**
|
|
305
|
+
* Called with the operator's translation dictionaries when they click Save. Empty inputs are
|
|
306
|
+
* stripped before this fires; if every input on a side was blank, that side is delivered as `null`
|
|
307
|
+
* rather than `{}` so the consumer can pass it straight through and the BE writes a NULL column
|
|
308
|
+
* (which the resolver treats as "no operator override" and falls back to the connector's
|
|
309
|
+
* vendor-default translation). A populated dictionary means "rewrite these fields explicitly"; the
|
|
310
|
+
* BE persists it verbatim and the resolver applies it on top of vendor defaults.
|
|
311
|
+
*
|
|
312
|
+
* `staticRequestFields` carries the constant tool-request parameters (e.g. `{ method: "get" }`);
|
|
313
|
+
* same null-vs-populated semantics — `null` when every static row was left blank.
|
|
314
|
+
*/
|
|
315
|
+
onSave: (requestFieldMap: Record<string, string> | null, responseFieldPaths: Record<string, string> | null, staticRequestFields: Record<string, string> | null) => void;
|
|
316
|
+
onCancel: () => void;
|
|
317
|
+
/**
|
|
318
|
+
* Optional. When provided, the editor renders a Test section below the field tables and a "Test
|
|
319
|
+
* mapping" button inside it. The consumer supplies this callback so the editor never owns a
|
|
320
|
+
* network call. Invoked with the sample args and the current (unsaved) field maps — stripped to
|
|
321
|
+
* `null` when every input is blank, matching `onSave`.
|
|
322
|
+
*/
|
|
323
|
+
onTestMapping?: (request: TestToolMappingRequest) => Promise<TestToolMappingResult>;
|
|
324
|
+
/**
|
|
325
|
+
* Optional. When provided, the editor renders an "Auto-configure" button in the Test section. The
|
|
326
|
+
* consumer invokes the AI suggestion endpoint with the sample inputs the Test section collects,
|
|
327
|
+
* then returns the suggested field maps so the editor can populate the request / response inputs.
|
|
328
|
+
* The operator reviews and clicks Save.
|
|
329
|
+
*
|
|
330
|
+
* @param sampleArgs - Sample values the operator typed, one entry per canonical request field.
|
|
331
|
+
* @returns The suggestion result (success/error + proposed maps).
|
|
332
|
+
*/
|
|
333
|
+
onSuggestTranslation?: (sampleArgs: Record<string, unknown>) => Promise<ConnectorMappingSuggestionResult>;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Per-mapping translation editor — surfaced when the operator picks a tool whose schema doesn't
|
|
337
|
+
* match the operation's canonical request/response field names. Renders two stacked tables
|
|
338
|
+
* (request / response): left column shows the canonical field's name + kind chip + required
|
|
339
|
+
* indicator; right column captures the operator-supplied tool field name (request side) or JSON
|
|
340
|
+
* path (response side).
|
|
341
|
+
*
|
|
342
|
+
* <p><b>Identity semantics.</b> An empty input on a field means "identity — pass the canonical name
|
|
343
|
+
* through unchanged". On Save the editor strips empty values; if every input on a side was blank,
|
|
344
|
+
* that side is delivered to `onSave` as <code>null</code> (not <code>{}</code>) so the BE writes a
|
|
345
|
+
* NULL column. The resolver then treats the NULL as "no operator override" and falls back to the
|
|
346
|
+
* connector's vendor-default translation; a populated dictionary means "rewrite these fields
|
|
347
|
+
* explicitly" and applies on top of the vendor default.</p>
|
|
348
|
+
*
|
|
349
|
+
* <p><b>State seeding.</b> The consumer page mounts/unmounts this dialog every time the editor opens
|
|
350
|
+
* (the `open` prop is always `true` in practice — the parent gates rendering via conditional mount),
|
|
351
|
+
* so local state initialises lazily from the props on each mount. This avoids the "setState in
|
|
352
|
+
* useEffect" pattern the eslint react-hooks ruleset blocks, AND keeps the operator's in-flight edits
|
|
353
|
+
* sovereign once the dialog is open — a concurrent refresh of the mapping row doesn't clobber what
|
|
354
|
+
* they're typing.</p>
|
|
355
|
+
*
|
|
356
|
+
* <p><b>Promoted, not designed here.</b> This file existed FOUR times — the host's gogo-ui adapter
|
|
357
|
+
* plus `self-healing`, `tech` and `timesheets` — and the copies had already drifted. Three
|
|
358
|
+
* differences were adjudicated on the way in, and each is a reason the duplication was expensive:</p>
|
|
359
|
+
*
|
|
360
|
+
* <ol>
|
|
361
|
+
* <li><b>The Test button is gated on `onTestMapping`.</b> The enclosing block is gated on
|
|
362
|
+
* <code>(onTestMapping || onSuggestTranslation)</code>, so without the inner guard a consumer
|
|
363
|
+
* supplying only <code>onSuggestTranslation</code> rendered a "Test mapping" button whose
|
|
364
|
+
* handler returns immediately — a DEAD CONTROL. `self-healing` shipped that; `tech` and
|
|
365
|
+
* `timesheets` had fixed it. Nothing noticed, because there was no single source to notice
|
|
366
|
+
* it against.</li>
|
|
367
|
+
* <li><b>Backgrounds come from the theme (`action.hover`), never a hardcoded
|
|
368
|
+
* <code>rgba()</code>.</b> A literal <code>rgba(0,0,0,0.03)</code> is invisible in dark mode
|
|
369
|
+
* and is what `coreconnect/no-hardcoded-colors` exists to stop.</li>
|
|
370
|
+
* <li><b>No emoji in chip labels.</b> The chips already carry an icon and a semantic colour, so
|
|
371
|
+
* the emoji was a third, untranslatable, screen-reader-hostile encoding of the same state.</li>
|
|
372
|
+
* </ol>
|
|
373
|
+
*
|
|
374
|
+
* <p>The null tolerance below came from the HOST copy, which was the most hardened of the four.
|
|
375
|
+
* `coreconnect/no-local-mapping-editor` stops a fifth being written.</p>
|
|
376
|
+
*/
|
|
377
|
+
declare function ConnectorMappingTranslationEditorView({ open, operationLabel, toolName, canonicalRequestFields: canonicalRequestFieldsProp, canonicalResponseFields: canonicalResponseFieldsProp, requestFieldMap, responseFieldPaths, staticRequestFields, onSave, onCancel, onTestMapping, onSuggestTranslation, }: ConnectorMappingTranslationEditorProps): react.JSX.Element;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Every operation with a gap, required or optional. Use it to decide whether to mount a banner at
|
|
381
|
+
* all, without duplicating the predicate.
|
|
382
|
+
*/
|
|
383
|
+
declare function selectIncompleteOperations(operations: MappingCompletenessItem[]): MappingCompletenessItem[];
|
|
384
|
+
/**
|
|
385
|
+
* REQUIRED operations with a gap. These genuinely block the module: the connector cannot run them.
|
|
386
|
+
*/
|
|
387
|
+
declare function selectRequiredIncompleteOperations(operations: MappingCompletenessItem[]): MappingCompletenessItem[];
|
|
388
|
+
/**
|
|
389
|
+
* OPTIONAL operations with a gap. Informational only: the operation powers an extra capability that
|
|
390
|
+
* stays off until its fields are mapped, and the connector may legitimately expose no suitable tool.
|
|
391
|
+
*
|
|
392
|
+
* @remarks
|
|
393
|
+
* Treat these as a note, never a warning. A connector need not expose every operation a module can
|
|
394
|
+
* use, so a correctly configured connector can sit here indefinitely — and a banner that never
|
|
395
|
+
* clears is a banner operators learn to ignore, which costs you the required warnings too.
|
|
396
|
+
*/
|
|
397
|
+
declare function selectOptionalIncompleteOperations(operations: MappingCompletenessItem[]): MappingCompletenessItem[];
|
|
398
|
+
/** A module's readiness, counted over REQUIRED operations only. */
|
|
399
|
+
interface MappingReadiness {
|
|
400
|
+
/** Required operations that are mapped AND have no field gaps. */
|
|
401
|
+
complete: number;
|
|
402
|
+
/** Total required operations declared by the module's catalogue. */
|
|
403
|
+
total: number;
|
|
404
|
+
/** True when every required operation is mapped and complete. */
|
|
405
|
+
isReady: boolean;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Readiness over REQUIRED operations only, for a progress indicator or a "ready" badge.
|
|
409
|
+
*
|
|
410
|
+
* @remarks
|
|
411
|
+
* Optional operations are excluded on purpose. Counting them would leave a properly configured
|
|
412
|
+
* connector permanently short of 100%, for operations the operator was never expected to map. This
|
|
413
|
+
* rule came from `ethisyscore-plugin-timesheets`, which had it right while another plugin was
|
|
414
|
+
* reporting optional field gaps in a standing banner.
|
|
415
|
+
*/
|
|
416
|
+
declare function selectMappingReadiness(operations: MappingCompletenessItem[]): MappingReadiness;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* React-query keys for a connector's tool mappings and the completeness read derived from them.
|
|
420
|
+
*
|
|
421
|
+
* @remarks
|
|
422
|
+
* The completeness key is built as a CHILD of the mappings key, and that nesting is the whole point
|
|
423
|
+
* of shipping these from the SDK rather than letting each plugin declare its own.
|
|
424
|
+
*
|
|
425
|
+
* Completeness is derived state: it is the mappings joined against the live tool schemas. Every write
|
|
426
|
+
* that changes a mapping can change it. When the two keys are siblings, a mutation has to remember to
|
|
427
|
+
* invalidate both, and one plugin duly invalidated only the mappings key — so its "operations aren't
|
|
428
|
+
* fully mapped" banner went stale after every save, clear and auto-match, and stayed stale until the
|
|
429
|
+
* operator navigated away. Nesting means a single `invalidateQueries({ queryKey: mappings })` takes
|
|
430
|
+
* the completeness read with it, and there is nothing left to forget.
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* ```ts
|
|
434
|
+
* const mappings = connectorMappingsQueryKey(connectorId, moduleKey);
|
|
435
|
+
*
|
|
436
|
+
* useQuery({ queryKey: mappings, queryFn: ... });
|
|
437
|
+
* useQuery({ queryKey: connectorMappingCompletenessQueryKey(connectorId, moduleKey), queryFn: ... });
|
|
438
|
+
*
|
|
439
|
+
* // One call. Covers both, and any future query keyed beneath the mappings key.
|
|
440
|
+
* queryClient.invalidateQueries({ queryKey: mappings });
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
/**
|
|
444
|
+
* Key for the mappings list of one connector + module.
|
|
445
|
+
*
|
|
446
|
+
* `connectorId` is accepted as possibly-undefined because the page renders before a connector is
|
|
447
|
+
* chosen; pair it with `enabled: !!connectorId` rather than branching on the key.
|
|
448
|
+
*/
|
|
449
|
+
declare function connectorMappingsQueryKey(connectorId: string | undefined, moduleKey: string): readonly ["connector-mappings", string | undefined, string];
|
|
450
|
+
/**
|
|
451
|
+
* The whole family's prefix, for the one invalidation that has to cross connectors.
|
|
452
|
+
*
|
|
453
|
+
* Almost every write is scoped to one connector and should invalidate only that connector's subtree
|
|
454
|
+
* via {@link connectorMappingsQueryKey}. Changing a module's PROVIDER is the exception: it moves the
|
|
455
|
+
* module from one provider's rows to another's, so the subtree that must be discarded is the one
|
|
456
|
+
* being left, which the new scope's key does not name.
|
|
457
|
+
*/
|
|
458
|
+
declare function connectorMappingsRootQueryKey(): readonly ["connector-mappings"];
|
|
459
|
+
/**
|
|
460
|
+
* Key for the mapping rows of a module served by an installed application.
|
|
461
|
+
*
|
|
462
|
+
* A sibling of {@link connectorMappingsQueryKey} rather than a variant of it, because the read
|
|
463
|
+
* behind it is a different one: the connector list read is scoped by connector id and a
|
|
464
|
+
* plugin-target row has none. Keyed under the same root so the family invalidation covers it.
|
|
465
|
+
*/
|
|
466
|
+
declare function pluginTargetMappingsQueryKey(moduleKey: string): readonly ["connector-mappings", "plugin-target", string];
|
|
467
|
+
/**
|
|
468
|
+
* Key for the completeness read, nested UNDER {@link connectorMappingsQueryKey} so invalidating the
|
|
469
|
+
* mappings prefix always covers it. Do not flatten this into a sibling key.
|
|
470
|
+
*/
|
|
471
|
+
declare function connectorMappingCompletenessQueryKey(connectorId: string | undefined, moduleKey: string): readonly ["connector-mappings", string | undefined, string, "completeness"];
|
|
472
|
+
|
|
473
|
+
export { type ConnectorCanonicalField as C, MappingCompletenessBanner as M, PLUGIN_PROVIDER_CONTROL_LABEL as P, type TestToolMappingExtractedField as T, CONNECTOR_CONTROL_LABEL as a, type ConnectorCanonicalFieldKind as b, type ConnectorMappingSuggestionResult as c, type ConnectorMappingTranslationEditorProps as d, ConnectorMappingTranslationEditorView as e, type MappingCompletenessBannerProps as f, type MappingCompletenessItem as g, type MappingCompletenessResult as h, type MappingReadiness as i, PROVIDER_CONTROL_LABEL as j, PROVIDER_OPTION_LABEL as k, type TestToolMappingRequest as l, type TestToolMappingResult as m, connectorMappingCompletenessQueryKey as n, connectorMappingsQueryKey as o, connectorMappingsRootQueryKey as p, pluginTargetMappingsQueryKey as q, selectMappingReadiness as r, selectIncompleteOperations as s, selectOptionalIncompleteOperations as t, selectRequiredIncompleteOperations as u };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Dispatches an MCP tool call by name. The seam a lifted REST service binds to. */
|
|
2
|
+
type ToolInvoker = (toolName: string, args: unknown) => Promise<unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Base for service classes lifted from a monolith REST plane onto MCP.
|
|
5
|
+
*
|
|
6
|
+
* The monolith versions extend a REST base (`this.get`/`this.post` over axios).
|
|
7
|
+
* The plugin runtime exposes no imperative MCP transport — only the
|
|
8
|
+
* `useMcpTool` hook — so a `useXService()` hook composes per-tool invokers (see
|
|
9
|
+
* {@link useToolInvokerMap}) into a single {@link ToolInvoker} and constructs
|
|
10
|
+
* the class with it. Method bodies call `this.tool("<tool-name>", args)`.
|
|
11
|
+
*/
|
|
12
|
+
declare abstract class BaseMcpService {
|
|
13
|
+
private readonly invoker;
|
|
14
|
+
constructor(invoker: ToolInvoker);
|
|
15
|
+
protected tool<TRes>(name: string, args?: unknown): Promise<TRes>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Thrown by a lifted service method whose monolith REST route has no MCP tool
|
|
19
|
+
* yet (typically because it needs client-side aggregation over a coarser tool).
|
|
20
|
+
*/
|
|
21
|
+
declare function notViaMcp(serviceName: string, method: string): never;
|
|
22
|
+
/** Imperative invoker for a single MCP tool (the hook's `.invoke`, stable across renders). */
|
|
23
|
+
declare function useToolInvoker<TReq, TRes>(toolName: string): (req: TReq) => Promise<TRes>;
|
|
24
|
+
/**
|
|
25
|
+
* Composes one {@link ToolInvoker} dispatching over every tool in `tools`, so a
|
|
26
|
+
* service class can call tools by name.
|
|
27
|
+
*
|
|
28
|
+
* Rules of hooks: `tools` MUST be a frozen, constant-length list (e.g. a
|
|
29
|
+
* module-level `as const` array) so the per-tool `useMcpTool` calls run in a
|
|
30
|
+
* stable order on every render. Passing a list whose length varies between
|
|
31
|
+
* renders is a violation and will break.
|
|
32
|
+
*/
|
|
33
|
+
declare function useToolInvokerMap(tools: readonly string[]): ToolInvoker;
|
|
34
|
+
|
|
35
|
+
export { BaseMcpService as B, type ToolInvoker as T, useToolInvokerMap as a, notViaMcp as n, useToolInvoker as u };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Dispatches an MCP tool call by name. The seam a lifted REST service binds to. */
|
|
2
|
+
type ToolInvoker = (toolName: string, args: unknown) => Promise<unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Base for service classes lifted from a monolith REST plane onto MCP.
|
|
5
|
+
*
|
|
6
|
+
* The monolith versions extend a REST base (`this.get`/`this.post` over axios).
|
|
7
|
+
* The plugin runtime exposes no imperative MCP transport — only the
|
|
8
|
+
* `useMcpTool` hook — so a `useXService()` hook composes per-tool invokers (see
|
|
9
|
+
* {@link useToolInvokerMap}) into a single {@link ToolInvoker} and constructs
|
|
10
|
+
* the class with it. Method bodies call `this.tool("<tool-name>", args)`.
|
|
11
|
+
*/
|
|
12
|
+
declare abstract class BaseMcpService {
|
|
13
|
+
private readonly invoker;
|
|
14
|
+
constructor(invoker: ToolInvoker);
|
|
15
|
+
protected tool<TRes>(name: string, args?: unknown): Promise<TRes>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Thrown by a lifted service method whose monolith REST route has no MCP tool
|
|
19
|
+
* yet (typically because it needs client-side aggregation over a coarser tool).
|
|
20
|
+
*/
|
|
21
|
+
declare function notViaMcp(serviceName: string, method: string): never;
|
|
22
|
+
/** Imperative invoker for a single MCP tool (the hook's `.invoke`, stable across renders). */
|
|
23
|
+
declare function useToolInvoker<TReq, TRes>(toolName: string): (req: TReq) => Promise<TRes>;
|
|
24
|
+
/**
|
|
25
|
+
* Composes one {@link ToolInvoker} dispatching over every tool in `tools`, so a
|
|
26
|
+
* service class can call tools by name.
|
|
27
|
+
*
|
|
28
|
+
* Rules of hooks: `tools` MUST be a frozen, constant-length list (e.g. a
|
|
29
|
+
* module-level `as const` array) so the per-tool `useMcpTool` calls run in a
|
|
30
|
+
* stable order on every render. Passing a list whose length varies between
|
|
31
|
+
* renders is a violation and will break.
|
|
32
|
+
*/
|
|
33
|
+
declare function useToolInvokerMap(tools: readonly string[]): ToolInvoker;
|
|
34
|
+
|
|
35
|
+
export { BaseMcpService as B, type ToolInvoker as T, useToolInvokerMap as a, notViaMcp as n, useToolInvoker as u };
|
|
@@ -203,6 +203,50 @@ function useOrgFormatSync() {
|
|
|
203
203
|
return () => controller.abort();
|
|
204
204
|
}, [transport, organisationId]);
|
|
205
205
|
}
|
|
206
|
+
var warnedAboutMissingAccessor2 = false;
|
|
207
|
+
var useOptionalExtensionRuntimeTransport4 = typeof extensionRuntime__namespace.useOptionalExtensionRuntimeTransport === "function" ? extensionRuntime__namespace.useOptionalExtensionRuntimeTransport : () => {
|
|
208
|
+
if (!warnedAboutMissingAccessor2) {
|
|
209
|
+
warnedAboutMissingAccessor2 = true;
|
|
210
|
+
console.warn(
|
|
211
|
+
"[plugin-ui] The host realm does not expose useOptionalExtensionRuntimeTransport (host SDK predates 1.88.0). The organisation reporting currency falls back to GBP for this bundle. Bump the host's @ethisyscore/extension-runtime pin to restore it."
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
};
|
|
216
|
+
var REPORTING_CURRENCY_TOOL = "settings:get-organisation-reporting-currency";
|
|
217
|
+
function useReportingCurrencySync() {
|
|
218
|
+
const transport = useOptionalExtensionRuntimeTransport4();
|
|
219
|
+
const organisationId = extensionRuntime__namespace.useHostIdentity()?.organisationId ?? null;
|
|
220
|
+
const [, bumpVersion] = react.useState(0);
|
|
221
|
+
const previousOrganisationId = react.useRef(void 0);
|
|
222
|
+
if (previousOrganisationId.current !== organisationId) {
|
|
223
|
+
previousOrganisationId.current = organisationId;
|
|
224
|
+
money.setOrgReportingCurrency(null);
|
|
225
|
+
}
|
|
226
|
+
react.useEffect(() => {
|
|
227
|
+
money.setOrgReportingCurrency(null);
|
|
228
|
+
bumpVersion((v) => v + 1);
|
|
229
|
+
if (!transport) return;
|
|
230
|
+
const controller = new AbortController();
|
|
231
|
+
transport.invokeTool(
|
|
232
|
+
REPORTING_CURRENCY_TOOL,
|
|
233
|
+
{},
|
|
234
|
+
controller.signal
|
|
235
|
+
).then((result) => {
|
|
236
|
+
if (controller.signal.aborted) return;
|
|
237
|
+
const currency = result?.currency ?? null;
|
|
238
|
+
money.setOrgReportingCurrency(
|
|
239
|
+
currency ? { code: currency.code, decimalPlaces: currency.decimalPlaces } : null
|
|
240
|
+
);
|
|
241
|
+
bumpVersion((v) => v + 1);
|
|
242
|
+
}).catch(() => {
|
|
243
|
+
if (controller.signal.aborted) return;
|
|
244
|
+
money.setOrgReportingCurrency(null);
|
|
245
|
+
bumpVersion((v) => v + 1);
|
|
246
|
+
});
|
|
247
|
+
return () => controller.abort();
|
|
248
|
+
}, [transport, organisationId]);
|
|
249
|
+
}
|
|
206
250
|
|
|
207
251
|
// src/platform-react/definePlatformReactPluginPage.tsx
|
|
208
252
|
function makeDefaultQueryClient() {
|
|
@@ -229,6 +273,7 @@ function definePlatformReactPluginPage(Page, options) {
|
|
|
229
273
|
} = options;
|
|
230
274
|
function PluginPage(props) {
|
|
231
275
|
useOrgFormatSync();
|
|
276
|
+
useReportingCurrencySync();
|
|
232
277
|
const page = react.createElement(Page, props);
|
|
233
278
|
const body = wrapPage ? wrapPage(page) : page;
|
|
234
279
|
const surfaceTree = react.createElement(
|
|
@@ -390,7 +435,7 @@ function useAuthenticatedQuery(options) {
|
|
|
390
435
|
function useAuthenticatedQueries(queries) {
|
|
391
436
|
return reactQuery.useQueries({ queries });
|
|
392
437
|
}
|
|
393
|
-
var
|
|
438
|
+
var REPORTING_CURRENCY_TOOL2 = "settings:get-organisation-reporting-currency";
|
|
394
439
|
var LIST_CURRENCIES_TOOL = "settings:list-currencies";
|
|
395
440
|
var emptyCatalogue = () => /* @__PURE__ */ new Map();
|
|
396
441
|
var SETTLED_FULFILLED = "fulfilled";
|
|
@@ -411,7 +456,7 @@ function useCurrency() {
|
|
|
411
456
|
const controller = new AbortController();
|
|
412
457
|
Promise.allSettled([
|
|
413
458
|
transport.invokeTool(
|
|
414
|
-
|
|
459
|
+
REPORTING_CURRENCY_TOOL2,
|
|
415
460
|
{},
|
|
416
461
|
controller.signal
|
|
417
462
|
),
|
|
@@ -423,7 +468,8 @@ function useCurrency() {
|
|
|
423
468
|
]).then(([reporting, list]) => {
|
|
424
469
|
if (controller.signal.aborted) return;
|
|
425
470
|
if (reporting.status === SETTLED_FULFILLED) {
|
|
426
|
-
|
|
471
|
+
const resolved = reporting.value?.currency ?? null;
|
|
472
|
+
setReportingCurrency(resolved);
|
|
427
473
|
}
|
|
428
474
|
if (list.status === SETTLED_FULFILLED && Array.isArray(list.value)) {
|
|
429
475
|
setCurrencies(new Map(list.value.map((currency) => [currency.id, currency])));
|
|
@@ -721,6 +767,7 @@ function definePlatformReactPluginOverlay(Overlay, options) {
|
|
|
721
767
|
const { rootClassName, styleId, css, manifest = {}, queryClient = defaultQueryClient2, wrapOverlay } = options;
|
|
722
768
|
return function MountedOverlay(props) {
|
|
723
769
|
useOrgFormatSync();
|
|
770
|
+
useReportingCurrencySync();
|
|
724
771
|
const tree = react.createElement(
|
|
725
772
|
OverlayHostContext.Provider,
|
|
726
773
|
{ value: props.overlayHost },
|
|
@@ -1057,7 +1104,7 @@ exports.OVERLAY_HOST_CONTRACT_VERSION = OVERLAY_HOST_CONTRACT_VERSION;
|
|
|
1057
1104
|
exports.OverlayHostContext = OverlayHostContext;
|
|
1058
1105
|
exports.PluginPortalScope = PluginPortalScope;
|
|
1059
1106
|
exports.PluginStyleScope = PluginStyleScope;
|
|
1060
|
-
exports.REPORTING_CURRENCY_TOOL =
|
|
1107
|
+
exports.REPORTING_CURRENCY_TOOL = REPORTING_CURRENCY_TOOL2;
|
|
1061
1108
|
exports.SIDEBAR_MODE = SIDEBAR_MODE;
|
|
1062
1109
|
exports.SurfaceBaseContext = SurfaceBaseContext;
|
|
1063
1110
|
exports.TemplateContext = TemplateContext;
|
|
@@ -1091,6 +1138,7 @@ exports.useMcpUpload = useMcpUpload;
|
|
|
1091
1138
|
exports.useOrgFormatSync = useOrgFormatSync;
|
|
1092
1139
|
exports.useOverlayHost = useOverlayHost;
|
|
1093
1140
|
exports.usePluginRealtime = usePluginRealtime;
|
|
1141
|
+
exports.useReportingCurrencySync = useReportingCurrencySync;
|
|
1094
1142
|
exports.useSurfaceUrl = useSurfaceUrl;
|
|
1095
1143
|
exports.useToolInvoker = useToolInvoker;
|
|
1096
1144
|
exports.useToolInvokerMap = useToolInvokerMap;
|