fix(storage): highlights and collapses leaking out to other subscriptions

Closes #7
This commit is contained in:
Victor Oliva
2024-12-13 11:15:10 +01:00
parent 9f18d773d8
commit a0a92cd782
10 changed files with 121 additions and 61 deletions

View File

@@ -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 })
}
>
<Marker id={path} />
<span
onClick={() => toggleCollapsed(pathStr)}
onClick={() => toggleCollapsed(pathsRootId, pathStr)}
className="cursor-pointer flex select-none items-center py-1 gap-1"
>
<ExpandBtn expanded={isExpanded} />

View File

@@ -1,3 +1,4 @@
import { PathsRoot, setHovered } from "@/codec-components/common/paths.state"
import { Enum } from "@/components/Icons"
import { isComplex } from "@/utils/shape"
import { EditEnum, NOTIN } from "@polkadot-api/react-builder"
@@ -11,7 +12,6 @@ import {
TitleContext,
useReportBinaryStatus,
} from "./codec-components"
import { setHovered } from "@/codec-components/common/paths.state"
export const CEnum: EditEnum = ({
value,
@@ -23,6 +23,7 @@ export const CEnum: EditEnum = ({
onValueChanged,
decode,
}) => {
const pathId = useContext(PathsRoot)
const className =
"text-slate-500 hover:text-polkadot-500 cursor-pointer whitespace-nowrap [&:not(:first-child)]:ml-1"
@@ -48,8 +49,12 @@ export const CEnum: EditEnum = ({
<Portal node={titleElement}>
<span
onClick={() => scrollToMarker(innerPath)}
onMouseEnter={() => setHovered({ id: pathStr, hover: true })}
onMouseLeave={() => setHovered({ id: pathStr, hover: false })}
onMouseEnter={() =>
setHovered(pathId, { id: pathStr, hover: true })
}
onMouseLeave={() =>
setHovered(pathId, { id: pathStr, hover: false })
}
>
/ {value.type}
</span>

View File

@@ -27,7 +27,7 @@ import {
useState,
} from "react"
import { twMerge } from "tailwind-merge"
import { isActive$, setHovered } from "../../common/paths.state"
import { isActive$, PathsRoot, setHovered } from "../../common/paths.state"
export const CVoid: EditVoid = () => null
@@ -137,6 +137,7 @@ export const ItemTitle: FC<
}) => {
const [binaryOpen, setBinaryOpen] = useState(false)
const isActive = useStateObservable(isActive$(path))
const pathId = useContext(PathsRoot)
return (
<>
@@ -147,8 +148,8 @@ export const ItemTitle: FC<
className,
)}
data-marker={`marker-${path}`}
onMouseEnter={() => setHovered({ id: path, hover: true })}
onMouseLeave={() => setHovered({ id: path, hover: false })}
onMouseEnter={() => setHovered(pathId, { id: path, hover: true })}
onMouseLeave={() => setHovered(pathId, { id: path, hover: false })}
>
<span
className={twMerge(

View File

@@ -4,7 +4,7 @@ import { useContext, useState } from "react"
import { Portal } from "react-portal"
import { twMerge } from "tailwind-merge"
import { Marker } from "../common/Markers"
import { isActive$, setHovered } from "../common/paths.state"
import { isActive$, PathsRoot, setHovered } from "../common/paths.state"
import { useSubtreeFocus } from "../common/SubtreeFocus"
import { useAppendTitle } from "../EditCodec/Tree/CEnum"
import { CopyBinary, useReportBinary } from "./CopyBinary"
@@ -17,6 +17,7 @@ export const CEnum: ViewEnum = ({ value, inner, path, encodedValue }) => {
const [newElement, setNewElement] = useState<HTMLElement | null>(null)
const pathStr = path.join(".")
const isActive = useStateObservable(isActive$(pathStr))
const pathId = useContext(PathsRoot)
useReportBinary(encodedValue)
const sub = focus.getNextPath(path)
if (sub) {
@@ -37,8 +38,8 @@ export const CEnum: ViewEnum = ({ value, inner, path, encodedValue }) => {
return (
<div
className={twMerge("flex flex-col")}
onMouseEnter={() => setHovered({ id: pathStr, hover: true })}
onMouseLeave={() => setHovered({ id: pathStr, hover: false })}
onMouseEnter={() => setHovered(pathId, { id: pathStr, hover: true })}
onMouseLeave={() => setHovered(pathId, { id: pathStr, hover: false })}
>
<Marker id={[...path, value.type]} />
<div className="flex gap-2 overflow-hidden justify-between">

View File

@@ -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 ? (
<span
onClick={() => toggleCollapsed(pathStr)}
onClick={() => toggleCollapsed(pathsRootId, pathStr)}
className="cursor-pointer flex items-center py-1 gap-1"
>
{hasParentTitle && <ItemMarker />}
@@ -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 })
}
>
<ChildProvider titleElement={titleElement}>
<Marker id={path} />

View File

@@ -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<{
<Marker id={path} />
<span
className="cursor-pointer flex items-center py-1 gap-1"
onClick={() => toggleCollapsed(pathStr)}
onClick={() => toggleCollapsed(pathsRootId, pathStr)}
>
<ExpandBtn expanded={!isCollapsed} />
Item {idx + 1}.
@@ -66,8 +68,10 @@ export const ListItem: React.FC<{
return (
<li
className={twMerge("flex flex-col mb-1", 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 })
}
>
{title}
{inline ? null : (

View File

@@ -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<string>()
export const PathsRoot = createContext<string>("")
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<string>()),
),
),
new Set(),
new Set<string>(),
)
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<string>()),
),
),
new Set(),
new Set<string>(),
)
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

18
src/lib/contextState.ts Normal file
View File

@@ -0,0 +1,18 @@
import { DefaultedStateObservable, state } from "@react-rxjs/core"
import { Observable } from "rxjs"
type LastOptional<T> = T extends [...infer R, any] ? T | R : T
export function createContextState<Ctx>(hook: () => Ctx) {
return function _state<A extends [Ctx, ...any[]], O>(
getObservable: (...args: A) => Observable<O>,
defaultValue: O | ((...args: A) => O),
): (...args: LastOptional<A>) => DefaultedStateObservable<O> {
const state$: (...args: any[]) => any = state(getObservable, defaultValue)
return (...args: any[]) =>
args.length < getObservable.length
? state$(...args, hook())
: state$(...args)
}
}

View File

@@ -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 }> = ({
</button>
</div>
</div>
<ResultDisplay runtimeCallResult={runtimeCallResult} mode={mode} />
<PathsRoot.Provider value={subscription}>
<ResultDisplay runtimeCallResult={runtimeCallResult} mode={mode} />
</PathsRoot.Provider>
</li>
)
}

View File

@@ -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 }> = ({
</button>
</div>
</div>
<ResultDisplay storageSubscription={storageSubscription} mode={mode} />
<ResultDisplay
storageSubscription={storageSubscription}
subscriptionKey={subscription}
mode={mode}
/>
</li>
)
}
const ResultDisplay: FC<{
storageSubscription: StorageSubscription
subscriptionKey: string
mode: "json" | "decoded"
}> = ({ storageSubscription, mode }) => {
}> = ({ storageSubscription, subscriptionKey, mode }) => {
if (!("result" in storageSubscription)) {
return <div className="text-sm text-foreground/50">Loading</div>
}
@@ -97,12 +103,14 @@ const ResultDisplay: FC<{
if (storageSubscription.single) {
return (
<div className="max-h-[60svh] overflow-auto">
<ValueDisplay
mode={mode}
type={storageSubscription.type}
value={storageSubscription.result}
title={"Result"}
/>
<PathsRoot.Provider value={subscriptionKey}>
<ValueDisplay
mode={mode}
type={storageSubscription.type}
value={storageSubscription.result}
title={"Result"}
/>
</PathsRoot.Provider>
</div>
)
}
@@ -119,12 +127,14 @@ const ResultDisplay: FC<{
.join(", ")
return (
<div key={title} className={itemClasses}>
<ValueDisplay
mode={mode}
title={title}
value={value}
type={storageSubscription.type}
/>
<PathsRoot.Provider value={`${subscriptionKey}-${title}`}>
<ValueDisplay
mode={mode}
title={title}
value={value}
type={storageSubscription.type}
/>
</PathsRoot.Provider>
</div>
)
}