@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.
@@ -0,0 +1,1061 @@
1
+ 'use strict';
2
+
3
+ var reactQuery = require('@tanstack/react-query');
4
+ var react = require('react');
5
+
6
+ // src/connector-mappings/ConnectorMappingService.ts
7
+ function pluginTargetSurfaceUnavailable(method, detail) {
8
+ throw new Error(
9
+ `ConnectorMappingService.${method} is not available. ${detail} Subclass this service with your own implementation, or leave the plugin-target surface unmounted (isPluginTargetSurfaceAvailable is false on this implementation).`
10
+ );
11
+ }
12
+ var ConnectorMappingService = class {
13
+ constructor(invoker) {
14
+ this.invoker = invoker;
15
+ }
16
+ invoker;
17
+ /** Single seam every method goes through, so a subclass can wrap logging or retries in one place. */
18
+ async tool(name, args = {}) {
19
+ return await this.invoker(name, args);
20
+ }
21
+ async getMappings(connectorId, moduleKey) {
22
+ const response = await this.tool("connectors:mappings:list", {
23
+ connectorId,
24
+ moduleKey
25
+ });
26
+ return response?.items ?? [];
27
+ }
28
+ async updateMapping(connectorId, moduleKey, operationKey, toolName, rowVersion, requestFieldMap, responseFieldPaths, staticRequestFields) {
29
+ const response = await this.tool("connectors:mappings:set", {
30
+ connectorId,
31
+ moduleKey,
32
+ operationKey,
33
+ toolName,
34
+ rowVersion,
35
+ // Coalesced to explicit nulls rather than left `undefined`. The host treats both the same
36
+ // way, but an explicit null states on the wire that the caller MEANT no override, which is
37
+ // what makes a request log readable when a translation goes missing.
38
+ requestFieldMap: requestFieldMap ?? null,
39
+ responseFieldPaths: responseFieldPaths ?? null,
40
+ staticRequestFields: staticRequestFields ?? null
41
+ });
42
+ return response?.rowVersion ?? null;
43
+ }
44
+ async deleteMapping(connectorId, moduleKey, operationKey, rowVersion) {
45
+ await this.tool("connectors:mappings:delete", {
46
+ connectorId,
47
+ moduleKey,
48
+ operationKey,
49
+ rowVersion
50
+ });
51
+ }
52
+ async autoMatch(connectorId, moduleKey) {
53
+ const response = await this.tool(
54
+ "connectors:mappings:auto-match",
55
+ { connectorId, moduleKey }
56
+ );
57
+ return {
58
+ items: response?.items ?? [],
59
+ aiAvailable: response?.aiAvailable ?? false,
60
+ aiUnavailableReason: response?.aiUnavailableReason ?? null,
61
+ unmatchedRequiredCount: response?.unmatchedRequiredCount ?? 0,
62
+ conflictReason: response?.conflictReason ?? null
63
+ };
64
+ }
65
+ async getMappingCompleteness(connectorId, moduleKey) {
66
+ const response = await this.tool(
67
+ "connectors:mappings:completeness",
68
+ { connectorId, moduleKey }
69
+ );
70
+ return response ?? { operations: [] };
71
+ }
72
+ async suggestMappingTranslation(connectorId, moduleKey, body) {
73
+ const response = await this.tool(
74
+ "connectors:mappings:suggest-translation",
75
+ {
76
+ connectorId,
77
+ moduleKey,
78
+ operationKey: body.operationKey,
79
+ toolName: body.toolName,
80
+ // The host declares SampleArgs non-nullable; an absent sample is an empty object, not null.
81
+ sampleArgs: body.sampleArgs ?? {}
82
+ }
83
+ );
84
+ return response ?? {
85
+ success: false,
86
+ error: "The host returned no suggestion.",
87
+ // Empty objects, not null: the contract types these as `Record<string, string>`, and an
88
+ // empty map is the accurate statement — no rewrites proposed.
89
+ requestFieldMap: {},
90
+ responseFieldPaths: {}
91
+ };
92
+ }
93
+ async testMapping(connectorId, moduleKey, body) {
94
+ const response = await this.tool("connectors:mappings:test", {
95
+ connectorId,
96
+ moduleKey,
97
+ operationKey: body.operationKey,
98
+ toolName: body.toolName,
99
+ requestFieldMap: body.requestFieldMap,
100
+ responseFieldPaths: body.responseFieldPaths,
101
+ staticRequestFields: body.staticRequestFields,
102
+ sampleArgs: body.sampleArgs
103
+ });
104
+ return response ?? {
105
+ success: false,
106
+ error: "The host returned no result for the mapping test.",
107
+ rawResponseJson: null,
108
+ extractedFields: []
109
+ };
110
+ }
111
+ // -------------------------------------------------------------------------
112
+ // IConnectorEnumerationService — optional, see the interface's remarks
113
+ // -------------------------------------------------------------------------
114
+ async listConnectors() {
115
+ const response = await this.tool(
116
+ "settings:list-connectors",
117
+ {}
118
+ );
119
+ return (response?.items ?? []).map((c) => ({
120
+ id: c.id,
121
+ name: c.name,
122
+ slug: c.slug ?? null,
123
+ isEnabled: c.isEnabled
124
+ }));
125
+ }
126
+ async getModuleProvider(moduleKey) {
127
+ const response = await this.tool("connectors:mappings:get-provider", {
128
+ moduleKey
129
+ });
130
+ return {
131
+ targetKind: response?.targetKind ?? null,
132
+ connectorId: response?.connectorId ?? null,
133
+ targetExtensionId: response?.targetExtensionId ?? null,
134
+ rowVersion: response?.rowVersion ?? null
135
+ };
136
+ }
137
+ async setModuleProvider(moduleKey, targetKind, connectorId, targetExtensionId, rowVersion) {
138
+ const response = await this.tool("connectors:mappings:set-provider", {
139
+ moduleKey,
140
+ targetKind,
141
+ connectorId,
142
+ targetExtensionId,
143
+ rowVersion
144
+ });
145
+ return {
146
+ targetKind: response?.targetKind ?? targetKind,
147
+ connectorId: response?.connectorId ?? null,
148
+ targetExtensionId: response?.targetExtensionId ?? null,
149
+ rowVersion: response?.rowVersion ?? null
150
+ };
151
+ }
152
+ async listConnectorTools(connectorId) {
153
+ const response = await this.tool(
154
+ "settings:get-connector",
155
+ { id: connectorId }
156
+ );
157
+ return response?.tools ?? [];
158
+ }
159
+ // -------------------------------------------------------------------------
160
+ // The PLUGIN-TARGET surface - declared, and deliberately not reachable yet
161
+ //
162
+ // The per-module PROVIDER choice above is reachable: CoreConnect-Api #1329 added
163
+ // `connectors:mappings:get-provider` and `connectors:mappings:set-provider`, so an operator can
164
+ // record that a module is served by an installed app. What they still cannot do from a plugin UI
165
+ // is everything BELOW that choice:
166
+ //
167
+ // - the candidate list has no endpoint in ANY transport, so who is on offer has to be supplied to
168
+ // this package rather than fetched by it;
169
+ // - `connectors:mappings:list` takes a connectorId and its rows carry no TargetKind or
170
+ // TargetExtensionId, so there is no connector-less read for a plugin-served module;
171
+ // - `SetPluginTargetMapping` exists as a REST route only
172
+ // (`PUT /api/v1/connectors/modules/{moduleKey}/operations/{operationKey}/plugin-target`), and a
173
+ // plugin's UI reaches the host over MCP, so the per-operation plugin-target write cannot be
174
+ // called from here.
175
+ //
176
+ // Implemented as refusals rather than omitted so the shape a real transport must satisfy is
177
+ // written down in one place, and so the gap is a message an operator's log can carry rather than
178
+ // a method that silently is not there. `isPluginTargetSurfaceAvailable` is the flag a caller
179
+ // reads; none of the methods below is called while it is false.
180
+ // -------------------------------------------------------------------------
181
+ /**
182
+ * Whether this transport can serve the plugin-target methods below. `false` here, and the three
183
+ * methods throw, for the reasons in the block comment above.
184
+ *
185
+ * A property and not a guess: "the method exists" and "the method works" are different facts, and
186
+ * a caller that probed by calling and catching would turn a known gap into a request, an error
187
+ * toast and a support ticket.
188
+ */
189
+ isPluginTargetSurfaceAvailable = false;
190
+ async listPluginProviders(moduleKey) {
191
+ pluginTargetSurfaceUnavailable(
192
+ "listPluginProviders",
193
+ "The host exposes no candidate listing in any transport. Its routable-tool listing is documented as unsafe to show an end user verbatim because it applies no caller-permission filter, and the write path additionally refuses any tool the consuming application did not declare in its manifest - so the candidate set is a server-side decision, not a client-side filter of a broader list."
194
+ );
195
+ }
196
+ async getPluginTargetMappings(moduleKey) {
197
+ pluginTargetSurfaceUnavailable(
198
+ "getPluginTargetMappings",
199
+ "The host's mapping list read is scoped BY connector (connectors:mappings:list takes a connectorId) and its rows carry no TargetKind or TargetExtensionId, so a plugin-target row has neither a connector to scope it by nor a way to be recognised in the result. No connector-less list exists."
200
+ );
201
+ }
202
+ async setPluginTargetMapping(moduleKey, args) {
203
+ pluginTargetSurfaceUnavailable(
204
+ "setPluginTargetMapping",
205
+ "The host serves PUT /api/v1/connectors/modules/{moduleKey}/operations/{operationKey}/plugin-target over REST only; there is no connectors:mappings:* tool for it, and a plugin's UI reaches the host over MCP."
206
+ );
207
+ }
208
+ };
209
+ var CONNECTOR_MAPPING_TOOLS = [
210
+ "connectors:mappings:list",
211
+ "connectors:mappings:set",
212
+ "connectors:mappings:delete",
213
+ "connectors:mappings:auto-match",
214
+ "connectors:mappings:completeness",
215
+ "connectors:mappings:suggest-translation",
216
+ "connectors:mappings:test",
217
+ "connectors:mappings:get-provider",
218
+ "connectors:mappings:set-provider"
219
+ ];
220
+ var CONNECTOR_ENUMERATION_TOOLS = [
221
+ "settings:list-connectors",
222
+ "settings:get-connector"
223
+ ];
224
+
225
+ // src/components/connectors/mappingQueryKeys.ts
226
+ var CONNECTOR_MAPPINGS_ROOT = "connector-mappings";
227
+ function connectorMappingsQueryKey(connectorId, moduleKey) {
228
+ return [CONNECTOR_MAPPINGS_ROOT, connectorId, moduleKey];
229
+ }
230
+ function connectorMappingsRootQueryKey() {
231
+ return [CONNECTOR_MAPPINGS_ROOT];
232
+ }
233
+ function pluginTargetMappingsQueryKey(moduleKey) {
234
+ return [CONNECTOR_MAPPINGS_ROOT, "plugin-target", moduleKey];
235
+ }
236
+ function connectorMappingCompletenessQueryKey(connectorId, moduleKey) {
237
+ return [...connectorMappingsQueryKey(connectorId, moduleKey), "completeness"];
238
+ }
239
+
240
+ // src/components/connectors/mappingCompleteness.ts
241
+ function hasUnmappedRequiredField(op) {
242
+ return op.unmappedRequiredCanonicalFields.length > 0;
243
+ }
244
+ function selectIncompleteOperations(operations) {
245
+ return operations.filter(hasUnmappedRequiredField);
246
+ }
247
+ function selectRequiredIncompleteOperations(operations) {
248
+ return operations.filter((op) => op.isRequired && hasUnmappedRequiredField(op));
249
+ }
250
+ function selectOptionalIncompleteOperations(operations) {
251
+ return operations.filter((op) => !op.isRequired && hasUnmappedRequiredField(op));
252
+ }
253
+ function selectMappingReadiness(operations) {
254
+ const required = operations.filter((op) => op.isRequired);
255
+ const complete = required.filter(
256
+ (op) => op.isToolMapped && op.unmappedRequiredCanonicalFields.length === 0
257
+ ).length;
258
+ return {
259
+ complete,
260
+ total: required.length,
261
+ isReady: required.length > 0 && complete === required.length
262
+ };
263
+ }
264
+ function useAuthenticatedQuery(options) {
265
+ const result = reactQuery.useQuery(options);
266
+ return {
267
+ ...result,
268
+ isLoading: result.isLoading,
269
+ isAuthLoading: false
270
+ };
271
+ }
272
+
273
+ // src/connector-mappings/useConnectorMappings.ts
274
+ function useConnectorMappings({
275
+ service,
276
+ connectorId,
277
+ moduleKey
278
+ }) {
279
+ const query = useAuthenticatedQuery({
280
+ queryKey: connectorMappingsQueryKey(connectorId, moduleKey),
281
+ queryFn: () => service.getMappings(connectorId, moduleKey),
282
+ enabled: !!connectorId && !!moduleKey,
283
+ meta: { errorTitle: "Error loading tool mappings" }
284
+ });
285
+ return {
286
+ mappings: query.data ?? [],
287
+ isLoading: query.isLoading,
288
+ isFetching: query.isFetching,
289
+ dataUpdatedAt: query.dataUpdatedAt,
290
+ error: query.error ?? void 0,
291
+ refetch: () => void query.refetch()
292
+ };
293
+ }
294
+ function useConnectorMappingCompleteness({
295
+ service,
296
+ connectorId,
297
+ moduleKey
298
+ }) {
299
+ const query = useAuthenticatedQuery({
300
+ queryKey: connectorMappingCompletenessQueryKey(connectorId, moduleKey),
301
+ queryFn: () => service.getMappingCompleteness(connectorId, moduleKey),
302
+ enabled: !!connectorId && !!moduleKey,
303
+ meta: { errorTitle: "Error loading mapping completeness" }
304
+ });
305
+ const operations = query.data?.operations ?? [];
306
+ return {
307
+ operations,
308
+ readiness: selectMappingReadiness(operations),
309
+ isLoading: query.isLoading,
310
+ isFetching: query.isFetching
311
+ };
312
+ }
313
+
314
+ // src/components/shared/toast.ts
315
+ function dispatchToast(title, description, variant) {
316
+ if (typeof window === "undefined" || title.length === 0) {
317
+ return;
318
+ }
319
+ window.dispatchEvent(new CustomEvent("ethisys:toast", { detail: { title, description, variant } }));
320
+ }
321
+ function showSuccessToast(title, description) {
322
+ dispatchToast(title, description, "success");
323
+ }
324
+
325
+ // src/connector-mappings/types.ts
326
+ var MAPPING_SOURCE = {
327
+ keyword: "keyword",
328
+ ai: "ai",
329
+ manual: "manual"
330
+ };
331
+ var AI_UNAVAILABLE_REASON = {
332
+ Timeout: "timeout",
333
+ ProviderError: "provider_error",
334
+ ParseError: "parse_error",
335
+ ContextTooLarge: "context_too_large",
336
+ NoConnectorConfigured: "no_connector_configured"
337
+ };
338
+ var AUTO_MATCH_CONFLICT_REASON = {
339
+ AnotherAutoMatchInProgress: "another_auto_match_in_progress"
340
+ };
341
+ var MAPPING_TARGET_KIND = {
342
+ connector: "connector",
343
+ plugin: "plugin"
344
+ };
345
+ var MODULE_PROVIDER_KIND = {
346
+ Connector: "Connector",
347
+ Plugin: "Plugin"
348
+ };
349
+ var MAPPING_TARGET_KIND_WIRE = {
350
+ connector: MODULE_PROVIDER_KIND.Connector,
351
+ plugin: MODULE_PROVIDER_KIND.Plugin
352
+ };
353
+
354
+ // src/connector-mappings/useConnectorMappingMutations.ts
355
+ function resolveTranslationMember(supplied, stored) {
356
+ if (supplied !== void 0) return supplied;
357
+ return stored ?? null;
358
+ }
359
+ function summarise(response) {
360
+ const items = response?.items ?? [];
361
+ return {
362
+ aiAvailable: response?.aiAvailable ?? false,
363
+ aiUnavailableReason: response?.aiUnavailableReason ?? null,
364
+ unmatchedRequiredCount: response?.unmatchedRequiredCount ?? 0,
365
+ matchedByKeyword: items.filter((i) => i.source === MAPPING_SOURCE.keyword).length,
366
+ matchedByAi: items.filter((i) => i.source === MAPPING_SOURCE.ai).length,
367
+ conflictReason: response?.conflictReason ?? null
368
+ };
369
+ }
370
+ function useConnectorMappingMutations({
371
+ service,
372
+ connectorId,
373
+ moduleKey,
374
+ onMappingSaved,
375
+ showSuccessToast: showSuccessToast2 = showSuccessToast
376
+ }) {
377
+ const queryClient = reactQuery.useQueryClient();
378
+ const [autoMatchSummary, setAutoMatchSummary] = react.useState(null);
379
+ const scopeKey = `${connectorId ?? ""}|${moduleKey}`;
380
+ const summaryScopeRef = react.useRef(scopeKey);
381
+ react.useEffect(() => {
382
+ if (summaryScopeRef.current === scopeKey) return;
383
+ summaryScopeRef.current = scopeKey;
384
+ setAutoMatchSummary(null);
385
+ }, [scopeKey]);
386
+ const queryKey = react.useMemo(
387
+ () => connectorMappingsQueryKey(connectorId, moduleKey),
388
+ [connectorId, moduleKey]
389
+ );
390
+ const invalidateMappingsAndCompleteness = react.useCallback(() => {
391
+ void queryClient.invalidateQueries({ queryKey });
392
+ }, [queryClient, queryKey]);
393
+ const autoMatch = reactQuery.useMutation({
394
+ mutationFn: () => service.autoMatch(connectorId, moduleKey),
395
+ meta: { errorTitle: "Auto-match failed" },
396
+ onSuccess: (response) => {
397
+ setAutoMatchSummary(summarise(response));
398
+ invalidateMappingsAndCompleteness();
399
+ const matched = (response?.items ?? []).filter((i) => i.mappedToolName).length;
400
+ if (connectorId && matched > 0) {
401
+ onMappingSaved?.(connectorId, moduleKey);
402
+ }
403
+ showSuccessToast2("Auto-match complete", `Matched ${matched} operations.`);
404
+ }
405
+ });
406
+ const updateMapping = reactQuery.useMutation({
407
+ mutationFn: (args) => service.updateMapping(
408
+ connectorId,
409
+ moduleKey,
410
+ args.operationKey,
411
+ args.toolName,
412
+ args.rowVersion,
413
+ args.requestFieldMap,
414
+ args.responseFieldPaths,
415
+ args.staticRequestFields
416
+ ),
417
+ meta: { errorTitle: "Update failed" },
418
+ onSuccess: () => {
419
+ invalidateMappingsAndCompleteness();
420
+ if (connectorId) {
421
+ onMappingSaved?.(connectorId, moduleKey);
422
+ }
423
+ showSuccessToast2("Mapping updated", "The tool mapping was saved.");
424
+ }
425
+ });
426
+ const deleteMapping = reactQuery.useMutation({
427
+ mutationFn: (args) => service.deleteMapping(connectorId, moduleKey, args.operationKey, args.rowVersion),
428
+ meta: { errorTitle: "Remove failed" },
429
+ onSuccess: () => {
430
+ invalidateMappingsAndCompleteness();
431
+ showSuccessToast2("Mapping removed", "The tool mapping was cleared.");
432
+ }
433
+ });
434
+ const triggerAutoMatch = react.useCallback(() => {
435
+ if (!connectorId) return;
436
+ autoMatch.mutate();
437
+ }, [autoMatch, connectorId]);
438
+ const updateOrDelete = react.useCallback(
439
+ (args, callbacks) => {
440
+ if (!connectorId) return;
441
+ if (args.toolName === "") {
442
+ if (args.rowVersion === "") return;
443
+ deleteMapping.mutate(
444
+ { operationKey: args.operationKey, rowVersion: args.rowVersion },
445
+ callbacks
446
+ );
447
+ return;
448
+ }
449
+ const rows = queryClient.getQueryData(queryKey) ?? [];
450
+ const current = rows.find((m) => m.operationKey === args.operationKey);
451
+ updateMapping.mutate(
452
+ {
453
+ ...args,
454
+ requestFieldMap: resolveTranslationMember(args.requestFieldMap, current?.requestFieldMap),
455
+ responseFieldPaths: resolveTranslationMember(
456
+ args.responseFieldPaths,
457
+ current?.responseFieldPaths
458
+ ),
459
+ staticRequestFields: resolveTranslationMember(
460
+ args.staticRequestFields,
461
+ current?.staticRequestFields
462
+ )
463
+ },
464
+ callbacks
465
+ );
466
+ },
467
+ [connectorId, updateMapping, deleteMapping, queryClient, queryKey]
468
+ );
469
+ return {
470
+ triggerAutoMatch,
471
+ isAutoMatching: autoMatch.isPending,
472
+ updateMapping: updateOrDelete,
473
+ isUpdating: updateMapping.isPending || deleteMapping.isPending,
474
+ autoMatchSummary
475
+ };
476
+ }
477
+ function useSuggestMappingTranslation(service) {
478
+ return reactQuery.useMutation({
479
+ mutationFn: async (vars) => {
480
+ const result = await service.suggestMappingTranslation(
481
+ vars.connectorId,
482
+ vars.moduleKey,
483
+ vars.body
484
+ );
485
+ if (!result.success) {
486
+ throw new Error(
487
+ result.error ?? "The AI couldn't suggest a translation for this operation."
488
+ );
489
+ }
490
+ return result;
491
+ },
492
+ meta: { errorTitle: "Suggest translation failed" }
493
+ });
494
+ }
495
+ function useTestMapping(service) {
496
+ return reactQuery.useMutation({
497
+ mutationFn: (vars) => service.testMapping(vars.connectorId, vars.moduleKey, vars.body),
498
+ meta: { errorTitle: "Mapping test failed" }
499
+ });
500
+ }
501
+ function testUnavailable(error) {
502
+ return { success: false, error, rawResponseJson: null, extractedFields: [] };
503
+ }
504
+ function useMappingTranslationEditor({
505
+ service,
506
+ connectorId,
507
+ moduleKey,
508
+ mappings,
509
+ updateMapping
510
+ }) {
511
+ const [editor, setEditor] = react.useState(null);
512
+ const scopeKey = `${connectorId ?? ""}|${moduleKey}`;
513
+ const editorScopeRef = react.useRef(scopeKey);
514
+ react.useEffect(() => {
515
+ if (editorScopeRef.current === scopeKey) return;
516
+ editorScopeRef.current = scopeKey;
517
+ setEditor(null);
518
+ }, [scopeKey]);
519
+ const suggest = useSuggestMappingTranslation(service);
520
+ const test = useTestMapping(service);
521
+ const onEditTranslation = react.useCallback(
522
+ (operationKey, current) => {
523
+ setEditor({ operationKey, current });
524
+ },
525
+ []
526
+ );
527
+ const onCancel = react.useCallback(() => setEditor(null), []);
528
+ const onOpenMappingEditor = react.useCallback(
529
+ (operation, seed) => {
530
+ const row = mappings.find((m) => m.operationKey === operation.operationKey);
531
+ setEditor({
532
+ operationKey: operation.operationKey,
533
+ current: seed ?? {
534
+ requestFieldMap: row?.requestFieldMap ?? null,
535
+ responseFieldPaths: row?.responseFieldPaths ?? null,
536
+ staticRequestFields: row?.staticRequestFields ?? null
537
+ }
538
+ });
539
+ },
540
+ [mappings]
541
+ );
542
+ const onSuggestWithAi = react.useCallback(
543
+ (operation) => {
544
+ if (!connectorId) return;
545
+ const row = mappings.find((m) => m.operationKey === operation.operationKey);
546
+ suggest.mutate(
547
+ {
548
+ connectorId,
549
+ moduleKey,
550
+ body: { operationKey: operation.operationKey, toolName: row?.mappedToolName ?? "" }
551
+ },
552
+ {
553
+ onSuccess: (result) => {
554
+ if (!result.success) return;
555
+ onOpenMappingEditor(operation, {
556
+ requestFieldMap: result.requestFieldMap,
557
+ responseFieldPaths: result.responseFieldPaths,
558
+ // Preserved, not proposed. See the remarks above.
559
+ staticRequestFields: row?.staticRequestFields ?? null
560
+ });
561
+ }
562
+ }
563
+ );
564
+ },
565
+ // `moduleKey` is a real dependency, not a module-scope constant: omitting it would capture
566
+ // whichever module was selected when this callback was created, so a multi-module consumer that
567
+ // switched module and then asked for a suggestion would get advice on the previous catalogue.
568
+ [connectorId, moduleKey, mappings, suggest, onOpenMappingEditor]
569
+ );
570
+ const onSave = react.useCallback(
571
+ (requestFieldMap, responseFieldPaths, staticRequestFields) => {
572
+ if (!editor) return;
573
+ const row = mappings.find((m) => m.operationKey === editor.operationKey);
574
+ if (!row || !row.mappedToolName) {
575
+ setEditor(null);
576
+ return;
577
+ }
578
+ updateMapping({
579
+ operationKey: row.operationKey,
580
+ toolName: row.mappedToolName,
581
+ rowVersion: row.rowVersion ?? "",
582
+ // Passed through verbatim, INCLUDING nulls. The editor delivers `null` for a side whose
583
+ // every input the operator emptied, and that is an explicit "clear this" the fill-in in
584
+ // `useConnectorMappingMutations` must not overwrite.
585
+ requestFieldMap,
586
+ responseFieldPaths,
587
+ staticRequestFields
588
+ });
589
+ setEditor(null);
590
+ },
591
+ [editor, mappings, updateMapping]
592
+ );
593
+ const onTestMapping = react.useCallback(
594
+ async (request) => {
595
+ if (!editor || !connectorId) {
596
+ return testUnavailable("No connector or operation selected.");
597
+ }
598
+ const row = mappings.find((m) => m.operationKey === editor.operationKey);
599
+ if (!row?.mappedToolName) {
600
+ return testUnavailable("No tool mapped to this operation.");
601
+ }
602
+ return test.mutateAsync({
603
+ connectorId,
604
+ moduleKey,
605
+ body: {
606
+ operationKey: editor.operationKey,
607
+ toolName: row.mappedToolName,
608
+ requestFieldMap: request.requestFieldMap,
609
+ responseFieldPaths: request.responseFieldPaths,
610
+ // The editor's DRAFT static params, not the persisted row's, so the dry-run exercises the
611
+ // unsaved discriminator the operator is actually testing.
612
+ staticRequestFields: request.staticRequestFields ?? null,
613
+ sampleArgs: request.sampleArgs
614
+ }
615
+ });
616
+ },
617
+ // Same reason as `onSuggestWithAi`: a stale `moduleKey` here would dry-run the edit against a
618
+ // different catalogue and report the result as if it were this one's.
619
+ [editor, connectorId, moduleKey, mappings, test]
620
+ );
621
+ const editorRow = editor ? mappings.find((m) => m.operationKey === editor.operationKey) : void 0;
622
+ return {
623
+ editorRow,
624
+ isOpen: editor !== null,
625
+ onEditTranslation,
626
+ onOpenMappingEditor,
627
+ onSuggestWithAi,
628
+ suggestingOperationKey: suggest.isPending ? suggest.variables?.body.operationKey ?? null : null,
629
+ editorProps: {
630
+ // Always `true`: the consumer gates the MOUNT on `isOpen`, which is what makes the editor's
631
+ // lazy state initialisation reseed from the row each time it opens.
632
+ open: true,
633
+ operationLabel: editorRow?.label,
634
+ toolName: editorRow?.mappedToolName,
635
+ canonicalRequestFields: editorRow?.canonicalRequestFields,
636
+ canonicalResponseFields: editorRow?.canonicalResponseFields,
637
+ requestFieldMap: editor?.current.requestFieldMap ?? null,
638
+ responseFieldPaths: editor?.current.responseFieldPaths ?? null,
639
+ staticRequestFields: editor?.current.staticRequestFields ?? null,
640
+ onSave,
641
+ onCancel,
642
+ onTestMapping
643
+ }
644
+ };
645
+ }
646
+ function useAutoMatchOnConnectorSwitch({
647
+ connectorId,
648
+ moduleKey,
649
+ mappings,
650
+ isLoading,
651
+ isAutoMatching,
652
+ dataUpdatedAt,
653
+ triggerAutoMatch,
654
+ initiallySelected = false
655
+ }) {
656
+ const connectorChangedAtRef = react.useRef(initiallySelected ? Date.now() : 0);
657
+ const autoMatchedRef = react.useRef(null);
658
+ const scopeKey = `${connectorId ?? ""}|${moduleKey}`;
659
+ const markConnectorChanged = react.useCallback(() => {
660
+ connectorChangedAtRef.current = Date.now();
661
+ }, []);
662
+ react.useEffect(() => {
663
+ if (!connectorId) return;
664
+ if (isLoading || isAutoMatching) return;
665
+ if (dataUpdatedAt <= connectorChangedAtRef.current) return;
666
+ if (autoMatchedRef.current === scopeKey) return;
667
+ if (mappings.length > 0 && mappings.some((m) => m.mappedToolName)) return;
668
+ autoMatchedRef.current = scopeKey;
669
+ triggerAutoMatch();
670
+ }, [connectorId, scopeKey, mappings, isLoading, isAutoMatching, dataUpdatedAt, triggerAutoMatch]);
671
+ return { markConnectorChanged };
672
+ }
673
+ var STORAGE_KEY_PREFIX = "cc-connector-mapping-banner-dismissed";
674
+ function readDismissed(storageKey) {
675
+ if (!storageKey) return false;
676
+ try {
677
+ return localStorage.getItem(storageKey) === "true";
678
+ } catch {
679
+ return false;
680
+ }
681
+ }
682
+ function useMappingBannerDismissal(organisationId, moduleKey) {
683
+ const storageKey = organisationId ? `${STORAGE_KEY_PREFIX}:${organisationId}:${moduleKey}` : null;
684
+ const [dismissed, setDismissed] = react.useState(() => readDismissed(storageKey));
685
+ const appliedKeyRef = react.useRef(storageKey);
686
+ react.useEffect(() => {
687
+ if (appliedKeyRef.current === storageKey) return;
688
+ appliedKeyRef.current = storageKey;
689
+ setDismissed(readDismissed(storageKey));
690
+ }, [storageKey]);
691
+ const dismiss = react.useCallback(() => {
692
+ if (!storageKey) return;
693
+ try {
694
+ localStorage.setItem(storageKey, "true");
695
+ } catch {
696
+ }
697
+ setDismissed(true);
698
+ }, [storageKey]);
699
+ const reset = react.useCallback(() => {
700
+ if (!storageKey) return;
701
+ try {
702
+ localStorage.removeItem(storageKey);
703
+ } catch {
704
+ }
705
+ setDismissed(false);
706
+ }, [storageKey]);
707
+ return { dismissed, dismiss, reset };
708
+ }
709
+ function moduleProviderQueryKey(moduleKey) {
710
+ return ["connector-mappings", "module-provider", moduleKey];
711
+ }
712
+ var UNCONFIGURED = {
713
+ targetKind: null,
714
+ connectorId: null,
715
+ targetExtensionId: null,
716
+ rowVersion: null
717
+ };
718
+ function useModuleProvider({
719
+ service,
720
+ moduleKey,
721
+ onProviderChanged
722
+ }) {
723
+ const queryClient = reactQuery.useQueryClient();
724
+ const queryKey = react.useMemo(() => moduleProviderQueryKey(moduleKey), [moduleKey]);
725
+ const query = useAuthenticatedQuery({
726
+ queryKey,
727
+ queryFn: () => service.getModuleProvider(moduleKey),
728
+ enabled: !!moduleKey,
729
+ meta: { errorTitle: "Error loading the module's provider" }
730
+ });
731
+ const provider = query.data ?? UNCONFIGURED;
732
+ const save = reactQuery.useMutation({
733
+ mutationFn: (next) => service.setModuleProvider(
734
+ moduleKey,
735
+ next.targetKind,
736
+ next.connectorId,
737
+ next.targetExtensionId,
738
+ provider.rowVersion
739
+ ),
740
+ meta: { errorTitle: "Could not change the module's provider" },
741
+ onSuccess: (stored) => {
742
+ queryClient.setQueryData(queryKey, stored);
743
+ onProviderChanged?.(stored);
744
+ }
745
+ });
746
+ const selectConnector = react.useCallback(
747
+ (connectorId) => save.mutate({
748
+ // An empty id is the CLEAR, and it must reach the host as one rather than being dropped
749
+ // here: leaving the previous selection in place is exactly the "connector is stuck forever"
750
+ // behaviour this surface has had to fix once already.
751
+ targetKind: MODULE_PROVIDER_KIND.Connector,
752
+ connectorId: connectorId || null,
753
+ targetExtensionId: null
754
+ }),
755
+ [save]
756
+ );
757
+ const selectExtension = react.useCallback(
758
+ (extensionId) => save.mutate({
759
+ targetKind: MODULE_PROVIDER_KIND.Plugin,
760
+ connectorId: null,
761
+ targetExtensionId: extensionId || null
762
+ }),
763
+ [save]
764
+ );
765
+ return {
766
+ provider,
767
+ // Only a CONNECTOR selection produces a connector id. A module served by a plugin has no
768
+ // connector, and reporting one would make the mapping surface fetch rows for a connector the
769
+ // operator did not choose.
770
+ connectorId: provider.targetKind === MODULE_PROVIDER_KIND.Connector ? provider.connectorId ?? "" : "",
771
+ isLoading: query.isLoading,
772
+ selectConnector,
773
+ selectExtension,
774
+ isSaving: save.isPending,
775
+ error: query.error ?? save.error ?? void 0
776
+ };
777
+ }
778
+
779
+ // src/connector-mappings/targetSelection.ts
780
+ function toWireTargetKind(kind) {
781
+ return kind === MAPPING_TARGET_KIND.plugin ? MAPPING_TARGET_KIND_WIRE.plugin : MAPPING_TARGET_KIND_WIRE.connector;
782
+ }
783
+ function fromWireTargetKind(value) {
784
+ if (value === MAPPING_TARGET_KIND_WIRE.connector) return MAPPING_TARGET_KIND.connector;
785
+ if (value === MAPPING_TARGET_KIND_WIRE.plugin) return MAPPING_TARGET_KIND.plugin;
786
+ return null;
787
+ }
788
+ function isSameTarget(existing, next) {
789
+ if (!existing) return false;
790
+ if (existing.targetKind !== next.kind) return false;
791
+ if (!existing.mappedToolName) return false;
792
+ if (existing.mappedToolName !== next.toolName) return false;
793
+ return next.kind === MAPPING_TARGET_KIND.connector ? existing.connectorId === next.connectorId : existing.targetExtensionId === next.targetExtensionId;
794
+ }
795
+ function buildMappingUpdate({
796
+ existing,
797
+ next,
798
+ translation
799
+ }) {
800
+ const sameTarget = isSameTarget(existing, next);
801
+ const decide = (member) => {
802
+ const supplied = translation?.[member];
803
+ if (supplied !== void 0) return supplied;
804
+ if (!sameTarget) return null;
805
+ return existing?.[member] ?? null;
806
+ };
807
+ const update = {
808
+ targetKind: next.kind,
809
+ toolName: next.toolName,
810
+ requestFieldMap: decide("requestFieldMap"),
811
+ responseFieldPaths: decide("responseFieldPaths"),
812
+ staticRequestFields: decide("staticRequestFields")
813
+ };
814
+ if (next.kind === MAPPING_TARGET_KIND.connector) {
815
+ update.connectorId = next.connectorId;
816
+ } else {
817
+ update.targetExtensionId = next.targetExtensionId;
818
+ }
819
+ return update;
820
+ }
821
+ function providerOf(target) {
822
+ return target.kind === MAPPING_TARGET_KIND.connector ? { kind: MAPPING_TARGET_KIND.connector, connectorId: target.connectorId } : { kind: MAPPING_TARGET_KIND.plugin, targetExtensionId: target.targetExtensionId };
823
+ }
824
+ function providerIdOf(provider) {
825
+ return provider.kind === MAPPING_TARGET_KIND.connector ? provider.connectorId : provider.targetExtensionId;
826
+ }
827
+ function useConnectorToolMappings({
828
+ service,
829
+ connectorId,
830
+ moduleKey,
831
+ moduleLabel,
832
+ connectors,
833
+ isConnectorsLoading = false,
834
+ availableTools,
835
+ onConnectorChange,
836
+ initiallySelected = false,
837
+ organisationId,
838
+ aiProvidersHref,
839
+ connectorSettingsHref,
840
+ withHeader,
841
+ onMappingSaved,
842
+ showSuccessToast: showSuccessToast2,
843
+ providerSelectionEnabled = false,
844
+ pluginProviders,
845
+ isPluginProvidersLoading = false
846
+ }) {
847
+ const {
848
+ provider: storedProvider,
849
+ connectorId: storedConnectorId,
850
+ isLoading: isProviderLoading,
851
+ selectConnector,
852
+ selectExtension
853
+ } = useModuleProvider({
854
+ service,
855
+ moduleKey: providerSelectionEnabled ? moduleKey : ""
856
+ });
857
+ const [pendingKind, setPendingKind] = react.useState(null);
858
+ const pendingKindModuleRef = react.useRef(moduleKey);
859
+ if (pendingKindModuleRef.current !== moduleKey) {
860
+ pendingKindModuleRef.current = moduleKey;
861
+ if (pendingKind !== null) setPendingKind(null);
862
+ }
863
+ const storedKind = fromWireTargetKind(storedProvider.targetKind);
864
+ const providerKind = providerSelectionEnabled ? pendingKind ?? storedKind ?? void 0 : void 0;
865
+ const isPluginMode = providerKind === MAPPING_TARGET_KIND.plugin;
866
+ const selectedPluginExtensionId = storedKind === MAPPING_TARGET_KIND.plugin ? storedProvider.targetExtensionId ?? "" : "";
867
+ const effectiveConnectorId = providerSelectionEnabled ? storedConnectorId : connectorId;
868
+ const selectedConnectorId = isPluginMode ? void 0 : effectiveConnectorId || void 0;
869
+ const scope = { service, connectorId: selectedConnectorId, moduleKey };
870
+ const { mappings, isLoading, dataUpdatedAt } = useConnectorMappings(scope);
871
+ const { operations: completenessOperations, readiness } = useConnectorMappingCompleteness(scope);
872
+ const { triggerAutoMatch, isAutoMatching, updateMapping, isUpdating, autoMatchSummary } = useConnectorMappingMutations({ ...scope, onMappingSaved, showSuccessToast: showSuccessToast2 });
873
+ const editor = useMappingTranslationEditor({
874
+ service,
875
+ connectorId: selectedConnectorId,
876
+ moduleKey,
877
+ mappings,
878
+ updateMapping
879
+ });
880
+ const { dismissed, dismiss, reset } = useMappingBannerDismissal(organisationId, moduleKey);
881
+ const { markConnectorChanged } = useAutoMatchOnConnectorSwitch({
882
+ connectorId: selectedConnectorId,
883
+ moduleKey,
884
+ mappings,
885
+ isLoading,
886
+ isAutoMatching,
887
+ dataUpdatedAt,
888
+ triggerAutoMatch,
889
+ initiallySelected
890
+ });
891
+ const handleProviderKindChange = react.useCallback(
892
+ (kind) => {
893
+ setPendingKind(kind);
894
+ },
895
+ []
896
+ );
897
+ const handlePluginProviderChange = react.useCallback(
898
+ (extensionId) => {
899
+ selectExtension(extensionId);
900
+ },
901
+ [selectExtension]
902
+ );
903
+ const handleConnectorChange = react.useCallback(
904
+ (id) => {
905
+ onConnectorChange(id);
906
+ markConnectorChanged();
907
+ if (providerSelectionEnabled) selectConnector(id);
908
+ },
909
+ // `onConnectorChange` is a real dependency: a consumer that persists per organisation rebinds
910
+ // its setter once identity resolves, and an empty dep array would capture the pre-identity one
911
+ // and stop writing through.
912
+ [onConnectorChange, markConnectorChanged, providerSelectionEnabled, selectConnector]
913
+ );
914
+ const handleUpdateMapping = react.useCallback(
915
+ (operationKey, toolName, rowVersion) => {
916
+ if (!selectedConnectorId) return;
917
+ updateMapping({ operationKey, toolName, rowVersion });
918
+ },
919
+ [selectedConnectorId, updateMapping]
920
+ );
921
+ const handleAutoMatch = react.useCallback(() => {
922
+ if (!selectedConnectorId) return;
923
+ triggerAutoMatch();
924
+ }, [selectedConnectorId, triggerAutoMatch]);
925
+ const previousScopeRef = react.useRef({
926
+ connectorId: selectedConnectorId || void 0,
927
+ moduleKey
928
+ });
929
+ react.useEffect(() => {
930
+ const previous = previousScopeRef.current;
931
+ const current = { connectorId: selectedConnectorId || void 0, moduleKey };
932
+ previousScopeRef.current = current;
933
+ if (previous.moduleKey !== current.moduleKey) return;
934
+ if (previous.connectorId === current.connectorId) return;
935
+ reset();
936
+ }, [selectedConnectorId, moduleKey, reset]);
937
+ const effectiveAvailableTools = react.useMemo(() => {
938
+ if (!isPluginMode) return availableTools;
939
+ const chosen = (pluginProviders ?? []).find(
940
+ (p) => p.extensionId === selectedPluginExtensionId
941
+ );
942
+ return (chosen?.tools ?? []).map((t) => ({
943
+ name: t.toolName,
944
+ description: t.description ?? ""
945
+ }));
946
+ }, [isPluginMode, availableTools, pluginProviders, selectedPluginExtensionId]);
947
+ const providerOptions = react.useMemo(
948
+ () => (pluginProviders ?? []).map((p) => ({ extensionId: p.extensionId, name: p.name })),
949
+ [pluginProviders]
950
+ );
951
+ const surfaceIsLoading = isLoading || isConnectorsLoading || isProviderLoading || isPluginProvidersLoading;
952
+ return {
953
+ view: {
954
+ isLoading: surfaceIsLoading,
955
+ moduleKey,
956
+ moduleLabel,
957
+ withHeader,
958
+ connectors,
959
+ selectedConnectorId: selectedConnectorId ?? "",
960
+ onConnectorChange: handleConnectorChange,
961
+ // Spread conditionally: for a consumer that has not opted in these keys are ABSENT, not
962
+ // undefined, so the view's `providerPickerEnabled` check sees exactly what a pre-provider
963
+ // consumer passes.
964
+ ...providerSelectionEnabled ? {
965
+ providerKind,
966
+ onProviderKindChange: handleProviderKindChange,
967
+ pluginProviders: providerOptions,
968
+ selectedPluginExtensionId,
969
+ onPluginProviderChange: handlePluginProviderChange
970
+ } : {},
971
+ mappings,
972
+ availableTools: effectiveAvailableTools,
973
+ onUpdateMapping: handleUpdateMapping,
974
+ onAutoMatch: handleAutoMatch,
975
+ isUpdating,
976
+ isAutoMatching,
977
+ bannerState: dismissed ? null : autoMatchSummary,
978
+ onDismissBanner: dismiss,
979
+ aiProvidersHref,
980
+ connectorSettingsHref,
981
+ onEditTranslation: editor.onEditTranslation
982
+ },
983
+ banner: {
984
+ operations: completenessOperations,
985
+ onSuggestWithAi: editor.onSuggestWithAi,
986
+ onOpenMappingEditor: editor.onOpenMappingEditor,
987
+ suggestingOperationKey: editor.suggestingOperationKey,
988
+ // False whenever the consumer has not opted in, not merely when a choice exists. A page with
989
+ // no provider control on it must not be told it has failed to use one. Also false while the
990
+ // read is in flight, so the banner does not accuse the operator during a page load.
991
+ isProviderUnconfigured: providerSelectionEnabled && !isProviderLoading && storedKind === null
992
+ },
993
+ editor: editor.editorProps,
994
+ isEditorOpen: editor.isOpen,
995
+ editorRow: editor.editorRow,
996
+ mappings,
997
+ completenessOperations,
998
+ readiness,
999
+ isLoading: surfaceIsLoading
1000
+ };
1001
+ }
1002
+
1003
+ // src/connector-mappings/connectorOptions.ts
1004
+ function mapConnectorsToOptions(connectors) {
1005
+ return connectors.map((c) => ({ id: c.id, name: c.name }));
1006
+ }
1007
+ function mapConnectorToolsToOptions(tools) {
1008
+ return tools.filter((t) => t.isEnabled).map((t) => ({ name: t.toolName, description: t.description ?? "" }));
1009
+ }
1010
+
1011
+ // src/components/connectors/providerLabels.ts
1012
+ var PROVIDER_CONTROL_LABEL = "Served by";
1013
+ var PROVIDER_OPTION_LABEL = {
1014
+ plugin: "This organisation's apps",
1015
+ connector: "External system"
1016
+ };
1017
+ var PLUGIN_PROVIDER_CONTROL_LABEL = "App";
1018
+ var CONNECTOR_CONTROL_LABEL = "Connector";
1019
+
1020
+ exports.AI_UNAVAILABLE_REASON = AI_UNAVAILABLE_REASON;
1021
+ exports.AUTO_MATCH_CONFLICT_REASON = AUTO_MATCH_CONFLICT_REASON;
1022
+ exports.CONNECTOR_CONTROL_LABEL = CONNECTOR_CONTROL_LABEL;
1023
+ exports.CONNECTOR_ENUMERATION_TOOLS = CONNECTOR_ENUMERATION_TOOLS;
1024
+ exports.CONNECTOR_MAPPING_TOOLS = CONNECTOR_MAPPING_TOOLS;
1025
+ exports.ConnectorMappingService = ConnectorMappingService;
1026
+ exports.MAPPING_SOURCE = MAPPING_SOURCE;
1027
+ exports.MAPPING_TARGET_KIND = MAPPING_TARGET_KIND;
1028
+ exports.MAPPING_TARGET_KIND_WIRE = MAPPING_TARGET_KIND_WIRE;
1029
+ exports.MODULE_PROVIDER_KIND = MODULE_PROVIDER_KIND;
1030
+ exports.PLUGIN_PROVIDER_CONTROL_LABEL = PLUGIN_PROVIDER_CONTROL_LABEL;
1031
+ exports.PROVIDER_CONTROL_LABEL = PROVIDER_CONTROL_LABEL;
1032
+ exports.PROVIDER_OPTION_LABEL = PROVIDER_OPTION_LABEL;
1033
+ exports.buildMappingUpdate = buildMappingUpdate;
1034
+ exports.connectorMappingCompletenessQueryKey = connectorMappingCompletenessQueryKey;
1035
+ exports.connectorMappingsQueryKey = connectorMappingsQueryKey;
1036
+ exports.connectorMappingsRootQueryKey = connectorMappingsRootQueryKey;
1037
+ exports.fromWireTargetKind = fromWireTargetKind;
1038
+ exports.isSameTarget = isSameTarget;
1039
+ exports.mapConnectorToolsToOptions = mapConnectorToolsToOptions;
1040
+ exports.mapConnectorsToOptions = mapConnectorsToOptions;
1041
+ exports.moduleProviderQueryKey = moduleProviderQueryKey;
1042
+ exports.pluginTargetMappingsQueryKey = pluginTargetMappingsQueryKey;
1043
+ exports.providerIdOf = providerIdOf;
1044
+ exports.providerOf = providerOf;
1045
+ exports.selectIncompleteOperations = selectIncompleteOperations;
1046
+ exports.selectMappingReadiness = selectMappingReadiness;
1047
+ exports.selectOptionalIncompleteOperations = selectOptionalIncompleteOperations;
1048
+ exports.selectRequiredIncompleteOperations = selectRequiredIncompleteOperations;
1049
+ exports.toWireTargetKind = toWireTargetKind;
1050
+ exports.useAutoMatchOnConnectorSwitch = useAutoMatchOnConnectorSwitch;
1051
+ exports.useConnectorMappingCompleteness = useConnectorMappingCompleteness;
1052
+ exports.useConnectorMappingMutations = useConnectorMappingMutations;
1053
+ exports.useConnectorMappings = useConnectorMappings;
1054
+ exports.useConnectorToolMappings = useConnectorToolMappings;
1055
+ exports.useMappingBannerDismissal = useMappingBannerDismissal;
1056
+ exports.useMappingTranslationEditor = useMappingTranslationEditor;
1057
+ exports.useModuleProvider = useModuleProvider;
1058
+ exports.useSuggestMappingTranslation = useSuggestMappingTranslation;
1059
+ exports.useTestMapping = useTestMapping;
1060
+ //# sourceMappingURL=index.cjs.map
1061
+ //# sourceMappingURL=index.cjs.map