atom.io 0.33.11 → 0.33.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,12 @@
1
- import { type Json, JSON_DEFAULTS } from "atom.io/json"
2
- import type { ReactElement } from "react"
1
+ import type { RegularAtomToken } from "atom.io"
2
+ import { findInStore } from "atom.io/internal"
3
+ import type { Json, JsonTypes } from "atom.io/json"
4
+ import { JSON_DEFAULTS } from "atom.io/json"
5
+ import { useI, useO } from "atom.io/react"
6
+ import { DevtoolsContext } from "atom.io/react-devtools/store"
7
+ import { type ReactElement, useContext } from "react"
3
8
 
9
+ import type { JsonEditorComponents, SetterOrUpdater } from ".."
4
10
  import type { JsonEditorProps_INTERNAL } from "../json-editor-internal"
5
11
  import { JsonEditor_INTERNAL } from "../json-editor-internal"
6
12
  import { makeElementSetters } from "./utilities/array-elements"
@@ -9,6 +15,53 @@ import {
9
15
  makePropertyRemovers,
10
16
  } from "./utilities/object-properties"
11
17
 
18
+ type ArrayElementProps = {
19
+ path: ReadonlyArray<number | string>
20
+ isReadonly: (path: ReadonlyArray<number | string>) => boolean
21
+ isHidden: (path: ReadonlyArray<number | string>) => boolean
22
+ data: unknown
23
+ set: SetterOrUpdater<Json.Tree.Array>
24
+ remove: (() => void) | undefined
25
+ recast: (newType: keyof JsonTypes) => void
26
+ Components: JsonEditorComponents
27
+ testid?: string | undefined
28
+ viewIsOpenAtom: RegularAtomToken<boolean, readonly (number | string)[]>
29
+ }
30
+ const ArrayElement = ({
31
+ path,
32
+ isReadonly,
33
+ isHidden,
34
+ data,
35
+ set,
36
+ remove,
37
+ recast,
38
+ Components,
39
+ testid,
40
+ viewIsOpenAtom,
41
+ }: ArrayElementProps): ReactElement => {
42
+ const index = path[path.length - 1]
43
+ const viewIsOpen = useO(viewIsOpenAtom)
44
+ const setViewIsOpen = useI(viewIsOpenAtom)
45
+
46
+ return (
47
+ <JsonEditor_INTERNAL
48
+ path={path}
49
+ name={`${index}`}
50
+ isReadonly={isReadonly}
51
+ isHidden={isHidden}
52
+ data={data}
53
+ set={set}
54
+ remove={remove}
55
+ recast={recast}
56
+ className="json_editor_element"
57
+ Components={Components}
58
+ isOpen={viewIsOpen}
59
+ setIsOpen={setViewIsOpen}
60
+ testid={`${testid}-element-${index}`}
61
+ />
62
+ )
63
+ }
64
+
12
65
  export const ArrayEditor = ({
13
66
  path = [],
14
67
  isReadonly = () => false,
@@ -18,6 +71,7 @@ export const ArrayEditor = ({
18
71
  Components,
19
72
  testid,
20
73
  }: JsonEditorProps_INTERNAL<Json.Tree.Array>): ReactElement => {
74
+ const { viewIsOpenAtoms, store } = useContext(DevtoolsContext)
21
75
  const disabled = isReadonly(path)
22
76
 
23
77
  const setElement = makeElementSetters(data, set)
@@ -26,41 +80,47 @@ export const ArrayEditor = ({
26
80
 
27
81
  return (
28
82
  <Components.ArrayWrapper>
29
- <div className={`json_editor_elements${disabled ? ` readonly` : ``}`}>
83
+ <main className={`json_editor_elements${disabled ? ` readonly` : ``}`}>
30
84
  {data.map((element, index) => {
31
- const newPath = [...path, index]
85
+ const elementPath = [...path, index]
86
+ const pathKey = elementPath.join(`,`)
87
+ const viewIsOpenAtom = findInStore(store, viewIsOpenAtoms, [
88
+ ...path,
89
+ index,
90
+ ])
32
91
  return (
33
- <JsonEditor_INTERNAL
34
- key={newPath.join(``)}
35
- path={newPath}
36
- name={`${index}`}
92
+ <ArrayElement
93
+ key={pathKey}
94
+ path={elementPath}
37
95
  isReadonly={isReadonly}
38
96
  isHidden={isHidden}
39
97
  data={element}
40
98
  set={setElement[index]}
41
99
  remove={removeElement[index]}
42
100
  recast={recastElement[index]}
43
- className="json_editor_element"
44
101
  Components={Components}
45
- testid={`${testid}-element-${index}`}
102
+ testid={testid}
103
+ viewIsOpenAtom={viewIsOpenAtom}
46
104
  />
47
105
  )
48
106
  })}
49
- </div>
50
- {disabled ? null : (
51
- <Components.Button
52
- testid={`${testid}-add-element`}
53
- disabled={disabled}
54
- onClick={() => {
55
- set((current) => {
56
- const newData = [...current, JSON_DEFAULTS.string]
57
- return newData
58
- })
59
- }}
60
- >
61
- +
62
- </Components.Button>
63
- )}
107
+ </main>
108
+ {!disabled ? (
109
+ <footer>
110
+ <Components.Button
111
+ testid={`${testid}-add-element`}
112
+ disabled={disabled}
113
+ onClick={() => {
114
+ set((current) => {
115
+ const newData = [...current, JSON_DEFAULTS.string]
116
+ return newData
117
+ })
118
+ }}
119
+ >
120
+ <Components.AddIcon />
121
+ </Components.Button>
122
+ </footer>
123
+ ) : null}
64
124
  </Components.ArrayWrapper>
65
125
  )
66
126
  }
@@ -1,8 +1,14 @@
1
- import type { Json } from "atom.io/json"
1
+ import type { RegularAtomToken } from "atom.io"
2
+ import { findInStore } from "atom.io/internal"
3
+ import type { Json, JsonTypes } from "atom.io/json"
4
+ import { useI } from "atom.io/react/use-i"
5
+ import { useO } from "atom.io/react/use-o"
6
+ import { DevtoolsContext } from "atom.io/react-devtools/store"
2
7
  import type { FC, ReactElement } from "react"
3
- import { useRef } from "react"
8
+ import { useContext, useRef } from "react"
4
9
 
5
10
  import { ElasticInput } from "../../elastic-input"
11
+ import type { SetterOrUpdater } from ".."
6
12
  import type { JsonEditorComponents } from "../default-components"
7
13
  import type { JsonEditorProps_INTERNAL } from "../json-editor-internal"
8
14
  import { JsonEditor_INTERNAL } from "../json-editor-internal"
@@ -43,6 +49,56 @@ export const PropertyAdder: FC<PropertyAdderProps> = ({
43
49
  </Components.MissingPropertyWrapper>
44
50
  )
45
51
 
52
+ type ObjectPropertyProps = {
53
+ path: ReadonlyArray<number | string>
54
+ isReadonly: (path: ReadonlyArray<number | string>) => boolean
55
+ isHidden: (path: ReadonlyArray<number | string>) => boolean
56
+ data: unknown
57
+ set: SetterOrUpdater<Json.Tree.Object>
58
+ rename: (newKey: string) => void
59
+ remove: (() => void) | undefined
60
+ recast: (newType: keyof JsonTypes) => void
61
+ Components: JsonEditorComponents
62
+ testid?: string | undefined
63
+ viewIsOpenAtom: RegularAtomToken<boolean, readonly (number | string)[]>
64
+ }
65
+ const ObjectProperty = ({
66
+ path,
67
+ isReadonly,
68
+ isHidden,
69
+ data,
70
+ set,
71
+ rename,
72
+ remove,
73
+ recast,
74
+ Components,
75
+ testid,
76
+ viewIsOpenAtom,
77
+ }: ObjectPropertyProps): ReactElement => {
78
+ const key = path[path.length - 1]
79
+ const viewIsOpen = useO(viewIsOpenAtom)
80
+ const setViewIsOpen = useI(viewIsOpenAtom)
81
+
82
+ return (
83
+ <JsonEditor_INTERNAL
84
+ path={path}
85
+ name={`${key}`}
86
+ isReadonly={isReadonly}
87
+ isHidden={isHidden}
88
+ data={data}
89
+ set={set}
90
+ rename={rename}
91
+ remove={remove}
92
+ recast={recast}
93
+ className="json_editor_property"
94
+ Components={Components}
95
+ isOpen={viewIsOpen}
96
+ setIsOpen={setViewIsOpen}
97
+ testid={`${testid}-property-${key}`}
98
+ />
99
+ )
100
+ }
101
+
46
102
  export const ObjectEditor = <T extends Json.Tree.Object>({
47
103
  path = [],
48
104
  isReadonly = () => false,
@@ -52,6 +108,8 @@ export const ObjectEditor = <T extends Json.Tree.Object>({
52
108
  Components,
53
109
  testid,
54
110
  }: JsonEditorProps_INTERNAL<T>): ReactElement => {
111
+ const { viewIsOpenAtoms, store } = useContext(DevtoolsContext)
112
+
55
113
  const disabled = isReadonly(path)
56
114
 
57
115
  const stableKeyMap = useRef<Record<keyof T, keyof T>>(
@@ -76,30 +134,34 @@ export const ObjectEditor = <T extends Json.Tree.Object>({
76
134
  <div className={`json_editor_properties${disabled ? ` readonly` : ``}`}>
77
135
  {Object.keys(data).map((key) => {
78
136
  const originalKey = stableKeyMap.current[key]
79
- const newPath = [...path, key]
80
- const originalPath = [...path, originalKey]
137
+ const propertyPath = [...path, key]
138
+ const originalPropertyPath = [...path, originalKey]
139
+ const stablePathKey = originalPropertyPath.join(`.`)
140
+ const viewIsOpenAtom = findInStore(store, viewIsOpenAtoms, [
141
+ ...path,
142
+ key,
143
+ ])
81
144
 
82
145
  return (
83
- <JsonEditor_INTERNAL
84
- key={originalPath.join(`.`)}
85
- path={newPath}
86
- name={key}
146
+ <ObjectProperty
147
+ key={stablePathKey}
148
+ path={propertyPath}
87
149
  isReadonly={isReadonly}
88
150
  isHidden={isHidden}
89
- data={data[key as keyof T]}
90
- set={setProperty[key as keyof T]}
91
- rename={renameProperty[key as keyof T]}
92
- remove={removeProperty[key as keyof T]}
93
- recast={recastProperty[key as keyof T]}
94
- className="json_editor_property"
151
+ data={data[key]}
152
+ set={setProperty[key]}
153
+ rename={renameProperty[key]}
154
+ remove={removeProperty[key]}
155
+ recast={recastProperty[key]}
95
156
  Components={Components}
96
- testid={`${testid}-property-${key}`}
157
+ testid={testid}
158
+ viewIsOpenAtom={viewIsOpenAtom}
97
159
  />
98
160
  )
99
161
  })}
100
162
  </div>
101
163
  {disabled ? null : (
102
- <>
164
+ <footer>
103
165
  <Components.Button
104
166
  disabled={disabled}
105
167
  testid={`${testid}-add-property`}
@@ -107,7 +169,7 @@ export const ObjectEditor = <T extends Json.Tree.Object>({
107
169
  makePropertyAdder(`new_property`, `string`)()
108
170
  }}
109
171
  >
110
- +
172
+ <Components.AddIcon />
111
173
  </Components.Button>
112
174
  <Components.Button
113
175
  testid={`${testid}-sort-properties`}
@@ -118,7 +180,7 @@ export const ObjectEditor = <T extends Json.Tree.Object>({
118
180
  >
119
181
  Sort
120
182
  </Components.Button>
121
- </>
183
+ </footer>
122
184
  )}
123
185
  </Components.ObjectWrapper>
124
186
  )
@@ -1,9 +1,17 @@
1
+ import { actUponStore, arbitrary } from "atom.io/internal"
1
2
  import { jsonRefinery } from "atom.io/introspection"
2
3
  import type { JsonTypes } from "atom.io/json"
3
4
  import { isJson } from "atom.io/json"
4
- import type { CSSProperties, FC, ReactElement } from "react"
5
+ import {
6
+ type CSSProperties,
7
+ type FC,
8
+ type ReactElement,
9
+ useContext,
10
+ } from "react"
5
11
 
12
+ import { button } from "../Button"
6
13
  import { ElasticInput } from "../elastic-input"
14
+ import { DevtoolsContext } from "../store"
7
15
  import type { SetterOrUpdater } from "."
8
16
  import { SubEditors } from "."
9
17
  import type { JsonEditorComponents } from "./default-components"
@@ -23,6 +31,8 @@ export type JsonEditorProps_INTERNAL<T> = {
23
31
  style?: CSSProperties | undefined
24
32
  Header?: FC<{ data: T }> | undefined
25
33
  Components: JsonEditorComponents
34
+ isOpen?: boolean
35
+ setIsOpen?: (newValue: boolean) => void
26
36
  testid?: string | undefined
27
37
  }
28
38
 
@@ -38,10 +48,13 @@ export const JsonEditor_INTERNAL = <T,>({
38
48
  isHidden = () => false,
39
49
  className,
40
50
  style,
41
- Header: HeaderDisplay,
42
51
  Components,
52
+ isOpen,
53
+ setIsOpen,
43
54
  testid,
44
55
  }: JsonEditorProps_INTERNAL<T>): ReactElement | null => {
56
+ const { openCloseAllTX, store } = useContext(DevtoolsContext)
57
+
45
58
  const dataIsJson = isJson(data)
46
59
  const refined = jsonRefinery.refine<unknown>(data) ?? {
47
60
  type: `non-json`,
@@ -53,6 +66,9 @@ export const JsonEditor_INTERNAL = <T,>({
53
66
 
54
67
  const disabled = isReadonly(path)
55
68
 
69
+ const dataIsTree = refined.type === `array` || refined.type === `object`
70
+ const dataIsExpandable = dataIsTree && isOpen !== undefined && setIsOpen
71
+
56
72
  return isHidden(path) ? null : (
57
73
  <Components.ErrorBoundary>
58
74
  <Components.EditorWrapper
@@ -60,64 +76,115 @@ export const JsonEditor_INTERNAL = <T,>({
60
76
  style={style}
61
77
  testid={testid}
62
78
  >
63
- {remove ? (
64
- <Components.Button
65
- disabled={disabled}
66
- onClick={() => {
67
- remove()
68
- }}
69
- testid={`${testid}-delete`}
70
- >
71
- <Components.DeleteIcon />
72
- </Components.Button>
73
- ) : null}
74
- {HeaderDisplay && <HeaderDisplay data={data} />}
75
- {rename && (
76
- <Components.KeyWrapper>
77
- <ElasticInput
78
- value={name}
79
- onChange={
80
- disabled
81
- ? undefined
82
- : (e) => {
83
- rename(e.target.value)
79
+ <header>
80
+ <main>
81
+ {remove || dataIsExpandable ? (
82
+ <button.OpenClose
83
+ isOpen={isOpen ?? false}
84
+ testid={`${testid}-open-close`}
85
+ onShiftClick={() => {
86
+ actUponStore(store, openCloseAllTX, arbitrary())(path, isOpen)
87
+ return false
88
+ }}
89
+ setIsOpen={setIsOpen}
90
+ disabled={!dataIsExpandable}
91
+ />
92
+ ) : null}
93
+ {rename && (
94
+ <Components.KeyWrapper>
95
+ <ElasticInput
96
+ value={name}
97
+ onChange={(e) => {
98
+ rename(e.target.value)
99
+ }}
100
+ disabled={disabled}
101
+ data-testid={`${testid}-rename`}
102
+ />
103
+ </Components.KeyWrapper>
104
+ )}
105
+ {dataIsTree ? (
106
+ <>
107
+ {isOpen !== undefined && setIsOpen ? (
108
+ <span className="json_viewer">{JSON.stringify(data)}</span>
109
+ ) : null}
110
+ {recast ? (
111
+ <select
112
+ onChange={(e) => {
113
+ recast(e.target.value as keyof JsonTypes)
114
+ }}
115
+ value={refined.type}
116
+ disabled={disabled}
117
+ data-testid={`${testid}-recast`}
118
+ >
119
+ {Object.keys(SubEditors).map((type) => (
120
+ <option key={type} value={type}>
121
+ {type}
122
+ </option>
123
+ ))}
124
+ </select>
125
+ ) : null}
126
+ </>
127
+ ) : (
128
+ <>
129
+ <SubEditor
130
+ data={refined.data as never}
131
+ set={set}
132
+ remove={remove}
133
+ rename={rename}
134
+ path={path}
135
+ isReadonly={isReadonly}
136
+ isHidden={isHidden}
137
+ Components={Components}
138
+ testid={testid}
139
+ />
140
+ {recast && dataIsJson ? (
141
+ <select
142
+ onChange={
143
+ disabled
144
+ ? undefined
145
+ : (e) => {
146
+ recast(e.target.value as keyof JsonTypes)
147
+ }
84
148
  }
85
- }
149
+ value={refined.type}
150
+ disabled={disabled}
151
+ data-testid={`${testid}-recast`}
152
+ >
153
+ {Object.keys(SubEditors).map((type) => (
154
+ <option key={type} value={type}>
155
+ {type}
156
+ </option>
157
+ ))}
158
+ </select>
159
+ ) : null}
160
+ </>
161
+ )}
162
+ </main>
163
+ {remove ? (
164
+ <Components.Button
86
165
  disabled={disabled}
87
- data-testid={`${testid}-rename`}
88
- />
89
- </Components.KeyWrapper>
90
- )}
91
- <SubEditor
92
- data={refined.data as never}
93
- set={set}
94
- remove={remove}
95
- rename={rename}
96
- path={path}
97
- isReadonly={isReadonly}
98
- isHidden={isHidden}
99
- Components={Components}
100
- testid={testid}
101
- />
102
- {recast && dataIsJson ? (
103
- <select
104
- onChange={
105
- disabled
106
- ? undefined
107
- : (e) => {
108
- recast(e.target.value as keyof JsonTypes)
109
- }
110
- }
111
- value={refined.type}
112
- disabled={disabled}
113
- data-testid={`${testid}-recast`}
114
- >
115
- {Object.keys(SubEditors).map((type) => (
116
- <option key={type} value={type}>
117
- {type}
118
- </option>
119
- ))}
120
- </select>
166
+ onClick={() => {
167
+ remove()
168
+ }}
169
+ testid={`${testid}-delete`}
170
+ >
171
+ <Components.DeleteIcon />
172
+ </Components.Button>
173
+ ) : null}
174
+ </header>
175
+
176
+ {dataIsTree && isOpen !== false ? (
177
+ <SubEditor
178
+ data={refined.data as never}
179
+ set={set}
180
+ remove={remove}
181
+ rename={rename}
182
+ path={path}
183
+ isReadonly={isReadonly}
184
+ isHidden={isHidden}
185
+ Components={Components}
186
+ testid={testid}
187
+ />
121
188
  ) : null}
122
189
  </Components.EditorWrapper>
123
190
  </Components.ErrorBoundary>
@@ -1,12 +1,22 @@
1
- import type { RegularAtomFamilyToken, RegularAtomToken } from "atom.io"
1
+ import type {
2
+ AtomToken,
3
+ RegularAtomFamilyToken,
4
+ RegularAtomToken,
5
+ SelectorToken,
6
+ TransactionToken,
7
+ } from "atom.io"
2
8
  import {
3
9
  createAtomFamily,
4
10
  createStandaloneAtom,
11
+ createTransaction,
5
12
  IMPLICIT,
6
13
  type Store,
7
14
  } from "atom.io/internal"
8
- import type { IntrospectionStates } from "atom.io/introspection"
9
- import { attachIntrospectionStates } from "atom.io/introspection"
15
+ import type {
16
+ IntrospectionStates,
17
+ WritableTokenIndex,
18
+ } from "atom.io/introspection"
19
+ import { attachIntrospectionStates, isPlainObject } from "atom.io/introspection"
10
20
  import { persistSync } from "atom.io/web"
11
21
  import type { Context } from "react"
12
22
  import { createContext } from "react"
@@ -17,7 +27,10 @@ export type DevtoolsStates = {
17
27
  devtoolsAreOpenState: RegularAtomToken<boolean>
18
28
  devtoolsViewSelectionState: RegularAtomToken<DevtoolsView>
19
29
  devtoolsViewOptionsState: RegularAtomToken<DevtoolsView[]>
20
- viewIsOpenAtoms: RegularAtomFamilyToken<boolean, string>
30
+ viewIsOpenAtoms: RegularAtomFamilyToken<boolean, readonly (number | string)[]>
31
+ openCloseAllTX: TransactionToken<
32
+ (path: readonly (number | string)[], current?: boolean) => void
33
+ >
21
34
  }
22
35
 
23
36
  export function attachDevtoolsStates(
@@ -52,13 +65,90 @@ export function attachDevtoolsStates(
52
65
  : [persistSync(window.localStorage, JSON, `🔍 Devtools View Options`)],
53
66
  })
54
67
 
55
- const viewIsOpenAtoms = createAtomFamily<boolean, string>(store, {
68
+ const viewIsOpenAtoms = createAtomFamily<
69
+ boolean,
70
+ readonly (number | string)[]
71
+ >(store, {
56
72
  key: `🔍 Devtools View Is Open`,
57
73
  default: false,
58
74
  effects: (key) =>
59
75
  typeof window === `undefined`
60
76
  ? []
61
- : [persistSync(window.localStorage, JSON, key + `:view-is-open`)],
77
+ : [persistSync(window.localStorage, JSON, `view-is-open:${key.join()}`)],
78
+ })
79
+
80
+ const openCloseAllTX: TransactionToken<
81
+ (path: readonly (number | string)[], current?: boolean) => void
82
+ > = createTransaction<
83
+ (path: readonly (number | string)[], current?: boolean) => void
84
+ >(store, {
85
+ key: `openCloseMultiView`,
86
+ do: ({ get, set }, path, current) => {
87
+ const currentView = get(devtoolsViewSelectionState)
88
+ let states:
89
+ | WritableTokenIndex<AtomToken<unknown>>
90
+ | WritableTokenIndex<SelectorToken<unknown>>
91
+ switch (currentView) {
92
+ case `atoms`:
93
+ states = get(introspectionStates.atomIndex)
94
+ break
95
+ case `selectors`:
96
+ states = get(introspectionStates.selectorIndex)
97
+ break
98
+ case `transactions`:
99
+ case `timelines`:
100
+ return
101
+ }
102
+
103
+ switch (path.length) {
104
+ case 1:
105
+ {
106
+ for (const [key] of states) {
107
+ set(viewIsOpenAtoms, [key], !current)
108
+ }
109
+ }
110
+ break
111
+ default: {
112
+ const item = states.get(path[0] as string)
113
+ let value: unknown
114
+ let segments: (number | string)[]
115
+ if (item) {
116
+ if (`familyMembers` in item) {
117
+ if (path.length === 2) {
118
+ for (const [subKey] of item.familyMembers) {
119
+ set(viewIsOpenAtoms, [path[0], subKey], !current)
120
+ }
121
+ return
122
+ }
123
+ // biome-ignore lint/style/noNonNullAssertion: fine here
124
+ const token = item.familyMembers.get(path[1] as string)!
125
+ value = get(token)
126
+ segments = path.slice(2, -1)
127
+ } else {
128
+ value = get(item)
129
+ segments = path.slice(1, -1)
130
+ }
131
+ for (const segment of segments) {
132
+ if (value && typeof value === `object`) {
133
+ value = value[segment as keyof typeof value]
134
+ }
135
+ }
136
+ const head = path.slice(0, -1)
137
+ if (Array.isArray(value)) {
138
+ for (let i = 0; i < value.length; i++) {
139
+ set(viewIsOpenAtoms, [...head, i], !current)
140
+ }
141
+ } else {
142
+ if (isPlainObject(value)) {
143
+ for (const key of Object.keys(value)) {
144
+ set(viewIsOpenAtoms, [...head, key], !current)
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ },
62
152
  })
63
153
 
64
154
  return {
@@ -67,6 +157,7 @@ export function attachDevtoolsStates(
67
157
  devtoolsViewSelectionState,
68
158
  devtoolsViewOptionsState,
69
159
  viewIsOpenAtoms,
160
+ openCloseAllTX,
70
161
  store,
71
162
  }
72
163
  }