Skip to content

Commit

Permalink
feat(devtools): add ability to edit atoms states
Browse files Browse the repository at this point in the history
  • Loading branch information
artalar committed Dec 13, 2024
1 parent 5228c75 commit f22bce8
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 9 deletions.
2 changes: 1 addition & 1 deletion packages/devtools/src/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export const Graph = ({ clientCtx, getColor, width, height }: Props) => {
return search.length < 2 ? '' : search.toLocaleLowerCase()
})

const inspector = reatomInspector({ filters }, `${name}.inspector`)
const inspector = reatomInspector({ clientCtx, filters }, `${name}.inspector`)

const listHeight = reatomResource(async (ctx) => {
ctx.spy(list)
Expand Down
103 changes: 96 additions & 7 deletions packages/devtools/src/Graph/reatomInspector.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { __root, action, atom, AtomCache, parseAtoms, withReset } from '@reatom/framework'
import { __root, action, atom, AtomCache, Ctx, parseAtoms, withReset } from '@reatom/framework'
import { h, hf, JSX } from '@reatom/jsx'
import { ObservableHQ, ObservableHQActionButton } from '../ObservableHQ'
import { reatomFilters } from './reatomFilters'
import { actionsStates, history } from './utils'
import { withComputed } from '@reatom/primitives'
import { reatomBoolean, withComputed } from '@reatom/primitives'

type InspectorState =
| { kind: 'hidden' }
| { kind: 'open'; patch: AtomCache }
| { kind: 'fixed'; patch: AtomCache; element: HTMLElement }

export const reatomInspector = ({ filters }: { filters: ReturnType<typeof reatomFilters> }, name: string) => {
export const reatomInspector = (
{ clientCtx, filters }: { clientCtx: Ctx; filters: ReturnType<typeof reatomFilters> },
name: string,
) => {
const state = atom<InspectorState>({ kind: 'hidden' }, `${name}.state`)

const patch = atom((ctx) => {
const s = ctx.spy(state)

return s.kind === 'hidden' ? null : s.patch
}, `${name}.patch`)

const patchState = atom<any>(null, `${name}.patchState`).pipe(
withComputed((ctx) => {
const patchState = ctx.spy(patch)
Expand All @@ -29,6 +34,22 @@ export const reatomInspector = ({ filters }: { filters: ReturnType<typeof reatom
return patchState?.state
}),
)

const edit = reatomBoolean(false, `${name}.edit`).pipe(
withComputed((ctx) => {
ctx.spy(patch)
return false
}),
)

const json = atom((ctx) => {
try {
return JSON.stringify(ctx.spy(patch)?.state, null, 2) ?? ''
} catch (error) {
return ''
}
}, `${name}.json`)

const patchHistory = atom((ctx) => {
const patchState = ctx.spy(patch)

Expand Down Expand Up @@ -94,6 +115,16 @@ export const reatomInspector = ({ filters }: { filters: ReturnType<typeof reatom
})
}, `${name}.close`)

// separate action for naming purpose, CALL ONLY WITH `clientCtx`
const update = action((ctx, value: string) => {
ctx.get((read, actualize) => {
const proto = ctx.get(patch)?.proto!
actualize!(ctx, proto, (patchCtx: Ctx, patch: AtomCache) => {
patch.state = JSON.parse(value)
})
})
}, `${name}.update`)

const OPACITY = {
hidden: '0',
open: '0.8',
Expand Down Expand Up @@ -138,8 +169,17 @@ export const reatomInspector = ({ filters }: { filters: ReturnType<typeof reatom
`}
>
<div
css:observablehq-display={atom((ctx) => (ctx.spy(edit) ? 'none' : 'block'))}
css:form-display={atom((ctx) => (ctx.spy(edit) ? 'block' : 'none'))}
css={`
min-height: 100px;
& .observablehq-container {
display: var(--observablehq-display);
}
& form {
display: var(--form-display);
}
`}
>
<h4
Expand All @@ -156,11 +196,60 @@ export const reatomInspector = ({ filters }: { filters: ReturnType<typeof reatom
<ObservableHQ
snapshot={patchState}
actions={
<ObservableHQActionButton on:click={close} title="Close" aria-label="Close this inspector">
x
</ObservableHQActionButton>
<>
<ObservableHQActionButton
// @ts-ignore TODO
css:display={atom((ctx) => (ctx.spy(patch)?.proto.isAction ? 'none' : ''))}
on:click={edit.toggle}
title="Edit"
aria-label="Toggle editing"
css={`
display: var(--display);
`}
>
✏️
</ObservableHQActionButton>
<ObservableHQActionButton on:click={close} title="Close" aria-label="Close this inspector">
x
</ObservableHQActionButton>
</>
}
/>
>
<form
on:submit={(ctx, e) => {
e.preventDefault()
const proto = ctx.get(patch)?.proto
const textarea = e.currentTarget.firstChild
if (proto && textarea instanceof HTMLTextAreaElement) {
update(clientCtx, textarea.value)
textarea.value = ctx.get(json)
}
}}
aria-label="Update state"
>
<textarea
placeholder="JSON"
css={`
margin: 1rem;
font-family: monospace;
resizable: both;
min-width: 50%;
min-height: 5rem;
`}
prop:value={atom((ctx) => (ctx.spy(edit) ? ctx.spy(json) : ''))}
/>
<div
css={`
margin: 0 1rem;
`}
>
<button>Update</button>
<button type="button" on:click={edit.setFalse}>
Cancel
</button>
</div>
</form>
</ObservableHQ>
</div>
<div>
<hr />
Expand Down
4 changes: 3 additions & 1 deletion packages/devtools/src/ObservableHQ.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const ObservableHQActionButton = (props: JSX.ButtonHTMLAttributes<HTMLEle
)
}

export const ObservableHQ: FC<{ snapshot: any; actions?: Element }> = ({ snapshot, actions }) => {
export const ObservableHQ: FC<{ snapshot: any; actions?: Element | Array<Element>, children?: JSX.Element }> = ({ snapshot, actions, children }) => {
let update = noop

return (
Expand Down Expand Up @@ -80,6 +80,7 @@ export const ObservableHQ: FC<{ snapshot: any; actions?: Element }> = ({ snapsho
{actions}
</div>
<div
class="observablehq-container"
ref={(ctx, observableContainer) => {
const shadowRoot = observableContainer.attachShadow({ mode: 'open' })
shadowRoot.append(
Expand Down Expand Up @@ -121,6 +122,7 @@ export const ObservableHQ: FC<{ snapshot: any; actions?: Element }> = ({ snapsho
)
}}
/>
{children}
</div>
)
}

0 comments on commit f22bce8

Please sign in to comment.