-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
feat: pinia init
1 parent
940e225
commit 3b3ebbb
Showing
23 changed files
with
1,728 additions
and
355 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
142
packages/client/src/components/state/RootStateViewer.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
166
packages/client/src/components/state/StateFieldEditor.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
95
packages/client/src/components/state/StateFieldInputEditor.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
292
packages/client/src/components/state/StateFieldViewer.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
}), | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.