Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
21f2b45
Add snapshots tab and arrowDownUp icon
fpena Oct 11, 2025
2fc5b61
Add EmptyState component and update StateScreen UI
fpena Oct 11, 2025
2df391b
Moving forward with implementation
fpena Oct 16, 2025
35a0c52
Add snapshot renaming with tooltip and input
fpena Oct 16, 2025
01d64ac
Moving forward
fpena Oct 17, 2025
7ab4041
Refactor snapshot list UI with card layout
fpena Oct 17, 2025
1600827
Remove snapshot download functionality from StateScreen
fpena Oct 17, 2025
7555996
Add restore logic
fpena Oct 17, 2025
ec41f97
Adjust style better
fpena Oct 21, 2025
39f633a
Remove snapshot renaming functionality temporarily from StateScreen
fpena Oct 24, 2025
f78791d
Remove unused isExpanded parameter from style functions
fpena Oct 24, 2025
b4cd61a
Add styling to empty state text in StateScreen
fpena Oct 24, 2025
b0a4bba
Refactor snapshot card and header styles
fpena Oct 24, 2025
dd691d5
Update Icon components to use theme color and key
fpena Oct 24, 2025
efadc1b
Merge branch 'main' into fpena/41-state-snapshots
fpena Oct 24, 2025
7be6756
Remove arrowDownUp icon and references
fpena Oct 24, 2025
56d181b
Remove unnecessary console logs and error messages
fpena Oct 24, 2025
2d27a26
Remove console error on missing client in StateScreen
fpena Oct 24, 2025
3cd2761
Refactor StateScreen to remove unnecessary fragment
fpena Oct 24, 2025
9289ee7
Refactor state backup response type check
fpena Oct 24, 2025
46c216a
Refactor StateScreen to use StateSubscriptions component
fpena Oct 25, 2025
decd0ae
Refactor snapshot UI into StateSnapshots component
fpena Oct 25, 2025
e6b978d
Refactor state management in State components
fpena Oct 27, 2025
2ba3bea
Move copySnapshotToClipboard to top-level function
fpena Oct 27, 2025
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
2 changes: 2 additions & 0 deletions app/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,15 @@ export function Icon(props: IconProps) {
}

export const iconRegistry = {
arrowUpFromLine: require("../../assets/icons/arrowUpFromLine.png"),
chevronsLeftRightEllipsis: require("../../assets/icons/chevronsLeftRightEllipsis.png"),
circleGauge: require("../../assets/icons/circleGauge.png"),
clipboard: require("../../assets/icons/clipboard.png"),
gitHub: require("../../assets/icons/gitHub.png"),
messageSquare: require("../../assets/icons/messageSquare.png"),
panelLeftClose: require("../../assets/icons/panelLeftClose.png"),
panelLeftOpen: require("../../assets/icons/panelLeftOpen.png"),
pen: require("../../assets/icons/pen.png"),
plug: require("../../assets/icons/plug.png"),
questionMark: require("../../assets/icons/questionMark.png"),
scrollText: require("../../assets/icons/scrollText.png"),
Expand Down
171 changes: 171 additions & 0 deletions app/components/State/StateSnapshots.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Text, ViewStyle, TextStyle, Pressable, View } from "react-native"
import { themed, useTheme } from "../../theme/theme"
import { TreeViewWithProvider } from "../TreeView"
import { Divider } from "../Divider"
import { Icon } from "../Icon"
import { Tooltip } from "../Tooltip"
import { useState } from "react"
import { useGlobal } from "../../state/useGlobal"
import { sendToCore } from "../../state/connectToServer"
import IRClipboard from "../../native/IRClipboard/NativeIRClipboard"
import type { Snapshot } from "app/types"

export function StateSnapshots() {
const theme = useTheme()
const [snapshots, setSnapshots] = useGlobal<Snapshot[]>("snapshots", [])
const [activeClientId, _] = useGlobal("activeClientId", "")
const [expandedSnapshotIds, setExpandedSnapshotIds] = useState<Set<string>>(new Set())

const deleteSnapshot = (snapshotId: string) => {
setSnapshots((prev) => prev.filter((s) => s.id !== snapshotId))
}

const restoreSnapshot = (snapshot: Snapshot) => {
if (!snapshot || !snapshot.state) return

// Use the snapshot's clientId if available, otherwise fall back to the active client
const targetClientId = snapshot.clientId || activeClientId

if (!targetClientId) return

// Send the restore command to the client
sendToCore("state.restore.request", {
clientId: targetClientId,
state: snapshot.state,
})
}

const toggleSnapshotExpanded = (snapshotId: string) => {
setExpandedSnapshotIds((prev) => {
const newSet = new Set(prev)
if (newSet.has(snapshotId)) {
newSet.delete(snapshotId)
} else {
newSet.add(snapshotId)
}
return newSet
})
}

if (snapshots.length === 0) {
return (
<Text style={$emptyStateText()}>
To take a snapshot of your current redux or mobx-state-tree store, press the Create Snapshot
button in the top right corner of this window.
</Text>
)
}

return (
<>
{snapshots.map((snapshot) => (
<View key={snapshot.id}>
<View style={$snapshotCard()}>
<Pressable
style={$snapshotHeader()}
onPress={() => toggleSnapshotExpanded(snapshot.id)}
>
<View style={$snapshotInfo()}>
<Text style={$snapshotName()}>{snapshot.name}</Text>
<Tooltip label="Copy Snapshot">
<Pressable
style={$iconButton()}
onPress={(e) => {
e.stopPropagation()
copySnapshotToClipboard(snapshot)
}}
>
<Icon icon="clipboard" size={18} color={theme.colors.mainText} />
</Pressable>
</Tooltip>
<Tooltip label="Restore Snapshot">
<Pressable
style={$iconButton()}
onPress={(e) => {
e.stopPropagation()
restoreSnapshot(snapshot)
}}
>
<Icon icon="arrowUpFromLine" size={18} color={theme.colors.mainText} />
</Pressable>
</Tooltip>
<Tooltip label="Delete Snapshot">
<Pressable
style={$iconButton()}
onPress={(e) => {
e.stopPropagation()
deleteSnapshot(snapshot.id)
}}
>
<Icon icon="trash" size={18} color={theme.colors.mainText} />
</Pressable>
</Tooltip>
</View>
</Pressable>
{expandedSnapshotIds.has(snapshot.id) && (
<View style={$snapshotContent()}>
<TreeViewWithProvider data={snapshot.state} />
</View>
)}
</View>
<Divider />
</View>
))}
</>
)
}

function copySnapshotToClipboard(snapshot: Snapshot) {
try {
IRClipboard.setString(JSON.stringify(snapshot.state, null, 2))
console.log("Snapshot copied to clipboard")
} catch (error) {
console.error("Failed to copy snapshot to clipboard:", error)
}
}

const $snapshotCard = themed<ViewStyle>(({ colors }) => ({
backgroundColor: colors.cardBackground,
overflow: "hidden",
}))

const $snapshotHeader = themed<ViewStyle>(({ spacing, colors }) => ({
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: spacing.sm,
backgroundColor: colors.cardBackground,
cursor: "pointer",
}))

const $snapshotInfo = themed<ViewStyle>(({ spacing }) => ({
flexDirection: "row",
alignItems: "center",
gap: spacing.md,
}))

const $snapshotName = themed<TextStyle>(({ colors, typography }) => ({
flex: 1,
fontSize: typography.body,
fontWeight: "600",
color: colors.mainText,
fontFamily: typography.code.normal,
}))

const $iconButton = themed<ViewStyle>(({ spacing, colors }) => ({
padding: spacing.xs,
borderRadius: 4,
cursor: "pointer",
backgroundColor: colors.neutralVery,
}))

const $snapshotContent = themed<ViewStyle>(({ spacing, colors }) => ({
padding: spacing.md,
backgroundColor: colors.cardBackground,
}))

const $emptyStateText = themed<TextStyle>(({ colors, typography }) => ({
fontSize: typography.body,
fontWeight: "400",
color: colors.mainText,
}))
82 changes: 82 additions & 0 deletions app/components/State/StateSubscriptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Text, ViewStyle, TextStyle, Pressable, View } from "react-native"
import { themed, useTheme } from "../../theme/theme"
import { TreeViewWithProvider } from "../TreeView"
import { Divider } from "../Divider"
import { Icon } from "../Icon"
import type { StateSubscription } from "app/types"
import { useGlobal } from "app/state/useGlobal"
import { sendToCore } from "app/state/connectToServer"

export function StateSubscriptions() {
const theme = useTheme()
const [stateSubscriptionsByClientId, setStateSubscriptionsByClientId] = useGlobal<{
[clientId: string]: StateSubscription[]
}>("stateSubscriptionsByClientId", {})
const [activeClientId, _] = useGlobal("activeClientId", "")
const clientStateSubscriptions = stateSubscriptionsByClientId[activeClientId] || []

const removeSubscription = (path: string) => {
const newStateSubscriptions = clientStateSubscriptions.filter((s) => s.path !== path)
sendToCore("state.values.subscribe", {
paths: newStateSubscriptions.map((s) => s.path),
clientId: activeClientId,
})
setStateSubscriptionsByClientId((prev) => ({
...prev,
[activeClientId]: newStateSubscriptions,
}))
}

if (clientStateSubscriptions.length === 0) {
return <Text style={$emptyStateText()}>State is empty</Text>
}

return (
<>
{clientStateSubscriptions.map((subscription, index) => (
<View key={`${subscription.path}-${index}`} style={$stateItemContainer()}>
<Text style={$pathText()}>{subscription.path ? subscription.path : "Full State"}</Text>
<View style={$treeViewContainer()}>
<View style={$treeViewInnerContainer()}>
<TreeViewWithProvider data={subscription.value} />
</View>
<Pressable onPress={() => removeSubscription(subscription.path)}>
<Icon icon="trash" size={20} color={theme.colors.mainText} />
</Pressable>
</View>
{index < clientStateSubscriptions.length - 1 && <Divider extraStyles={$stateDivider()} />}
</View>
))}
</>
)
}

const $pathText = themed<TextStyle>(({ colors, typography, spacing }) => ({
fontSize: typography.body,
fontWeight: "400",
color: colors.mainText,
marginBottom: spacing.sm,
}))

const $stateItemContainer = themed<ViewStyle>(({ spacing }) => ({
marginTop: spacing.xl,
}))

const $treeViewInnerContainer = themed<ViewStyle>(() => ({
flex: 1,
}))

const $treeViewContainer = themed<ViewStyle>(() => ({
flexDirection: "row",
justifyContent: "space-between",
}))

const $stateDivider = themed<ViewStyle>(({ spacing }) => ({
marginTop: spacing.lg,
}))

const $emptyStateText = themed<TextStyle>(({ colors, typography }) => ({
fontSize: typography.body,
fontWeight: "400",
color: colors.mainText,
}))
Loading