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