From a12e52cf78ce1f268a0f2df83b0af53c0249de30 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Fri, 13 Dec 2024 11:15:10 +0100 Subject: [PATCH] fix(storage): highlights and collapses leaking out to other subscriptions Closes #7 --- src/codec-components/EditCodec/CStruct.tsx | 12 ++-- src/codec-components/ViewCodec/CStruct.tsx | 10 ++- src/codec-components/common/ListItem.tsx | 12 ++-- src/codec-components/common/paths.state.ts | 62 +++++++++++-------- src/lib/contextState.ts | 18 ++++++ src/pages/RuntimeCalls/RuntimeCallResults.tsx | 5 +- src/pages/Storage/StorageSubscriptions.tsx | 38 +++++++----- tailwind.config.js | 4 +- 8 files changed, 106 insertions(+), 55 deletions(-) create mode 100644 src/lib/contextState.ts diff --git a/src/codec-components/EditCodec/CStruct.tsx b/src/codec-components/EditCodec/CStruct.tsx index 2232f71..6d4ba00 100644 --- a/src/codec-components/EditCodec/CStruct.tsx +++ b/src/codec-components/EditCodec/CStruct.tsx @@ -2,13 +2,14 @@ import { ExpandBtn } from "@/components/Expand" import { getFinalType } from "@/utils/shape" import { EditStruct } from "@polkadot-api/react-builder" import { useStateObservable } from "@react-rxjs/core" -import React from "react" +import React, { useContext } from "react" import { twMerge as clsx, twMerge } from "tailwind-merge" import { Marker } from "../common/Markers" import { useSubtreeFocus } from "../common/SubtreeFocus" import { isActive$, isCollapsed$, + PathsRoot, setHovered, toggleCollapsed, } from "../common/paths.state" @@ -19,6 +20,7 @@ const StructItem: React.FC<{ path: string[] type?: string }> = ({ name, children, path, type }) => { + const pathsRootId = useContext(PathsRoot) const pathStr = path.join(".") const isActive = useStateObservable(isActive$(pathStr)) const isExpanded = !useStateObservable(isCollapsed$(pathStr)) @@ -29,12 +31,14 @@ const StructItem: React.FC<{ "flex flex-col transition-all duration-300", isActive && "bg-secondary/20", )} - onMouseEnter={() => setHovered({ id: pathStr, hover: true })} - onMouseLeave={() => setHovered({ id: pathStr, hover: false })} + onMouseEnter={() => setHovered(pathsRootId, { id: pathStr, hover: true })} + onMouseLeave={() => + setHovered(pathsRootId, { id: pathStr, hover: false }) + } > toggleCollapsed(pathStr)} + onClick={() => toggleCollapsed(pathsRootId, pathStr)} className="cursor-pointer flex select-none items-center py-1 gap-1" > diff --git a/src/codec-components/ViewCodec/CStruct.tsx b/src/codec-components/ViewCodec/CStruct.tsx index 3dc0559..117cfc3 100644 --- a/src/codec-components/ViewCodec/CStruct.tsx +++ b/src/codec-components/ViewCodec/CStruct.tsx @@ -8,6 +8,7 @@ import { Marker } from "../common/Markers" import { isActive$, isCollapsed$, + PathsRoot, setHovered, toggleCollapsed, } from "../common/paths.state" @@ -25,6 +26,7 @@ const StructItem: React.FC<{ field: Var value: unknown }> = ({ name, children, path, field, value }) => { + const pathsRootId = useContext(PathsRoot) const pathStr = path.join(".") const isActive = useStateObservable(isActive$(pathStr)) const isExpanded = !useStateObservable(isCollapsed$(pathStr)) @@ -35,7 +37,7 @@ const StructItem: React.FC<{ const title = isComplexShape ? ( toggleCollapsed(pathStr)} + onClick={() => toggleCollapsed(pathsRootId, pathStr)} className="cursor-pointer flex items-center py-1 gap-1" > {hasParentTitle && } @@ -68,8 +70,10 @@ const StructItem: React.FC<{ "flex flex-col transition-all duration-300", isActive && "bg-secondary/80", )} - onMouseEnter={() => setHovered({ id: pathStr, hover: true })} - onMouseLeave={() => setHovered({ id: pathStr, hover: false })} + onMouseEnter={() => setHovered(pathsRootId, { id: pathStr, hover: true })} + onMouseLeave={() => + setHovered(pathsRootId, { id: pathStr, hover: false }) + } > diff --git a/src/codec-components/common/ListItem.tsx b/src/codec-components/common/ListItem.tsx index fbb5755..f665513 100644 --- a/src/codec-components/common/ListItem.tsx +++ b/src/codec-components/common/ListItem.tsx @@ -1,15 +1,16 @@ import { ExpandBtn } from "@/components/Expand" import { useStateObservable } from "@react-rxjs/core" import { Dot, Trash2 } from "lucide-react" +import { ReactNode, useContext } from "react" import { twMerge as clsx, twMerge } from "tailwind-merge" import { Marker } from "./Markers" import { isActive$, isCollapsed$, + PathsRoot, setHovered, toggleCollapsed, } from "./paths.state" -import { ReactNode } from "react" export const ListItem: React.FC<{ idx: number @@ -19,6 +20,7 @@ export const ListItem: React.FC<{ actions?: ReactNode inline?: boolean }> = ({ idx, onDelete, children, path, actions, inline }) => { + const pathsRootId = useContext(PathsRoot) const pathStr = path.join(".") const isActive = useStateObservable(isActive$(pathStr)) const isCollapsed = useStateObservable(isCollapsed$(pathStr)) @@ -46,7 +48,7 @@ export const ListItem: React.FC<{ toggleCollapsed(pathStr)} + onClick={() => toggleCollapsed(pathsRootId, pathStr)} > Item {idx + 1}. @@ -66,8 +68,10 @@ export const ListItem: React.FC<{ return (
  • setHovered({ id: pathStr, hover: true })} - onMouseLeave={() => setHovered({ id: pathStr, hover: false })} + onMouseEnter={() => setHovered(pathsRootId, { id: pathStr, hover: true })} + onMouseLeave={() => + setHovered(pathsRootId, { id: pathStr, hover: false }) + } > {title} {inline ? null : ( diff --git a/src/codec-components/common/paths.state.ts b/src/codec-components/common/paths.state.ts index 445605a..e2937af 100644 --- a/src/codec-components/common/paths.state.ts +++ b/src/codec-components/common/paths.state.ts @@ -1,24 +1,32 @@ -import { createSignal } from "@react-rxjs/utils" -import { state } from "@react-rxjs/core" -import { defer, map, scan } from "rxjs" +import { createContextState } from "@/lib/contextState" +import { createKeyedSignal } from "@react-rxjs/utils" +import { createContext, useContext } from "react" +import { map, scan } from "rxjs" -export const [collapsedToggle$, toggleCollapsed] = createSignal() +export const PathsRoot = createContext("") -const collapsedPaths$ = state( - defer(() => - collapsedToggle$.pipe( +const pathsState = createContextState(() => useContext(PathsRoot)) + +export const [collapsedToggle$, toggleCollapsed] = createKeyedSignal< + string, + string +>() + +const collapsedPaths$ = pathsState( + (id) => + collapsedToggle$(id).pipe( scan((acc, v) => { if (acc.has(v)) acc.delete(v) else acc.add(v) return acc }, new Set()), ), - ), - new Set(), + new Set(), ) -export const isCollapsed$ = state( - (path: string) => collapsedPaths$.pipe(map((v) => v.has(path))), +export const isCollapsed$ = pathsState( + (path: string, id: string) => + collapsedPaths$(id).pipe(map((v) => v.has(path))), false, ) @@ -26,9 +34,9 @@ export const isCollapsed$ = state( * Returns true if it's a collapsed root. * Same as `isCollapsed`, but returns `false` if a parent path is also collapsed. */ -export const isCollapsedRoot$ = state( - (path: string) => - collapsedPaths$.pipe( +export const isCollapsedRoot$ = pathsState( + (path: string, id: string) => + collapsedPaths$(id).pipe( map((collapsedPaths) => { if (!collapsedPaths.has(path)) return false @@ -40,27 +48,29 @@ export const isCollapsedRoot$ = state( false, ) -export const [hoverChange$, setHovered] = createSignal<{ - id: string - hover: boolean -}>() +export const [hoverChange$, setHovered] = createKeyedSignal< + string, + { + id: string + hover: boolean + } +>() -const hoverPaths$ = state( - defer(() => - hoverChange$.pipe( +const hoverPaths$ = pathsState( + (id: string) => + hoverChange$(id).pipe( scan((acc, v) => { if (v.hover) acc.add(v.id) else acc.delete(v.id) return acc }, new Set()), ), - ), - new Set(), + new Set(), ) -export const isActive$ = state( - (path: string) => - hoverPaths$.pipe( +export const isActive$ = pathsState( + (path: string, id: string) => + hoverPaths$(id).pipe( map((hoverPaths) => { if (!hoverPaths.has(path)) return false diff --git a/src/lib/contextState.ts b/src/lib/contextState.ts new file mode 100644 index 0000000..b3002ac --- /dev/null +++ b/src/lib/contextState.ts @@ -0,0 +1,18 @@ +import { DefaultedStateObservable, state } from "@react-rxjs/core" +import { Observable } from "rxjs" + +type LastOptional = T extends [...infer R, any] ? T | R : T + +export function createContextState(hook: () => Ctx) { + return function _state( + getObservable: (...args: A) => Observable, + defaultValue: O | ((...args: A) => O), + ): (...args: LastOptional) => DefaultedStateObservable { + const state$: (...args: any[]) => any = state(getObservable, defaultValue) + + return (...args: any[]) => + args.length < getObservable.length + ? state$(...args, hook()) + : state$(...args) + } +} diff --git a/src/pages/RuntimeCalls/RuntimeCallResults.tsx b/src/pages/RuntimeCalls/RuntimeCallResults.tsx index 82ad747..492d772 100644 --- a/src/pages/RuntimeCalls/RuntimeCallResults.tsx +++ b/src/pages/RuntimeCalls/RuntimeCallResults.tsx @@ -9,6 +9,7 @@ import { runtimeCallResult$, runtimeCallResultKeys$, } from "./runtimeCalls.state" +import { PathsRoot } from "@/codec-components/common/paths.state" export const RuntimeCallResults: FC = () => { const keys = useStateObservable(runtimeCallResultKeys$) @@ -63,7 +64,9 @@ const RuntimeCallResultBox: FC<{ subscription: string }> = ({ - + + +
  • ) } diff --git a/src/pages/Storage/StorageSubscriptions.tsx b/src/pages/Storage/StorageSubscriptions.tsx index aeb7aae..c6bd1da 100644 --- a/src/pages/Storage/StorageSubscriptions.tsx +++ b/src/pages/Storage/StorageSubscriptions.tsx @@ -16,6 +16,7 @@ import { stringifyArg, toggleSubscriptionPause, } from "./storage.state" +import { PathsRoot } from "@/codec-components/common/paths.state" export const StorageSubscriptions: FC = () => { const keys = useStateObservable(storageSubscriptionKeys$) @@ -81,15 +82,20 @@ const StorageSubscriptionBox: FC<{ subscription: string }> = ({ - + ) } const ResultDisplay: FC<{ storageSubscription: StorageSubscription + subscriptionKey: string mode: "json" | "decoded" -}> = ({ storageSubscription, mode }) => { +}> = ({ storageSubscription, subscriptionKey, mode }) => { if (!("result" in storageSubscription)) { return
    Loading…
    } @@ -97,12 +103,14 @@ const ResultDisplay: FC<{ if (storageSubscription.single) { return (
    - + + +
    ) } @@ -119,12 +127,14 @@ const ResultDisplay: FC<{ .join(", ") return (
    - + + +
    ) } diff --git a/tailwind.config.js b/tailwind.config.js index e52341a..2e9694a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,5 +1,3 @@ -import tailwindAnimate from "tailwindcss-animate" - /** * @type {import("tailwindcss").Config} */ @@ -98,6 +96,6 @@ export default { function ({ addVariant }) { addVariant("group-state-open", ':merge(.group)[data-state="open"] &') }, - tailwindAnimate, + require("tailwindcss-animate"), ], }