diff --git a/packages/app/components/permissions/PermissionsDiff/classes.ts b/packages/app/components/permissions/PermissionsDiff/classes.ts new file mode 100644 index 000000000..37cd63f8a --- /dev/null +++ b/packages/app/components/permissions/PermissionsDiff/classes.ts @@ -0,0 +1 @@ +export const DIFF_CONTAINER_CLASS = "diffContainer" diff --git a/packages/app/components/permissions/PermissionsDiff/index.tsx b/packages/app/components/permissions/PermissionsDiff/index.tsx index 045fae8f0..3333facb6 100644 --- a/packages/app/components/permissions/PermissionsDiff/index.tsx +++ b/packages/app/components/permissions/PermissionsDiff/index.tsx @@ -8,7 +8,7 @@ import { diffPermissions, diffPresets } from "./diff" import { PresetsAndPermissionsView } from "../PermissionsList" import classes from "./style.module.css" import { SpawnAnchorContext } from "@/ui/Anchor" -import { DIFF_CONTAINER_CLASS } from "../PresetItem/IndividualPermissionsExpandable" +import { DIFF_CONTAINER_CLASS } from "./classes" interface Props { left: { targets: Target[]; annotations: Annotation[] } @@ -54,7 +54,7 @@ const PermissionsDiff = async ({ left, right, chainId }: Props) => { /> - + = (props) => { const [hashOnMount, setHashOnMount] = useState(undefined) @@ -49,10 +50,12 @@ export default IndividualPermissionsExpandable const BOX_CLASS = "permissionBox" const TOGGLE_CLASS = "permissionBoxToggle" -export const DIFF_CONTAINER_CLASS = "diffContainer" -// TODO: make this work! const syncToggle = (ev: React.MouseEvent) => { + // Don't handle the programmatically triggered click on counterpartToggle, so we don't get into a loop + // https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted + if (!ev.isTrusted) return + const diffContainers = [ ...document.querySelectorAll(`.${DIFF_CONTAINER_CLASS}`), ] @@ -91,5 +94,6 @@ const syncToggle = (ev: React.MouseEvent) => { if (!counterpartToggle) { throw new Error("Expected to find counterpart toggle") } + counterpartToggle.click() } diff --git a/packages/app/components/permissions/TargetItem/index.tsx b/packages/app/components/permissions/TargetItem/index.tsx index c35a5045c..4be1ac606 100644 --- a/packages/app/components/permissions/TargetItem/index.tsx +++ b/packages/app/components/permissions/TargetItem/index.tsx @@ -96,7 +96,7 @@ const TargetItem: React.FC<{ diff={targetDiff === DiffFlag.Modified ? undefined : targetDiff} // we don't highlight the modified target since this would get a bit too colorful > Promise } ): Promise => { + const normalizedUri = normalizeUri(annotation.uri) + const [permissions, schema] = await Promise.all([ - fetchPermissions(annotation.uri).catch((e: Error) => { - console.error(`Error resolving annotation ${annotation.uri}`, e) + fetchPermissions(normalizedUri).catch((e: Error) => { + console.error(`Error resolving annotation ${normalizedUri}`, e) throw new Error(`Error resolving annotation: ${e}`) }), fetchSchema(annotation.schema).catch((e) => { @@ -162,7 +164,7 @@ export const resolveAnnotation = async ( } const { serverUrl, path, query } = parseUri( - annotation.uri, + normalizedUri, schema, annotation.schema ) @@ -184,7 +186,7 @@ export const resolveAnnotation = async ( return { permissions, - uri: annotation.uri, + uri: normalizedUri, serverUrl, apiInfo: schema.definition.info || { title: "", version: "" }, path: operation.path, @@ -198,6 +200,23 @@ export const resolveAnnotation = async ( } } +/** Normalize a URI to a canonical form in which query params are sorted alphabetically */ +const normalizeUri = (uri: string) => { + let url: URL + try { + url = new URL(uri) // Parse the URI into its components + } catch (error) { + throw new Error(`Invalid URI: ${uri}`) // Handle invalid URI errors + } + + const searchParams = new URLSearchParams(url.search) + // Sort the key/value pairs in place + searchParams.sort() + + // Reconstruct the URI with normalized query parameters + return `${url.origin}${url.pathname}?${searchParams}` +} + /** Returns the annotation's path relative to the API server's base URL */ const parseUri = ( annotationUrl: string,