Skip to content

Commit

Permalink
feat: pinia init
Browse files Browse the repository at this point in the history
FliPPeDround committed Dec 5, 2024

Verified

This commit was signed with the committer’s verified signature.
scala-steward Scala Steward
1 parent 940e225 commit 3b3ebbb
Showing 23 changed files with 1,728 additions and 355 deletions.
16 changes: 16 additions & 0 deletions packages/client/auto-imports.d.ts
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ declare global {
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSelectedContext: typeof import('./src/composables/select')['createSelectedContext']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createStateEditorContext: typeof import('./src/composables/state-editor')['createStateEditorContext']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
@@ -40,6 +41,7 @@ declare global {
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const filterInspectorState: typeof import('./src/utils/search')['filterInspectorState']
const formatStateType: typeof import('./src/composables/formatStateType')['formatStateType']
const formatStateValue: typeof import('./src/composables/format')['formatStateValue']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
@@ -106,10 +108,12 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const searchDeepInObject: typeof import('./src/utils/search')['searchDeepInObject']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const shiki: typeof import('./src/composables/shiki')['shiki']
const specialTokenToString: typeof import('./src/utils/search')['specialTokenToString']
const stateTypeColorMap: typeof import('./src/constants/stateColorMap')['stateTypeColorMap']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
@@ -205,6 +209,7 @@ declare global {
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useHover: typeof import('./src/composables/hover')['useHover']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
@@ -268,6 +273,9 @@ declare global {
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStateEditor: typeof import('./src/composables/state-editor')['useStateEditor']
const useStateEditorContext: typeof import('./src/composables/state-editor')['useStateEditorContext']
const useStateEditorDrafting: typeof import('./src/composables/state-editor')['useStateEditorDrafting']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
@@ -357,6 +365,7 @@ declare module 'vue' {
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createSelectedContext: UnwrapRef<typeof import('./src/composables/select')['createSelectedContext']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createStateEditorContext: UnwrapRef<typeof import('./src/composables/state-editor')['createStateEditorContext']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
@@ -370,6 +379,7 @@ declare module 'vue' {
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly filterInspectorState: UnwrapRef<typeof import('./src/utils/search')['filterInspectorState']>
readonly formatStateType: UnwrapRef<typeof import('./src/composables/formatStateType')['formatStateType']>
readonly formatStateValue: UnwrapRef<typeof import('./src/composables/format')['formatStateValue']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
@@ -435,10 +445,12 @@ declare module 'vue' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly searchDeepInObject: UnwrapRef<typeof import('./src/utils/search')['searchDeepInObject']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly shiki: UnwrapRef<typeof import('./src/composables/shiki')['shiki']>
readonly specialTokenToString: UnwrapRef<typeof import('./src/utils/search')['specialTokenToString']>
readonly stateTypeColorMap: UnwrapRef<typeof import('./src/constants/stateColorMap')['stateTypeColorMap']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
@@ -530,6 +542,7 @@ declare module 'vue' {
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useHover: UnwrapRef<typeof import('./src/composables/hover')['useHover']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
@@ -592,6 +605,9 @@ declare module 'vue' {
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStateEditor: UnwrapRef<typeof import('./src/composables/state-editor')['useStateEditor']>
readonly useStateEditorContext: UnwrapRef<typeof import('./src/composables/state-editor')['useStateEditorContext']>
readonly useStateEditorDrafting: UnwrapRef<typeof import('./src/composables/state-editor')['useStateEditorDrafting']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
6 changes: 4 additions & 2 deletions packages/client/components.d.ts
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ declare module 'vue' {
CodeSnippets: typeof import('./src/components/code/CodeSnippets.vue')['default']
ComponentTreeNode: typeof import('./src/components/components/ComponentTreeNode.vue')['default']
DevToolsLogo: typeof import('./src/components/common/DevToolsLogo.vue')['default']
Empty: typeof import('./src/components/basic/Empty.vue')['default']
FilepathItem: typeof import('./src/components/assets/FilepathItem.vue')['default']
IconTitle: typeof import('./src/components/common/IconTitle.vue')['default']
IframeView: typeof import('./src/components/common/IframeView.vue')['default']
@@ -33,8 +34,9 @@ declare module 'vue' {
SectionBlock: typeof import('./src/components/common/SectionBlock.vue')['default']
SideNav: typeof import('./src/components/SideNav.vue')['default']
SideNavItem: typeof import('./src/components/SideNavItem.vue')['default']
StateFields: typeof import('./src/components/state/StateFields.vue')['default']
StateFieldsTree: typeof import('./src/components/state/StateFieldsTree.vue')['default']
StateFieldEditor: typeof import('./src/components/state/StateFieldEditor.vue')['default']
StateFieldInputEditor: typeof import('./src/components/state/StateFieldInputEditor.vue')['default']
StateFieldViewer: typeof import('./src/components/state/StateFieldViewer.vue')['default']
TabIcon: typeof import('./src/components/TabIcon.vue')['default']
ToggleExpanded: typeof import('./src/components/basic/ToggleExpanded.vue')['default']
TreeViewer: typeof import('./src/components/tree/TreeViewer.vue')['default']
2 changes: 2 additions & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@
"@trpc/client": "^10.45.2",
"@uni-helper/devtools-shared": "workspace:*",
"@uni-helper/devtools-types": "workspace:*",
"@vue/devtools-applet": "^7.6.7",
"@vue/devtools-kit": "^7.3.6",
"@vue/devtools-ui": "^7.3.4",
"@vueuse/core": "^10.9.0",
@@ -48,6 +49,7 @@
"@unocss/reset": "^0.60.2",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"sass-embedded": "^1.82.0",
"unocss": "^0.60.2",
"unplugin-auto-import": "^0.17.6",
"unplugin-vue-components": "^0.27.0",
8 changes: 8 additions & 0 deletions packages/client/src/components/basic/Empty.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<div class="h-full flex flex-col items-center justify-center op50">
<i class="i-lets-icons:blank-light" />
<span>
<slot />
</span>
</div>
</template>
14 changes: 0 additions & 14 deletions packages/client/src/components/common/ToggleExpanded.vue

This file was deleted.

18 changes: 14 additions & 4 deletions packages/client/src/components/state/ChildStateViewer.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
<script setup lang="ts">
import type { CustomInspectorState } from '@vue/devtools-kit'
import StateFieldViewer from './StateFieldViewer.vue'
withDefaults(defineProps<{
data: unknown
depth?: number
expandedId: string[]
data: CustomInspectorState[]
depth: number
index: string
expandedStateId?: string
}>(), {
depth: 0,
expandedStateId: '',
})
</script>

<template>
<div>
{{ data }}
<div
v-for="(item, i) in data"
:key="i"
>
<StateFieldViewer :data="item" :depth="depth + 1" :index="`${index}-${i}`" :expanded-state-id="expandedStateId" />
</div>
</div>
</template>
142 changes: 54 additions & 88 deletions packages/client/src/components/state/RootStateViewer.vue
Original file line number Diff line number Diff line change
@@ -1,98 +1,64 @@
<script setup lang="tsx">
defineProps<{
data?: unknown
}>()
<script setup lang="ts">
import type { CustomInspectorState } from '@vue/devtools-kit'
import { watchEffect } from 'vue'
import ChildStateViewer from './ChildStateViewer.vue'
import ToggleExpanded from '~/components/basic/ToggleExpanded.vue'
import { createStateEditorContext } from '~/composables/state-editor'
import { useToggleExpanded } from '~/composables/toggle-expanded'
function renderCollection(items: any[], isObject: boolean = false) {
return items.map((item, index, array) => {
const key = isObject ? item[0] : null
const value = isObject ? item[1] : item
const formatState = formatStateValue(value)
const props = withDefaults(defineProps<{
data: Record<string, CustomInspectorState[]>
nodeId: string
inspectorId: string
disableEdit?: boolean
expandedStateId?: string
}>(), {
disableEdit: false,
expandedStateId: '',
})
return (
<>
{isObject && `${key}: `}
<span style={{ color: formatState?.color }}>
{formatState.rawDisplay}
</span>
{index !== array.length - 1 ? ', ' : ''}
</>
)
})
}
function DataKeysPreview(data: object) {
if (isPlainObject(data)) {
const entries = Object.entries(data)
return (
<>
{'{'}
{renderCollection(entries, true)}
{'}'}
</>
)
}
else if (isArray(data)) {
const arrayData = Array.from(data)
return (
<>
{`(${arrayData.length}) `}
[
{renderCollection(arrayData)}
]
</>
)
function initEditorContext() {
return {
nodeId: props.nodeId,
inspectorId: props.inspectorId,
disableEdit: props.disableEdit,
}
}
else if (isSet(data)) {
const setData = Array.from(data)
return (
<>
{`Set(${setData.length}) `}
{`{`}
{renderCollection(setData)}
{`}`}
</>
)
}
const { context } = createStateEditorContext(initEditorContext())
watchEffect(() => {
context.value = initEditorContext()
})
else if (isMap(data)) {
const mapData = Array.from((data as Map<any, any>).entries())
return (
<>
{`Map(${mapData.length}) `}
{`{`}
{renderCollection(mapData, true)}
{`}`}
</>
)
}
}
function CustomValuePreview(data: unknown) {
const formatState = formatStateValue(data)
return (
<span style={{ color: formatState?.color }} class="pl1rem">
{formatState.rawDisplay}
</span>
)
}
const isExpanded = ref(false)
function toggleExpanded() {
isExpanded.value = !isExpanded.value
}
const { expanded, toggleExpanded } = useToggleExpanded(props.expandedStateId)
</script>

<template>
<div v-if="typeof data === 'object' && data !== null" truncate @click="toggleExpanded">
<ToggleExpanded
:value="isExpanded"
cursor-pointer
/>
<span class="font-state-field text-3.5 italic">
<component :is="DataKeysPreview(data)" />
</span>
<ChildStateViewer v-if="isExpanded" :data />
<div>
<div
v-for="(item, key, index) in data"
:key="index"
>
<div
class="flex items-center"
:class="[item?.length && 'cursor-pointer hover:(bg-active)']"
@click="toggleExpanded(`${index}`)"
>
<ToggleExpanded
v-if="item?.length"
:value="expanded.includes(`${index}`)"
/>
<!-- placeholder -->
<span v-else pl5 />
<span font-state-field text-3.5 text-hex-a3a3a3>
{{ key }}
</span>
</div>
<div
v-if="item?.length && expanded.includes(`${index}`)"
>
<ChildStateViewer :data="item" :index="`${index}`" :expanded-state-id="expandedStateId" />
</div>
</div>
</div>
<component :is="CustomValuePreview(data)" v-else />
</template>
166 changes: 166 additions & 0 deletions packages/client/src/components/state/StateFieldEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<script setup lang="ts">
import type { CustomInspectorState, DevToolsV6PluginAPIHookKeys, DevToolsV6PluginAPIHookPayloads } from '@vue/devtools-kit'
import type { ButtonProps } from '@vue/devtools-ui'
import { rpc } from '@vue/devtools-core'
import { getRaw } from '@vue/devtools-kit'
import { VueButton, VueDropdown, VueDropdownButton, VueIcon, vTooltip } from '@vue/devtools-ui'
import { useClipboard } from '@vueuse/core'
import { computed, ref, toRaw } from 'vue'
import type { EditorAddNewPropType, EditorInputValidType } from '~/composables/state-editor'
import { useStateEditorContext } from '~/composables/state-editor'
const props = withDefaults(defineProps<{
data: CustomInspectorState & { key?: string }
hovering: boolean
depth: number
showAddIfNeeded?: boolean
disableEdit?: boolean
}>(), {
showAddIfNeeded: true,
})
defineEmits<{
enableEditInput: [type: EditorInputValidType]
addNewProp: [type: EditorAddNewPropType]
}>()
const state = useStateEditorContext()
const { copy, isSupported } = useClipboard()
const popupVisible = ref(false)
const raw = computed(() => getRaw(props.data.value))
const rawValue = computed(() => raw.value.value)
const customType = computed(() => raw.value.customType)
const dataType = computed(() => rawValue.value === null ? 'null' : typeof rawValue.value)
const iconButtonProps = {
flat: true,
size: 'mini',
} satisfies ButtonProps
const buttonClass = computed(() => ({
'opacity-0': !props.hovering,
}))
async function quickEdit(v: unknown, remove: boolean = false) {
await rpc.value.editInspectorState({
path: props.data.path || [props.data.key],
inspectorId: state.value.inspectorId,
type: props.data.stateType!,
nodeId: state.value.nodeId,
state: {
newKey: null!,
value: toRaw(v),
type: dataType.value,
remove,
},
} as unknown as DevToolsV6PluginAPIHookPayloads[DevToolsV6PluginAPIHookKeys.EDIT_COMPONENT_STATE])
await rpc.value.sendInspectorState(state.value.inspectorId)
}
function quickEditNum(v: number | string, offset: 1 | -1) {
const target = typeof v === 'number'
? v + offset
: BigInt(v) + BigInt(offset)
quickEdit(target)
}
</script>

<template>
<div class="flex pl5px">
<!-- only editable will show operate actions -->
<template v-if="!props.disableEdit && data.editable">
<!-- input edit, number/string/object -->
<template v-if="dataType === 'string' || dataType === 'number' || dataType === 'object' || dataType === 'null'">
<VueButton
v-tooltip="{
content: 'Edit value',
}" v-bind="iconButtonProps" :class="buttonClass" @click.stop="$emit('enableEditInput', dataType)"
>
<template #icon>
<VueIcon icon="i-material-symbols-edit-rounded" />
</template>
</VueButton>
<VueButton
v-if="dataType === 'object' && showAddIfNeeded"
v-tooltip="{
content: 'Add new value',
}" v-bind="iconButtonProps" :class="buttonClass" @click.stop="
$emit('addNewProp', Array.isArray(rawValue) ? 'array' : 'object')"
>
<template #icon>
<VueIcon icon="i-material-symbols-add-circle-rounded" />
</template>
</VueButton>
</template>
<!-- checkbox, button value only -->
<VueButton
v-if="dataType === 'boolean'" v-bind="iconButtonProps" :class="buttonClass"
@click="quickEdit(!rawValue)"
>
<template #icon>
<VueIcon :icon="rawValue ? 'i-material-symbols-check-box-sharp' : 'i-material-symbols-check-box-outline-blank-sharp'" />
</template>
</VueButton>
<!-- increment/decrement button, numeric/bigint -->
<template v-else-if="dataType === 'number' || customType === 'bigint'">
<VueButton v-bind="iconButtonProps" :class="buttonClass" @click.stop="quickEditNum(rawValue as number | string, 1)">
<template #icon>
<VueIcon icon="i-carbon-add" />
</template>
</VueButton>
<VueButton v-bind="iconButtonProps" :class="buttonClass" @click.stop="quickEditNum(rawValue as number | string, -1)">
<template #icon>
<VueIcon icon="i-carbon-subtract" />
</template>
</VueButton>
</template>
</template>
<!-- delete prop, only appear if depth > 0 -->
<VueButton v-if="!props.disableEdit && depth > 0 && data.editable" v-bind="iconButtonProps" :class="buttonClass" @click.stop="quickEdit(rawValue, true)">
<template #icon>
<VueIcon icon="i-material-symbols-delete-rounded" />
</template>
</VueButton>
<!-- Copy key/value -->
<VueDropdown
:class="{
'opacity-0': !hovering && !popupVisible,
}"
:button-props="{
flat: true,
size: 'mini',
}"
:disabled="!isSupported"
@update:visible="v => popupVisible = v"
>
<template #popper>
<div class="w160px py5px">
<VueDropdownButton
@click="copy(typeof rawValue === 'object' ? JSON.stringify(rawValue) : rawValue.toString())"
>
<template #icon>
<VueIcon icon="i-material-symbols-copy-all-rounded" class="mt4px" />
Copy Value
</template>
</VueDropdownButton>
<VueDropdownButton
@click="() => {
copy(data.key!)
}"
>
<template #icon>
<VueIcon icon="i-material-symbols-copy-all-rounded" class="mt4px" />
Copy Path
</template>
</VueDropdownButton>
</div>
</template>
<template #button-icon>
<VueIcon icon="i-material-symbols:more-vert" />
</template>
</VueDropdown>
</div>
</template>
95 changes: 95 additions & 0 deletions packages/client/src/components/state/StateFieldInputEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<script setup lang="ts">
import type { customTypeEnums } from '@vue/devtools-kit'
import { toSubmit } from '@vue/devtools-kit'
import { VueButton, VueIcon, VueInput, vTooltip } from '@vue/devtools-ui'
import { useMagicKeys, useVModel } from '@vueuse/core'
import { debounce } from 'perfect-debounce'
import { computed, ref, watch, watchEffect } from 'vue'
const props = withDefaults(defineProps<{
modelValue: string
customType?: customTypeEnums
showActions?: boolean
autoFocus?: boolean
}>(), {
showActions: true,
autoFocus: true,
})
const emit = defineEmits<{
'cancel': []
'submit': []
'update:modelValue': [value: string]
}>()
const inputType = computed(() => {
if (props.customType === 'date')
return 'datetime-local'
return ''
})
// TODO: keyboard shortcut, esc to cancel, enter to submit
// and show tooltip on button when hovering
const { escape, enter } = useMagicKeys()
watchEffect(() => {
if (escape.value)
emit('cancel')
else if (enter.value)
emit('submit')
})
const value = useVModel(props, 'modelValue', emit)
function tryToParseJSONString(v: unknown) {
try {
toSubmit(v as string, props.customType)
return true
}
catch {
return false
}
}
const isWarning = ref(false)
function checkWarning() {
return debounce(() => {
isWarning.value = !tryToParseJSONString(value.value)
}, 300)
}
watch(value, checkWarning())
</script>

<template>
<span class="flex-inline items-center gap4px">
<VueInput v-model="value" :type="inputType" :variant="isWarning ? 'warning' : 'normal'" class="h25px px4px" :class="customType === 'date' ? 'w240px' : 'w120px'" :auto-focus="autoFocus" @click.stop />
<template v-if="showActions">
<VueButton
v-tooltip="{
content: 'Esc to cancel',
}" size="mini" flat class="p2px!" @click.stop="$emit('cancel')"
>
<template #icon>
<VueIcon icon="i-material-symbols-cancel" />
</template>
</VueButton>
<template v-if="!isWarning">
<VueButton
v-tooltip="{
content: 'Enter to submit change',
}" size="mini" flat class="p2px!" @click.stop="$emit('submit')"
>
<template #icon>
<VueIcon icon="i-material-symbols-save" />
</template>
</VueButton>
</template>
<VueIcon
v-else v-tooltip="{
content: 'Invalid value',
}" icon="i-material-symbols-warning" class="color-warning-500 dark:color-warning-300"
/>
</template>
</span>
</template>
292 changes: 292 additions & 0 deletions packages/client/src/components/state/StateFieldViewer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
<script setup lang="ts">
import type { CustomInspectorState, DevToolsV6PluginAPIHookKeys, DevToolsV6PluginAPIHookPayloads, InspectorCustomState } from '@vue/devtools-kit'
import { rpc } from '@vue/devtools-core'
import { formatInspectorStateValue, getInspectorStateValueType, getRaw, toEdit, toSubmit } from '@vue/devtools-kit'
import { isArray, isObject, sortByKey } from '@vue/devtools-shared'
import { VueButton, VueIcon, vTooltip } from '@vue/devtools-ui'
import { computed, ref, watch } from 'vue'
import ChildStateViewer from './ChildStateViewer.vue'
import StateFieldEditor from './StateFieldEditor.vue'
import StateFieldInputEditor from './StateFieldInputEditor.vue'
import ToggleExpanded from '~/components/basic/ToggleExpanded.vue'
import { useHover } from '~/composables/hover'
import type { EditorAddNewPropType } from '~/composables/state-editor'
import { useStateEditor, useStateEditorContext, useStateEditorDrafting } from '~/composables/state-editor'
import { useToggleExpanded } from '~/composables/toggle-expanded'
const props = defineProps<{
data: CustomInspectorState
depth: number
index: string
expandedStateId?: string
}>()
const STATE_FIELDS_LIMIT_SIZE = 30
const limit = ref(STATE_FIELDS_LIMIT_SIZE)
// display value
const displayedValue = computed(() => formatInspectorStateValue(props.data.value, false, {
customClass: {
string: 'max-w-120 truncate',
},
}))
const type = computed(() => getInspectorStateValueType(props.data.value))
const raw = computed(() => getRaw(props.data.value))
const { expanded, toggleExpanded } = useToggleExpanded(props.expandedStateId ?? '')
// custom state format class
const stateFormatClass = computed(() => {
if (type.value === 'custom')
return `${(props.data.value as InspectorCustomState)._custom?.type ?? 'string'}-custom-state`
else
return 'unknown-state-type'
})
const fieldsCount = computed(() => {
const { value } = raw.value
if (isArray(value))
return value.length
else if (isObject(value))
return Object.keys(value).length
else
return 0
})
const normalizedPath = computed(() => props.data.path || [props.data.key])
// normalized display key
const normalizedDisplayedKey = computed(() => normalizedPath.value[normalizedPath.value.length - 1])
// normalized display value
const normalizedDisplayedValue = computed(() => {
const directlyDisplayedValueMap = ['Reactive']
const extraDisplayedValue = (props.data.value as InspectorCustomState)?._custom?.stateTypeName || props.data?.stateTypeName
if (directlyDisplayedValueMap.includes(extraDisplayedValue as string)) {
return extraDisplayedValue
}
else if ((props.data.value as InspectorCustomState['_custom'])?.fields?.abstract) {
return ''
}
else {
const _type = (props.data.value as InspectorCustomState)?._custom?.type
const _value = type.value === 'custom' && !_type ? `"${displayedValue.value}"` : (displayedValue.value === '' ? `""` : displayedValue.value)
const normalizedType = type.value === 'custom' && _type === 'ref' ? getInspectorStateValueType(_value) : type.value
const selectText = type.value === 'string' ? 'select-text' : ''
const result = `<span title="${type.value === 'string' ? props.data.value : ''}" class="${normalizedType}-state-type flex whitespace-nowrap ${selectText}">${_value}</span>`
if (extraDisplayedValue)
return `${result} <span class="text-gray-500">(${extraDisplayedValue})</span>`
return result
}
})
// normalized display children
const normalizedDisplayedChildren = computed(() => {
const { value, inherit, customType } = raw.value
// The member in native set can only be added or removed.
// It cannot be modified.
const isUneditableType = customType === 'set'
let displayedChildren: unknown[] = []
if (isArray(value)) {
const sliced = value.slice(0, limit.value)
return sliced.map((item, i) => ({
key: i.toString(),
path: [...normalizedPath.value, i.toString()],
value: item,
...inherit,
editable: props.data.editable && !isUneditableType,
creating: false,
})) as unknown as CustomInspectorState[]
}
else if (isObject(value)) {
displayedChildren = Object.keys(value).slice(0, limit.value).map(key => ({
key,
path: [...normalizedPath.value, key],
// @ts-expect-error type is not correct
value: value[key],
...inherit,
editable: props.data.editable && !isUneditableType,
creating: false,
}))
if (type.value !== 'custom')
displayedChildren = sortByKey(displayedChildren)
}
return (displayedChildren === props.data.value ? [] : displayedChildren) as CustomInspectorState[]
})
// has children
const hasChildren = computed(() => {
return normalizedDisplayedChildren.value.length > 0
})
// #region editor
const containerRef = ref<HTMLDivElement>()
const state = useStateEditorContext()
const { isHovering } = useHover(() => containerRef.value)
const { editingType, editing, editingText, toggleEditing, nodeId } = useStateEditor()
watch(() => editing.value, (v) => {
if (v) {
const { value } = raw.value
editingText.value = toEdit(value, raw.value.customType)
}
else {
editingText.value = ''
}
})
async function submit() {
const data = props.data
await rpc.value.editInspectorState({
path: normalizedPath.value,
inspectorId: state.value.inspectorId,
type: data.stateType!,
nodeId: nodeId.value,
state: {
newKey: null!,
type: editingType.value,
value: toSubmit(editingText.value, raw.value.customType),
},
} as unknown as DevToolsV6PluginAPIHookPayloads[DevToolsV6PluginAPIHookKeys.EDIT_COMPONENT_STATE])
await rpc.value.sendInspectorState(state.value.inspectorId)
toggleEditing()
}
// ------ add new prop ------
const { addNewProp: addNewPropApi, draftingNewProp, resetDrafting } = useStateEditorDrafting()
function addNewProp(type: EditorAddNewPropType) {
const index = `${props.depth}-${props.index}`
if (!expanded.value.includes(index))
toggleExpanded(index)
addNewPropApi(type, raw.value.value)
}
async function submitDrafting() {
const data = props.data
await rpc.value.editInspectorState({
path: [...normalizedPath.value, draftingNewProp.value.key],
inspectorId: state.value.inspectorId,
type: data.stateType!,
nodeId: nodeId.value,
state: {
newKey: draftingNewProp.value.key,
type: typeof toSubmit(draftingNewProp.value.value),
value: toSubmit(draftingNewProp.value.value),
},
} as unknown as DevToolsV6PluginAPIHookPayloads[DevToolsV6PluginAPIHookKeys.EDIT_COMPONENT_STATE])
await rpc.value.sendInspectorState(state.value.inspectorId)
resetDrafting()
}
// #endregion
</script>

<template>
<div>
<div
ref="containerRef"
class="font-state-field flex items-center text-3.5"
:class="[hasChildren && 'cursor-pointer hover:(bg-active)']"
:style="{ paddingLeft: `${depth * 15 + 4}px` }"
@click="toggleExpanded(`${depth}-${index}`)"
>
<ToggleExpanded
v-if="hasChildren"
:value="expanded.includes(`${depth}-${index}`)"
/>
<!-- placeholder -->
<span v-else pl5 />
<span whitespace-nowrap text-purple-700 op70 dark:text-purple-300>
{{ normalizedDisplayedKey }}
</span>
<span mx1>:</span>
<StateFieldInputEditor v-if="editing" v-model="editingText" class="mr-1" :custom-type="raw.customType" @cancel="toggleEditing" @submit="submit" />
<span :class="stateFormatClass" class="flex whitespace-nowrap dark:text-#bdc6cf">
<span class="flex" v-html="normalizedDisplayedValue" />
</span>
<StateFieldEditor
:hovering="isHovering" :disable-edit="state.disableEdit || editing"
:data="data" :depth="depth" @enable-edit-input="toggleEditing"
@add-new-prop="addNewProp"
/>
</div>
<div v-if="hasChildren && expanded.includes(`${depth}-${index}`)">
<ChildStateViewer :data="normalizedDisplayedChildren" :depth="depth" :index="index" />
<VueButton v-if="fieldsCount > limit" v-tooltip="'Show more'" flat size="mini" class="ml-4" @click="limit += STATE_FIELDS_LIMIT_SIZE">
<template #icon>
<VueIcon icon="i-material-symbols-more-horiz" />
</template>
</VueButton>
<div v-if="draftingNewProp.enable" :style="{ paddingLeft: `${(depth + 1) * 15 + 4}px` }">
<span overflow-hidden text-ellipsis whitespace-nowrap state-key>
<StateFieldInputEditor v-model="draftingNewProp.key" :show-actions="false" />
</span>
<span mx-1>:</span>
<StateFieldInputEditor v-model="draftingNewProp.value" :auto-focus="false" @cancel="resetDrafting" @submit="submitDrafting" />
</div>
</div>
</div>
</template>

<style lang="scss">
// Maybe related https://github.com/vuejs/core/issues/12241
// Let's leave it global for now, until it's fixed
// This will compiled to `.dark[v-xxx] selectors` if using scoped
.function-custom-state {
& > span {
--at-apply: 'dark:text-#997fff!';
}
}
</style>

<style lang="scss" scoped>
// string
:deep(.string-custom-state) {
--at-apply: string-state-type;
}
// function
:deep(.function-custom-state) {
--at-apply: font-italic;
& > span {
--at-apply: 'text-#0033cc dark:text-#997fff';
font-family: Menlo, monospace;
}
}
// component-definition
:deep(.component-definition-custom-state) {
--at-apply: text-primary-500;
& > span {
--at-apply: 'text-#aaa';
}
}
// component
:deep(.component-custom-state) {
--at-apply: text-primary-500;
&::before {
content: '<';
}
&::after {
content: '>';
}
&::before,
&::after {
--at-apply: 'text-#aaa';
}
}
// native error
:deep(.native.Error-state-type) {
--at-apply: 'text-red-500';
&::before {
content: 'Error:';
margin-right: 4px;
}
}
</style>
46 changes: 0 additions & 46 deletions packages/client/src/components/state/StateFields.vue

This file was deleted.

66 changes: 0 additions & 66 deletions packages/client/src/components/state/StateFieldsTree.vue

This file was deleted.

20 changes: 13 additions & 7 deletions packages/client/src/components/tree/TreeViewer.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
<script setup lang="ts">
import type { ComponentTreeNode } from '@uni-helper/devtools-types'
import type { InspectorTree } from '@vue/devtools-kit'
import { VueIcon } from '@vue/devtools-ui'
import ToggleExpanded from '~/components/basic/ToggleExpanded.vue'
import ComponentTreeViewer from '~/components/tree/TreeViewer.vue'
import { useSelect } from '~/composables/select'
import { useToggleExpanded } from '~/composables/toggle-expanded'
withDefaults(defineProps<{
data: ComponentTreeNode[]
depth: number
withTag: boolean
data: ComponentTreeNode[] | InspectorTree[]
depth?: number
withTag?: boolean
}>(), {
depth: 0,
withTag: false,
})
const emit = defineEmits(['hover', 'leave'])
const emit = defineEmits(['hover', 'leave', 'change'])
const selectedNodeId = defineModel()
const { expanded, toggleExpanded } = useToggleExpanded()
const { select: _select } = useSelect()
function select(id: string) {
selectedNodeId.value = id
emit('change', id)
}
function normalizeLabel(item: ComponentTreeNode | InspectorTree) {
return ('name' in item && item?.name) || ('label' in item && item.label)
}
</script>

@@ -54,20 +60,20 @@ function select(id: string) {
<span v-else pl5 />
<span font-state-field>
<span v-if="withTag" class="text-gray-400 dark:text-gray-600 group-hover:(text-white op50) [.active_&]:(op50 text-white!)">&lt;</span>
<span group-hover:text-white class="ws-nowrap [.active_&]:(text-white)">{{ item.name }}</span>
<span group-hover:text-white class="ws-nowrap [.active_&]:(text-white)">{{ normalizeLabel(item) }}</span>
<span v-if="withTag" class="text-gray-400 dark:text-gray-600 group-hover:(text-white op50) [.active_&]:(op50 text-white!)">&gt;</span>
</span>
</div>
<VueIcon
v-if="item.file"
v-if="(item as ComponentTreeNode).file"
v-tooltip="'Open in Editor'"
title="Open in Editor"
icon="i-carbon-script-reference"

action mr3 flex-none op-0 group-focus:op50 group-hover:op-50
:class="{ 'op-100!': selectedNodeId === item.id }"
:border="false"
@click="openInEditor(item.file)"
@click="openInEditor((item as ComponentTreeNode).file)"
/>
</div>
<div v-if="item?.children?.length && (expanded.includes(item.id) || depth < 2)">
31 changes: 31 additions & 0 deletions packages/client/src/composables/hover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { MaybeRefOrGetter } from '@vueuse/core'
import { useEventListener } from '@vueuse/core'
import { ref } from 'vue'

export interface UseHoverOptions {
enter?: () => void
leave?: () => void
initial?: boolean
}

export function useHover(el: MaybeRefOrGetter<HTMLElement | null | undefined>, options: UseHoverOptions = {}) {
const {
enter = () => { },
leave = () => { },
initial = false,
} = options
const isHovering = ref(initial)

useEventListener(el, 'mouseenter', () => {
isHovering.value = true
enter()
})
useEventListener(el, 'mouseleave', () => {
isHovering.value = false
leave()
})

return {
isHovering,
}
}
91 changes: 91 additions & 0 deletions packages/client/src/composables/state-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { InjectionKey, Ref } from 'vue'
import { computed, inject, provide, ref } from 'vue'

interface StateEditorContext {
nodeId: string
inspectorId: string
disableEdit: boolean
}
const StateEditorSymbolKey: InjectionKey<Ref<StateEditorContext>> = Symbol('StateEditorSymbol')

export function createStateEditorContext(initial: StateEditorContext) {
const context = ref<StateEditorContext>(initial)
provide(StateEditorSymbolKey, context)
return {
context,
}
}

export function useStateEditorContext() {
const context = inject(StateEditorSymbolKey)!
return context
}

export type EditorInputValidType = 'number' | 'string' | 'object' | 'null'
export type EditorAddNewPropType = 'object' | 'array'

export function useStateEditor() {
const editingText = ref('')
const editingType = ref<EditorInputValidType>('string')
const editing = ref(false)

const state = useStateEditorContext()

return {
editingText,
editing,
toggleEditing(t?: EditorInputValidType) {
if (t)
editingType.value = t
editing.value = !editing.value
},
editingType,
nodeId: computed(() => state.value.nodeId),
}
}

function getNextAvailableKey(type: EditorAddNewPropType, data: any) {
if (type === 'array') {
const len = (data as any[]).length
return len
}
const prefix = 'newProp'
let i = 1
while (true) {
const key = `${prefix}${i}`
if (!data[key])
return key
i++
}
}

export function useStateEditorDrafting() {
const draftingNewProp = ref({
enable: false,
key: '',
value: 'undefined',
})

function addNewProp(type: EditorAddNewPropType, data: any) {
const key = getNextAvailableKey(type, data)
draftingNewProp.value = {
enable: true,
key: key.toString(),
value: 'undefined',
}
}

function resetDrafting() {
draftingNewProp.value = {
enable: false,
key: '',
value: 'undefined',
}
}

return {
addNewProp,
resetDrafting,
draftingNewProp,
}
}
134 changes: 75 additions & 59 deletions packages/client/src/pages/pinia.vue
Original file line number Diff line number Diff line change
@@ -1,75 +1,91 @@
<script setup lang="ts">
import { VueDrawer } from '@vue/devtools-ui'
import { VueInput } from '@vue/devtools-ui'
import type { CustomInspectorNode, CustomInspectorState } from '@vue/devtools-kit'
import { parse } from '@vue/devtools-kit'
import { Pane, Splitpanes } from 'splitpanes'
import { filterInspectorState } from '~/utils/search'
import ComponentTree from '~/components/tree/TreeViewer.vue'
const { initState } = useInitState()
const piniaState = initState.value!.piniaState
const piniaStores = ref([
const state = ref<Record<string, CustomInspectorState[]>>({})
const tree = ref<CustomInspectorNode[]>([])
const selectId = ref('')
// const emptyState = computed(() => true)
const piniaRootLabel = [
{
id: '__pinia_root',
label: '🍍 Pinia (root)',
value: piniaState,
},
])
]
for (const [id, store] of Object.entries(piniaState)) {
piniaStores.value.push({
id,
label: id,
value: store,
})
}
trpc.onPiniaState.subscribe(undefined, {
onData: (data) => {
const _state = data.map(item => parse(item))
state.value = {
state: _state,
}
tree.value = piniaRootLabel.concat(_state.map((item) => {
const key = item.key
return {
id: key,
label: key,
}
}))
},
})
const activeIndex = ref('')
const selected = ref(false)
const data = computed(() => {
const store = piniaStores.value.find(item => item.id === activeIndex.value)!
return [
{
key: 'State',
value: store.value,
},
]
// 搜索store
const filterStoreKey = ref('')
const filterTree = computed(() => {
if (!filterStoreKey.value)
return tree.value
return tree.value.filter(item => item.label.toLowerCase().includes(filterStoreKey.value.toLowerCase()))
})
function select(index: string) {
activeIndex.value = index
selected.value = true
// data.value = [{
// key: 'State',
// value: piniaStores.value.find(item => item.id === index)!.value,
// }]
}
// const data = ref([
// {
// key: 'state',
// value: piniaState,
// },
// ])
const filterStateKey = ref('')
const selectData = computed(() => {
console.log(selectId.value)

Check warning on line 47 in packages/client/src/pages/pinia.vue

GitHub Actions / lint

Unexpected console statement
if (!selectId.value)
return state.value
return {
state: state.value.state?.find(item => (item.key as unknown as string) === selectId.value),
}
})
const emptyState = computed(() => !selectData.value.state?.length)
const displayState = computed(() => {
const state = selectData.value
return filterInspectorState({
state,
filterKey: filterStateKey.value,
})
})
</script>

<template>
<PanelGrids class="drawer-container" relative block h-full of-hidden>
<div px2>
<div
v-for="item in piniaStores" :key="item.id"
selectable-item
:class="{ active: activeIndex === item.id }"
@click="select(item.id)"
>
<span class="selectable-item-label">
{{ item.label }}
</span>
</div>
</div>
<VueDrawer
v-model="selected"
permanent mount-to=".drawer-container"
content-class="text-sm b-l-0 w-full"
position="absolute"
>
<div h-screen select-none overflow-scroll p-2 class="no-scrollbar">
<StateFields v-for="(item, index) in data" :id="index" :key="index" :data="item" />
</div>
</VueDrawer>
<Splitpanes class="flex-1 overflow-auto">
<Pane border="r base" size="40" h-full>
<div class="h-full flex flex-col p2">
<div class="grid grid-cols-[1fr_auto] mb1 items-center gap2 pb1" border="b dashed base">
<VueInput v-model="filterStoreKey" placeholder="filter Pinia store" />
</div>
<div class="no-scrollbar flex-1 select-none overflow-scroll">
<ComponentTree :data="filterTree" @change="(id) => selectId = id" />
</div>
</div>
</Pane>
<Pane size="60">
<div class="h-full flex flex-col p2">
<div class="grid grid-cols-[1fr_auto] mb1 items-center gap2 pb1" border="b dashed base">
<VueInput v-model="filterStateKey" placeholder="filter Pinia state" />
</div>
<RootStateViewer v-if="!emptyState" class="no-scrollbar flex-1 overflow-scroll" :data="displayState" :node-id="piniaRootLabel[0].id" inspector-id="pinia" expanded-state-id="pinia-store-state" />
<Empty v-else>
No Data
</Empty>
</div>
</Pane>
</Splitpanes>
</PanelGrids>
</template>
184 changes: 184 additions & 0 deletions packages/client/src/utils/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* @source https://github.com/vuejs/devtools/blob/main/packages/applet/src/utils/search.ts
* @license
> MIT License
> Copyright (c) 2023 webfansplz
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.
*/

import type { CustomInspectorState } from '@vue/devtools-kit'
import { INFINITY, NAN, NEGATIVE_INFINITY, UNDEFINED, isPlainObject } from '@vue/devtools-kit'

/**
* Searches a key or value in the object, with a maximum deepness
* @param {*} obj Search target
* @param {string} searchTerm Search string
* @returns {boolean} Search match
*/
export function searchDeepInObject(obj: Record<any, any>, searchTerm: string) {
const seen = new Map()
const result = internalSearchObject(obj, searchTerm.toLowerCase(), seen, 0)
seen.clear()
return result
}

const SEARCH_MAX_DEPTH = 10

/**
* Executes a search on each field of the provided object
* @param {*} obj Search target
* @param {string} searchTerm Search string
* @param {Map<any,boolean>} seen Map containing the search result to prevent stack overflow by walking on the same object multiple times
* @param {number} depth Deep search depth level, which is capped to prevent performance issues
* @returns {boolean} Search match
*/
function internalSearchObject(obj: Record<any, any>, searchTerm: string, seen: Map<unknown, boolean | null>, depth: number): boolean {
if (depth > SEARCH_MAX_DEPTH)
return false

let match = false
const keys = Object.keys(obj)
let key, value
for (let i = 0; i < keys.length; i++) {
key = keys[i]
value = obj[key]
match = internalSearchCheck(searchTerm, key, value, seen, depth + 1)
if (match)
break
}
return match
}

/**
* Checks if the provided field matches the search terms
* @param {string} searchTerm Search string
* @param {string} key Field key (null if from array)
* @param {*} value Field value
* @param {Map<any,boolean>} seen Map containing the search result to prevent stack overflow by walking on the same object multiple times
* @param {number} depth Deep search depth level, which is capped to prevent performance issues
* @returns {boolean} Search match
*/
function internalSearchCheck(searchTerm: string, key: string | null, value: any, seen: Map<unknown, boolean | null>, depth: number): boolean {
let match = false
let result
if (key === '_custom') {
key = value.display
value = value.value
}
(result = specialTokenToString(value)) && (value = result)
if (key && compare(key, searchTerm)) {
match = true
seen.set(value, true)
}
else if (seen.has(value)) {
match = seen.get(value)!
}
else if (Array.isArray(value)) {
seen.set(value, null)
match = internalSearchArray(value, searchTerm, seen, depth)
seen.set(value, match)
}
else if (isPlainObject(value)) {
seen.set(value, null)
match = internalSearchObject(value, searchTerm, seen, depth)
seen.set(value, match)
}
else if (compare(value, searchTerm)) {
match = true
seen.set(value, true)
}
return match
}

/**
* Compares two values
* @param {*} value Mixed type value that will be cast to string
* @param {string} searchTerm Search string
* @returns {boolean} Search match
*/
function compare(value: unknown, searchTerm: string): boolean {
return (`${value}`).toLowerCase().includes(searchTerm)
}

export function specialTokenToString(value: unknown) {
if (value === null)
return 'null'

else if (value === UNDEFINED)
return 'undefined'

else if (value === NAN)
return 'NaN'

else if (value === INFINITY)
return 'Infinity'

else if (value === NEGATIVE_INFINITY)
return '-Infinity'

return false
}

/**
* Executes a search on each value of the provided array
* @param {*} array Search target
* @param {string} searchTerm Search string
* @param {Map<any,boolean>} seen Map containing the search result to prevent stack overflow by walking on the same object multiple times
* @param {number} depth Deep search depth level, which is capped to prevent performance issues
* @returns {boolean} Search match
*/
function internalSearchArray(array: unknown[], searchTerm: string, seen: Map<unknown, boolean | null>, depth: number): boolean {
if (depth > SEARCH_MAX_DEPTH)
return false

let match = false
let value
for (let i = 0; i < array.length; i++) {
value = array[i]
match = internalSearchCheck(searchTerm, null, value, seen, depth + 1)
if (match)
break
}
return match
}

export function filterInspectorState<T extends CustomInspectorState>(params: {
state: Record<string, T[]>
filterKey?: string | null | undefined
// Each group is a flatten object
processGroup?: (item: T[]) => T[]
}) {
const { state, filterKey, processGroup } = params
if (!filterKey || !filterKey.trim().length)
return state
const result = {}
for (const groupKey in state) {
const group = state[groupKey]
const groupFields = group.filter(el => searchDeepInObject({
// @ts-expect-error typing weak
[el.key]: el.value,
}, filterKey))
if (groupFields.length) {
// @ts-expect-error typing weak
result[groupKey] = processGroup ? processGroup(groupFields) : groupFields
}
}
return result
}
37 changes: 31 additions & 6 deletions packages/plugin/inspect/piniaProxy.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
import { stringify } from '@vue/devtools-kit'
import { trpc } from './trpc'

/**
*
* @param {string} id
* @param {*} state
*/
function sendPiniaState(id, state) {
const data = {
key: id,
value: state,
}

trpc.sendPiniaState.subscribe(
// @ts-ignore
{
[id]: stringify(data),
},
{
onComplete: () => {},
},
)
}

/**
* @param {import("pinia").PiniaPluginContext} ctx
*/
export default ({ store }) => {
console.log('store created', store.$id)
console.log('initState', store.$state)
export default ({ store, options }) => {
sendPiniaState(store.$id, store.$state)
console.log('Pinia store', store)
console.log('Pinia options', options)
store.$subscribe((mutation, state) => {
console.log(mutation.storeId)
console.log('store changed', state)
}, { detached: true })
sendPiniaState(mutation.storeId, state)
})
}
6 changes: 0 additions & 6 deletions packages/plugin/src/devtoolServer/rpc/component.ts
Original file line number Diff line number Diff line change
@@ -9,11 +9,6 @@ import { publicProcedure, router } from './../trpc'
export function componentRouter(eventEmitter: EventEmitter) {
const { input, subscription, query } = publicProcedure
let componentTree: ComponentTreeNode
const ComponentTreeNodeSchema: z.ZodSchema<ComponentTreeNode> = z.object({
name: z.string(),
file: z.string(),
children: z.lazy(() => z.array(ComponentTreeNodeSchema).optional()),
})

return router({
getComponent: query(() => {
@@ -22,7 +17,6 @@ export function componentRouter(eventEmitter: EventEmitter) {
}),
setComponentTree: input(z.unknown()).subscription(({ input }) => {
eventEmitter.emit('setComponentTree', input)
console.log('setComponentTree', input)
componentTree = input as ComponentTreeNode
}),
onComponentTree: subscription(() => {
2 changes: 2 additions & 0 deletions packages/plugin/src/devtoolServer/rpc/index.ts
Original file line number Diff line number Diff line change
@@ -5,12 +5,13 @@
import { parseStack } from 'error-stack-parser-es/lite'
import { extractPathByStack, sourceFile } from '../../utils/sourceFile'
import { openInBrowser, openInEditor } from '../../openCommands'
import type { ConsoleInfo, ModuleInfo, Options } from './../../types'

Check failure on line 8 in packages/plugin/src/devtoolServer/rpc/index.ts

GitHub Actions / lint

'ModuleInfo' is defined but never used
import { mergeRouters, publicProcedure, router } from './../trpc'
import { getImageMeta, getStaticAssets, getTextAssetContent } from './assets'
import { versionRouter } from './version'
import { pageRouter } from './page'
import { componentRouter } from './component'
import { piniaRouter } from './pinia'

export default function (
config: ResolvedConfig,
@@ -75,5 +76,6 @@
versionRouter(eventEmitter),
pageRouter(eventEmitter, options),
componentRouter(eventEmitter),
piniaRouter(eventEmitter),
)
}
40 changes: 40 additions & 0 deletions packages/plugin/src/devtoolServer/rpc/pinia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { EventEmitter } from 'node:stream'
import { z } from 'zod'
import { observable } from '@trpc/server/observable'
import { publicProcedure, router } from './../trpc'

export function piniaRouter(eventEmitter: EventEmitter) {
const { input, subscription } = publicProcedure
type PiniaState = Record<string, string>
const piniaState: PiniaState = {}

return router({
sendPiniaState: input(
z.record(
z.string(),
z.string(),
),
).subscription(({ input }) => {
for (const [key, value] of Object.entries(input)) {
piniaState[key] = value
}
eventEmitter.emit('pinia', Object.values(piniaState))
}),
onPiniaState: subscription(() => {
return observable<string[]>((emit) => {
const piniaStateHandler = (data: string[]) => {
emit.next(data)
}

eventEmitter.on('pinia', piniaStateHandler)

if (Object.keys(piniaState).length !== 0) {
emit.next(Object.values(piniaState))
}
return () => {
eventEmitter.off('pinia', piniaStateHandler)
}
})
}),
})
}
3 changes: 2 additions & 1 deletion playground/src/stores/couter1.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { defineStore } from 'pinia';

export const useCounterStore1 = defineStore('counter1', () => {
const count = ref(0);
const test=1
const count = ref(test);
const doubleCount = computed(() => count.value * 2);
function inc() {
count.value++;
664 changes: 608 additions & 56 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

0 comments on commit 3b3ebbb

Please sign in to comment.