Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 170 additions & 20 deletions portals/management-portal/src/context/validationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import React, {
} from "react";
import {
useGithubProjectValidation,
useApiUniquenessValidation,
type GithubProjectValidationRequest,
type GithubProjectValidationResponse,
type ApiNameVersionValidationRequest,
type ApiUniquenessValidationResponse,
} from "../hooks/validation";

/** Keep the name “githubprojectvalidation” semantic in value keys & exports */
Expand All @@ -27,21 +30,39 @@ type GithubProjectValidationContextValue = {
setBranch: (b: string | null) => void;
setPath: (p: string | null) => void;

// state
// state (github project validation)
loading: boolean;
error: string | null;
result: GithubProjectValidationResponse | null;

// actions
// actions (github project validation)
validate: (
override?: Partial<GithubProjectValidationRequest>
) => Promise<GithubProjectValidationResponse>;

// convenience
// convenience (github project validation)
isValid: boolean | null; // null when no result yet
errors: string[]; // [] when valid or no result

// NEW: allow consumers to clear error/result/loading
// name+version uniqueness validation
nameVersionLoading: boolean;
nameVersionError: string | null;
nameVersionResult: ApiUniquenessValidationResponse | null;
validateNameVersion: (
payload: ApiNameVersionValidationRequest
) => Promise<ApiUniquenessValidationResponse>;
isNameVersionUnique: boolean | null;

// identifier uniqueness validation
identifierLoading: boolean;
identifierError: string | null;
identifierResult: ApiUniquenessValidationResponse | null;
validateIdentifier: (
identifier: string
) => Promise<ApiUniquenessValidationResponse>;
isIdentifierUnique: boolean | null;

// reset
reset: () => void;
};

Expand All @@ -51,31 +72,60 @@ const Ctx = createContext<GithubProjectValidationContextValue | undefined>(

type Props = { children: ReactNode };

export const GithubProjectValidationProvider: React.FC<Props> = ({ children }) => {
/** --- Type guards (avoid any / avoid TS2367) --- */
const isGithubValidationErr = (
r: GithubProjectValidationResponse
): r is Extract<
GithubProjectValidationResponse,
{ isAPIProjectValid: false }
> => r.isAPIProjectValid === false;

export const GithubProjectValidationProvider: React.FC<Props> = ({
children,
}) => {
const { validateGithubApiProject } = useGithubProjectValidation();
const { validateApiNameVersion, validateApiIdentifier } =
useApiUniquenessValidation();

// inputs
const [repoUrl, setRepoUrl] = useState<string>("");
const [provider, setProvider] = useState<"github">("github");
const [branch, setBranch] = useState<string | null>(null);
const [path, setPath] = useState<string | null>(null);

// state
// state (github project)
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<GithubProjectValidationResponse | null>(null);
const [result, setResult] = useState<GithubProjectValidationResponse | null>(
null
);

// state (name+version)
const [nameVersionLoading, setNameVersionLoading] = useState(false);
const [nameVersionError, setNameVersionError] = useState<string | null>(null);
const [nameVersionResult, setNameVersionResult] =
useState<ApiUniquenessValidationResponse | null>(null);

// state (identifier)
const [identifierLoading, setIdentifierLoading] = useState(false);
const [identifierError, setIdentifierError] = useState<string | null>(null);
const [identifierResult, setIdentifierResult] =
useState<ApiUniquenessValidationResponse | null>(null);

// lifecycle guards
const mountedRef = useRef(true);
const pendingRef = useRef(0);

const begin = () => {
pendingRef.current += 1;
setLoading(true);
};

const end = () => {
pendingRef.current = Math.max(0, pendingRef.current - 1);
if (pendingRef.current === 0 && mountedRef.current) setLoading(false);
};

useEffect(() => {
mountedRef.current = true;
return () => {
Expand All @@ -101,7 +151,9 @@ export const GithubProjectValidationProvider: React.FC<Props> = ({ children }) =
setError(null);

try {
const res = await validateGithubApiProject(effective, { signal: ac.signal });
const res = await validateGithubApiProject(effective, {
signal: ac.signal,
});
if (mountedRef.current) setResult(res);
return res;
} catch (e) {
Expand All @@ -119,27 +171,96 @@ export const GithubProjectValidationProvider: React.FC<Props> = ({ children }) =
[repoUrl, provider, branch, path, validateGithubApiProject]
);

const validateNameVersion = useCallback(
async (payload: ApiNameVersionValidationRequest) => {
setNameVersionLoading(true);
setNameVersionError(null);

try {
const res = await validateApiNameVersion(payload);
if (mountedRef.current) setNameVersionResult(res);
return res;
} catch (e) {
const msg =
e instanceof Error
? e.message
: "Failed to validate API name & version";
if (mountedRef.current) {
setNameVersionError(msg);
setNameVersionResult(null);
}
throw e;
} finally {
if (mountedRef.current) setNameVersionLoading(false);
}
},
[validateApiNameVersion]
);

const validateIdentifier = useCallback(
async (identifier: string) => {
setIdentifierLoading(true);
setIdentifierError(null);

try {
const res = await validateApiIdentifier(identifier);
if (mountedRef.current) setIdentifierResult(res);
return res;
} catch (e) {
const msg =
e instanceof Error ? e.message : "Failed to validate API identifier";
if (mountedRef.current) {
setIdentifierError(msg);
setIdentifierResult(null);
}
throw e;
} finally {
if (mountedRef.current) setIdentifierLoading(false);
}
},
[validateApiIdentifier]
);

/** ✅ FIX: no impossible comparisons; use discriminant narrowing */
const isValid = useMemo(() => {
if (!result) return null;
return (
!!(result as any).isAPIProjectValid &&
!!(result as any).isAPIConfigValid &&
!!(result as any).isAPIDefinitionValid
);
return !isGithubValidationErr(result);
}, [result]);

/** ✅ Type-safe errors extraction */
const errors = useMemo<string[]>(() => {
if (!result) return [];
return Array.isArray((result as any).errors) ? (result as any).errors : [];
return isGithubValidationErr(result) ? result.errors : [];
}, [result]);

// NEW: reset helper
const isNameVersionUnique = useMemo(() => {
if (!nameVersionResult) return null;
return nameVersionResult.valid;
}, [nameVersionResult]);

const isIdentifierUnique = useMemo(() => {
if (!identifierResult) return null;
return identifierResult.valid;
}, [identifierResult]);

const reset = useCallback(() => {
pendingRef.current = 0;
if (!mountedRef.current) return;

// github project
setLoading(false);
setError(null);
setResult(null);

// name+version
setNameVersionLoading(false);
setNameVersionError(null);
setNameVersionResult(null);

// identifier
setIdentifierLoading(false);
setIdentifierError(null);
setIdentifierResult(null);
}, []);

const value = useMemo<GithubProjectValidationContextValue>(
Expand All @@ -153,16 +274,34 @@ export const GithubProjectValidationProvider: React.FC<Props> = ({ children }) =
setProvider,
setBranch,
setPath,
// state

// github project state
loading,
error,
result,
// actions

// github project actions
validate,
// convenience

// github project convenience
isValid,
errors,
// new

// name+version uniqueness
nameVersionLoading,
nameVersionError,
nameVersionResult,
validateNameVersion,
isNameVersionUnique,

// identifier uniqueness
identifierLoading,
identifierError,
identifierResult,
validateIdentifier,
isIdentifierUnique,

// reset
reset,
}),
[
Expand All @@ -176,6 +315,16 @@ export const GithubProjectValidationProvider: React.FC<Props> = ({ children }) =
validate,
isValid,
errors,
nameVersionLoading,
nameVersionError,
nameVersionResult,
validateNameVersion,
isNameVersionUnique,
identifierLoading,
identifierError,
identifierResult,
validateIdentifier,
isIdentifierUnique,
reset,
]
);
Expand All @@ -185,9 +334,10 @@ export const GithubProjectValidationProvider: React.FC<Props> = ({ children }) =

export const useGithubProjectValidationContext = () => {
const ctx = useContext(Ctx);
if (!ctx)
if (!ctx) {
throw new Error(
"useGithubProjectValidationContext must be used within GithubProjectValidationProvider"
);
}
return ctx;
};
Loading