fix(storage): highlights and collapses leaking out to other subscriptions
Closes #7
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 : (
|
||||
|
||||
@@ -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
18
src/lib/contextState.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user