Skip to content

Commit f843e24

Browse files
committed
feat(api): custom state actions
1 parent f2ea499 commit f843e24

7 files changed

Lines changed: 109 additions & 12 deletions

File tree

docs/plugin/api-reference.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ The `_custom` object has the following properties:
113113
- `readOnly`: mark this value has not editable
114114
- `fields`: an object of configure immediate child fields
115115
- `abstract`
116+
- `actions`: an array of buttons to add to the field
117+
- `icon`: material icon identifier
118+
- `tooltip`: button tooltip
119+
- `action`: function to be executed
116120

117121
When you add new sections with the `type` property, you should declare them in the `componentStateTypes` array in the plugin descriptor when you call the `setupDevtoolsPlugin`.
118122

@@ -135,7 +139,14 @@ api.on.inspectComponent(payload => {
135139
readOnly: true,
136140
display: `${time}s`,
137141
tooltip: 'Elapsed time',
138-
value: time
142+
value: time,
143+
actions: [
144+
{
145+
icon: 'input',
146+
tooltip: 'Log to console',
147+
action: () => console.log('current time:', time)
148+
}
149+
]
139150
}
140151
}
141152
})

packages/api/src/api/component.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ export type CustomState = {
6969
fields?: {
7070
abstract?: boolean
7171
}
72+
id?: any
73+
actions?: {
74+
icon: string
75+
tooltip?: string
76+
action: () => void | Promise<void>
77+
}[]
78+
/** internal */
79+
_reviveId?: number
7280
}
7381
}
7482

packages/app-backend-core/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,20 @@ async function connect () {
238238
}
239239
})
240240

241+
ctx.bridge.on(BridgeEvents.TO_BACK_CUSTOM_STATE_ACTION, async ({ value, actionIndex }) => {
242+
const rawAction = value._custom.actions[actionIndex]
243+
const action = revive(rawAction?.action)
244+
if (action) {
245+
try {
246+
await action()
247+
} catch (e) {
248+
console.error(e)
249+
}
250+
} else {
251+
console.warn(`Couldn't revive action ${actionIndex} from`, value)
252+
}
253+
})
254+
241255
// Component perf
242256

243257
hook.on(HookEvents.PERFORMANCE_START, (app, uid, vm, type, time) => {

packages/app-frontend/src/features/inspector/DataField.vue

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<script>
1+
<script lang="ts">
22
import {
33
isPlainObject,
44
sortByKey,
@@ -204,6 +204,10 @@ export default {
204204
key = key.replace('__vue__', '')
205205
}
206206
return key
207+
},
208+
209+
customActions () {
210+
return this.field.value?._custom?.actions ?? []
207211
}
208212
},
209213
@@ -284,6 +288,13 @@ export default {
284288
value: this.field.value,
285289
revive: true
286290
})
291+
},
292+
293+
executeCustomAction (index: number) {
294+
getBridge().send(BridgeEvents.TO_BACK_CUSTOM_STATE_ACTION, {
295+
value: this.field.value,
296+
actionIndex: index
297+
})
287298
}
288299
}
289300
}
@@ -414,10 +425,18 @@ export default {
414425
</div>
415426
</template>
416427
</VueDropdown>
428+
<VueButton
429+
v-for="(action, index) of customActions"
430+
:key="index"
431+
v-tooltip="action.tooltip"
432+
class="icon-button flat"
433+
:icon-left="action.icon"
434+
@click="executeCustomAction(index)"
435+
/>
417436
<VueButton
418437
v-if="valueType === 'native Error'"
419438
v-tooltip="'Log error to console'"
420-
class="edit-value icon-button flat"
439+
class=" icon-button flat"
421440
icon-left="input"
422441
@click="logToConsole('error')"
423442
/>

packages/shared-utils/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
"build:watch": "yarn ts -w",
1111
"ts": "tsc -p tsconfig.json -d -outDir lib"
1212
},
13+
"dependencies": {
14+
"@vue/devtools-api": "^6.0.0-beta.11"
15+
},
1316
"devDependencies": {
1417
"@types/node": "^13.9.1",
1518
"@types/webpack-env": "^1.15.1",

packages/shared-utils/src/consts.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ export enum BridgeEvents {
7474
TO_FRONT_CUSTOM_INSPECTOR_TREE = 'f:custom-inspector:tree',
7575
TO_BACK_CUSTOM_INSPECTOR_STATE = 'b:custom-inspector:state',
7676
TO_FRONT_CUSTOM_INSPECTOR_STATE = 'f:custom-inspector:state',
77-
TO_BACK_CUSTOM_INSPECTOR_EDIT_STATE = 'b:custom-inspector:edit-state'
77+
TO_BACK_CUSTOM_INSPECTOR_EDIT_STATE = 'b:custom-inspector:edit-state',
78+
79+
// Custom state
80+
TO_BACK_CUSTOM_STATE_ACTION = 'b:custom-state:action'
7881
}
7982

8083
export enum BridgeSubscriptions {

packages/shared-utils/src/util.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,38 @@ class EncodeCache {
137137

138138
const encodeCache = new EncodeCache()
139139

140+
class ReviveCache {
141+
map: Map<number, any>
142+
index: number
143+
size: number
144+
maxSize: number
145+
146+
constructor (maxSize: number) {
147+
this.maxSize = maxSize
148+
this.map = new Map()
149+
this.index = 0
150+
this.size = 0
151+
}
152+
153+
cache (value: any) {
154+
const currentIndex = this.index
155+
this.map.set(currentIndex, value)
156+
this.size++
157+
if (this.size > this.maxSize) {
158+
this.map.delete(currentIndex - this.size)
159+
this.size--
160+
}
161+
this.index++
162+
return currentIndex
163+
}
164+
165+
read (id: number) {
166+
return this.map.get(id)
167+
}
168+
}
169+
170+
const reviveCache = new ReviveCache(1000)
171+
140172
export function stringify (data) {
141173
// Create a fresh cache for each serialization
142174
encodeCache.clear()
@@ -231,7 +263,7 @@ export function reviveMap (val) {
231263
const list = val._custom.value
232264
for (let i = 0; i < list.length; i++) {
233265
const { key, value } = list[i]
234-
result.set(key, reviver(null, value))
266+
result.set(key, revive(value))
235267
}
236268
return result
237269
}
@@ -253,7 +285,7 @@ export function reviveSet (val) {
253285
const list = val._custom.value
254286
for (let i = 0; i < list.length; i++) {
255287
const value = list[i]
256-
result.add(reviver(null, value))
288+
result.add(revive(value))
257289
}
258290
return result
259291
}
@@ -301,7 +333,8 @@ export function getCustomComponentDefinitionDetails (def) {
301333
}
302334
}
303335

304-
export function getCustomFunctionDetails (func) {
336+
// eslint-disable-next-line @typescript-eslint/ban-types
337+
export function getCustomFunctionDetails (func: Function): CustomState {
305338
let string = ''
306339
let matches = null
307340
try {
@@ -319,7 +352,8 @@ export function getCustomFunctionDetails (func) {
319352
return {
320353
_custom: {
321354
type: 'function',
322-
display: `<span>ƒ</span> ${escape(name)}${args}`
355+
display: `<span>ƒ</span> ${escape(name)}${args}`,
356+
_reviveId: reviveCache.cache(func)
323357
}
324358
}
325359
}
@@ -397,12 +431,17 @@ export function revive (val) {
397431
} else if (val === NAN) {
398432
return NaN
399433
} else if (val && val._custom) {
400-
if (val._custom.type === 'component') {
401-
return getInstanceMap().get(val._custom.id)
402-
} else if (val._custom.type === 'map') {
434+
const { _custom: custom }: CustomState = val
435+
if (custom.type === 'component') {
436+
return getInstanceMap().get(custom.id)
437+
} else if (custom.type === 'map') {
403438
return reviveMap(val)
404-
} else if (val._custom.type === 'set') {
439+
} else if (custom.type === 'set') {
405440
return reviveSet(val)
441+
} else if (custom._reviveId) {
442+
return reviveCache.read(custom._reviveId)
443+
} else {
444+
return revive(custom.value)
406445
}
407446
} else if (symbolRE.test(val)) {
408447
const [, string] = symbolRE.exec(val)

0 commit comments

Comments
 (0)