History

fix build

Rhys Sullivan committed 8 months ago

Commit 5d27afe1 parent c42ef9d2 files · +51 −43Browse files

Unified · Split

2 files
  1. executor/apps/web/src/components/tools/use/add/source/form-state.ts +49 −41
  2. executor/apps/web/src/lib/openapi/spec-inspector.ts +2 −2
executor/apps/web/src/components/tools/use/add/source/form-state.ts+49−41
@@ -1,4 +1,4 @@
1import { useDeferredValue, useEffect, useMemo, useState, type Dispatch, type SetStateAction } from "react";1import { useDeferredValue, useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from "react";
2import { useQuery as useTanstackQuery } from "@tanstack/react-query";2import { useQuery as useTanstackQuery } from "@tanstack/react-query";
3import { useForm, useWatch } from "react-hook-form";3import { useForm, useWatch } from "react-hook-form";
4import { fetchAndInspectOpenApiSpec, type InferredSpecAuth } from "@/lib/openapi/spec-inspector";4import { fetchAndInspectOpenApiSpec, type InferredSpecAuth } from "@/lib/openapi/spec-inspector";
@@ -117,6 +117,11 @@ export function useAddSourceFormState({ · 112 unmodified lines
117 const values: SourceFormValues = { ...createDefaultFormValues(), ...watched };117 const values: SourceFormValues = { ...createDefaultFormValues(), ...watched };
118 const [ui, setUi] = useState<AddSourceUiState>(() => createDefaultUiState(sourceToEdit ? "custom" : "catalog"));118 const [ui, setUi] = useState<AddSourceUiState>(() => createDefaultUiState(sourceToEdit ? "custom" : "catalog"));
119 119
120 // Refs to let effects read current values without adding reactive deps that cause loops.
121 const authManuallyEditedRef = useRef(ui.authManuallyEdited);
122 authManuallyEditedRef.current = ui.authManuallyEdited;
123 const appliedDetectionRef = useRef<{ endpoint: string; type: SourceType } | null>(null);
124
120 const visibleCatalogItems = useMemo(125 const visibleCatalogItems = useMemo(
121 () => getVisibleCatalogItems(ui.catalogQuery, ui.catalogSort),126 () => getVisibleCatalogItems(ui.catalogQuery, ui.catalogSort),
122 [ui.catalogQuery, ui.catalogSort],127 [ui.catalogQuery, ui.catalogSort],
@@ -160,6 +165,9 @@ export function useAddSourceFormState({ · 37 unmodified lines
160 return;165 return;
161 }166 }
162 167
168 // Clear applied detection tracking when dialog opens/resets
169 appliedDetectionRef.current = null;
170
163 if (sourceToEdit) {171 if (sourceToEdit) {
164 const editValues = sourceToFormValues(sourceToEdit);172 const editValues = sourceToFormValues(sourceToEdit);
165 form.reset(editValues);173 form.reset(editValues);
@@ -396,60 +404,60 @@ export function useAddSourceFormState({ · 230 unmodified lines
396 ? "error"404 ? "error"
397 : "idle";405 : "idle";
398 406
399 // Auto-set the type when detection succeeds407 // Auto-set the type when detection succeeds.
408 // Uses a ref to track what we last applied, avoiding deps on values the effect itself mutates.
400 useEffect(() => {409 useEffect(() => {
401 if (!typeDetectionEnabled || !typeDetectionQuery.data) {410 if (!typeDetectionEnabled || !typeDetectionQuery.data) {
402 return;411 return;
403 }412 }
404 const detected = typeDetectionQuery.data.type;413 const detected = typeDetectionQuery.data.type;
405 if (values.type !== detected) {414
406 form.setValue("type", detected, { shouldDirty: false, shouldTouch: false });415 // Skip if we already applied this exact detection result for this endpoint
407 416 if (
408 // Trigger the same side-effects as handleTypeChange but without marking as explicitly set417 appliedDetectionRef.current
409 if (detected === "openapi") {418 && appliedDetectionRef.current.endpoint === detectionEndpoint
410 const inferredBaseUrl = deriveBaseUrlFromEndpoint(values.endpoint);419 && appliedDetectionRef.current.type === detected
411 patchUi(setUi, {420 ) {
412 openApiBaseUrlOptions: inferredBaseUrl ? [inferredBaseUrl] : [],421 return;
413 authManuallyEdited: false,422 }
414 mcpOAuthLinkedEndpoint: null,423
415 });424 appliedDetectionRef.current = { endpoint: detectionEndpoint, type: detected };
416 if (!baseUrlManuallyEdited) {425 form.setValue("type", detected, { shouldDirty: false, shouldTouch: false });
417 form.setValue("baseUrl", inferredBaseUrl, { shouldDirty: false, shouldTouch: false });426
418 }427 // Trigger the same side-effects as handleTypeChange but without marking as explicitly set.
419 } else {428 // Uses functional setUi to read current state without depending on it reactively.
420 patchUi(setUi, {429 if (detected === "openapi") {
421 openApiBaseUrlOptions: [],430 const inferredBaseUrl = deriveBaseUrlFromEndpoint(detectionEndpoint);
422 mcpOAuthLinkedEndpoint: null,431 setUi((current) => ({
423 // Preserve authManuallyEdited — user may have set credentials before detection432 ...current,
424 });433 openApiBaseUrlOptions: inferredBaseUrl ? [inferredBaseUrl] : [],
425 // Only reset auth fields if the user hasn't manually edited them434 mcpOAuthLinkedEndpoint: null,
426 if (!ui.authManuallyEdited) {435 }));
427 patchUi(setUi, { authManuallyEdited: false });436 if (!baseUrlManuallyEdited) {
428 form.setValue("authType", "none", { shouldDirty: false, shouldTouch: false });437 form.setValue("baseUrl", inferredBaseUrl, { shouldDirty: false, shouldTouch: false });
429 form.setValue("authScope", "workspace", { shouldDirty: false, shouldTouch: false });438 }
430 }439 } else {
440 setUi((current) => ({
441 ...current,
442 openApiBaseUrlOptions: [],
443 mcpOAuthLinkedEndpoint: null,
444 }));
445 // Only reset auth form values if the user hasn't manually set credentials.
446 // Read from ref to avoid adding a reactive dep that causes render loops.
447 if (!authManuallyEditedRef.current) {
448 form.setValue("authType", "none", { shouldDirty: false, shouldTouch: false });
449 form.setValue("authScope", "workspace", { shouldDirty: false, shouldTouch: false });
431 }450 }
432 }451 }
433 452
434 // Also infer name from URL if not already set453 // Also infer name from URL if not already set
435 if (!nameManuallyEdited) {454 if (!nameManuallyEdited) {
436 const inferredName = inferNameFromUrl(detectionEndpoint);455 const inferredName = inferNameFromUrl(detectionEndpoint);
437 if (inferredName && values.name !== inferredName) {456 if (inferredName) {
438 form.setValue("name", inferredName, { shouldDirty: false, shouldTouch: false });457 form.setValue("name", inferredName, { shouldDirty: false, shouldTouch: false });
439 }458 }
440 }459 }
441 }, [460 }, [typeDetectionEnabled, typeDetectionQuery.data, detectionEndpoint, form, baseUrlManuallyEdited, nameManuallyEdited]);
442 typeDetectionEnabled,
443 typeDetectionQuery.data,
444 baseUrlManuallyEdited,
445 detectionEndpoint,
446 form,
447 nameManuallyEdited,
448 ui.authManuallyEdited,
449 values.endpoint,
450 values.name,
451 values.type,
452 ]);
453 461
454 // Type is "resolved" when explicitly set by the user/catalog, or auto-detected from the URL.462 // Type is "resolved" when explicitly set by the user/catalog, or auto-detected from the URL.
455 // Type-specific queries (spec inspection, MCP OAuth) should only fire after resolution,463 // Type-specific queries (spec inspection, MCP OAuth) should only fire after resolution,
executor/apps/web/src/lib/openapi/spec-inspector.ts+2−2
@@ -17,9 +17,9 @@ type OpenApiInspectionResult = { · 16 unmodified lines
17 inferredAuth: InferredSpecAuth;17 inferredAuth: InferredSpecAuth;
18};18};
19 19
20const recordSchema = z.record(z.unknown());20const recordSchema = z.record(z.string(), z.unknown());
21 21
22const securityRequirementSchema = z.record(z.array(z.unknown()).optional());22const securityRequirementSchema = z.record(z.string(), z.array(z.unknown()).optional());
23 23
24const securitySchemeSchema = z.object({24const securitySchemeSchema = z.object({
25 type: z.string().optional(),25 type: z.string().optional(),