mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
fase 5
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-present Tanner Linsley
|
||||
|
||||
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.
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "@tanstack/query-devtools",
|
||||
"version": "5.93.0",
|
||||
"description": "Developer tools to interact with and visualize the TanStack Query cache",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TanStack/query.git",
|
||||
"directory": "packages/query-devtools"
|
||||
},
|
||||
"homepage": "https://tanstack.com/query",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./build/index.cjs",
|
||||
"module": "./build/index.js",
|
||||
"types": "./build/index.d.ts",
|
||||
"browser": {},
|
||||
"exports": {
|
||||
"@tanstack/custom-condition": "./src/index.ts",
|
||||
"solid": {
|
||||
"development": "./build/index.js",
|
||||
"import": "./build/index.js"
|
||||
},
|
||||
"development": {
|
||||
"import": {
|
||||
"types": "./build/index.d.ts",
|
||||
"default": "./build/dev.js"
|
||||
},
|
||||
"require": "./build/dev.cjs"
|
||||
},
|
||||
"import": {
|
||||
"types": "./build/index.d.ts",
|
||||
"default": "./build/index.js"
|
||||
},
|
||||
"require": "./build/index.cjs"
|
||||
},
|
||||
"files": [
|
||||
"build",
|
||||
"src",
|
||||
"!src/__tests__"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@kobalte/core": "^0.13.4",
|
||||
"@solid-primitives/keyed": "^1.2.2",
|
||||
"@solid-primitives/resize-observer": "^2.0.26",
|
||||
"@solid-primitives/storage": "^1.3.11",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"clsx": "^2.1.1",
|
||||
"goober": "^2.1.16",
|
||||
"npm-run-all2": "^5.0.0",
|
||||
"solid-js": "^1.9.7",
|
||||
"solid-transition-group": "^0.2.3",
|
||||
"superjson": "^2.2.2",
|
||||
"tsup-preset-solid": "^2.2.0",
|
||||
"vite-plugin-solid": "^2.11.6",
|
||||
"@tanstack/query-core": "5.90.20"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "premove ./build ./coverage ./dist-ts",
|
||||
"compile": "tsc --build",
|
||||
"test:eslint": "eslint --concurrency=auto ./src",
|
||||
"test:types": "npm-run-all --serial test:types:*",
|
||||
"test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js --build",
|
||||
"test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js --build",
|
||||
"test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js --build",
|
||||
"test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js --build",
|
||||
"test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build",
|
||||
"test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build",
|
||||
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build",
|
||||
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build",
|
||||
"test:types:tscurrent": "tsc --build",
|
||||
"test:lib": "vitest",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"test:build": "publint --strict && attw --pack",
|
||||
"build": "tsup --tsconfig tsconfig.prod.json",
|
||||
"build:dev": "tsup --watch"
|
||||
}
|
||||
}
|
||||
+3779
File diff suppressed because it is too large
Load Diff
+36
@@ -0,0 +1,36 @@
|
||||
import { createLocalStorage } from '@solid-primitives/storage'
|
||||
import { createMemo } from 'solid-js'
|
||||
import { Devtools } from './Devtools'
|
||||
import { getPreferredColorScheme } from './utils'
|
||||
import { THEME_PREFERENCE } from './constants'
|
||||
import { PiPProvider, QueryDevtoolsContext, ThemeContext } from './contexts'
|
||||
import type { Theme } from './contexts'
|
||||
import type { DevtoolsComponentType } from './Devtools'
|
||||
|
||||
const DevtoolsComponent: DevtoolsComponentType = (props) => {
|
||||
const [localStore, setLocalStore] = createLocalStorage({
|
||||
prefix: 'TanstackQueryDevtools',
|
||||
})
|
||||
|
||||
const colorScheme = getPreferredColorScheme()
|
||||
|
||||
const theme = createMemo(() => {
|
||||
const preference = (props.theme ||
|
||||
localStore.theme_preference ||
|
||||
THEME_PREFERENCE) as Theme
|
||||
if (preference !== 'system') return preference
|
||||
return colorScheme()
|
||||
})
|
||||
|
||||
return (
|
||||
<QueryDevtoolsContext.Provider value={props}>
|
||||
<PiPProvider localStore={localStore} setLocalStore={setLocalStore}>
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<Devtools localStore={localStore} setLocalStore={setLocalStore} />
|
||||
</ThemeContext.Provider>
|
||||
</PiPProvider>
|
||||
</QueryDevtoolsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default DevtoolsComponent
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { createLocalStorage } from '@solid-primitives/storage'
|
||||
import { createMemo } from 'solid-js'
|
||||
import { ContentView, ParentPanel } from './Devtools'
|
||||
import { getPreferredColorScheme } from './utils'
|
||||
import { THEME_PREFERENCE } from './constants'
|
||||
import { PiPProvider, QueryDevtoolsContext, ThemeContext } from './contexts'
|
||||
import type { Theme } from './contexts'
|
||||
import type { DevtoolsComponentType } from './Devtools'
|
||||
|
||||
const DevtoolsPanelComponent: DevtoolsComponentType = (props) => {
|
||||
const [localStore, setLocalStore] = createLocalStorage({
|
||||
prefix: 'TanstackQueryDevtools',
|
||||
})
|
||||
|
||||
const colorScheme = getPreferredColorScheme()
|
||||
|
||||
const theme = createMemo(() => {
|
||||
const preference = (props.theme ||
|
||||
localStore.theme_preference ||
|
||||
THEME_PREFERENCE) as Theme
|
||||
if (preference !== 'system') return preference
|
||||
return colorScheme()
|
||||
})
|
||||
|
||||
return (
|
||||
<QueryDevtoolsContext.Provider value={props}>
|
||||
<PiPProvider
|
||||
disabled
|
||||
localStore={localStore}
|
||||
setLocalStore={setLocalStore}
|
||||
>
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<ParentPanel>
|
||||
<ContentView
|
||||
localStore={localStore}
|
||||
setLocalStore={setLocalStore}
|
||||
onClose={props.onClose}
|
||||
showPanelViewOnly
|
||||
/>
|
||||
</ParentPanel>
|
||||
</ThemeContext.Provider>
|
||||
</PiPProvider>
|
||||
</QueryDevtoolsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default DevtoolsPanelComponent
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
import { serialize, stringify } from 'superjson'
|
||||
import { clsx as cx } from 'clsx'
|
||||
import {
|
||||
Index,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createMemo,
|
||||
createSignal,
|
||||
createUniqueId,
|
||||
} from 'solid-js'
|
||||
import { Key } from '@solid-primitives/keyed'
|
||||
import * as goober from 'goober'
|
||||
import { tokens } from './theme'
|
||||
import {
|
||||
deleteNestedDataByPath,
|
||||
displayValue,
|
||||
updateNestedDataByPath,
|
||||
} from './utils'
|
||||
import {
|
||||
Check,
|
||||
CopiedCopier,
|
||||
Copier,
|
||||
ErrorCopier,
|
||||
List,
|
||||
Pencil,
|
||||
Trash,
|
||||
} from './icons'
|
||||
import { useQueryDevtoolsContext, useTheme } from './contexts'
|
||||
import type { Query } from '@tanstack/query-core'
|
||||
|
||||
/**
|
||||
* Chunk elements in the array by size
|
||||
*
|
||||
* when the array cannot be chunked evenly by size, the last chunk will be
|
||||
* filled with the remaining elements
|
||||
*
|
||||
* @example
|
||||
* chunkArray(['a','b', 'c', 'd', 'e'], 2) // returns [['a','b'], ['c', 'd'], ['e']]
|
||||
*/
|
||||
function chunkArray<T extends { label: string; value: unknown }>(
|
||||
array: Array<T>,
|
||||
size: number,
|
||||
): Array<Array<T>> {
|
||||
if (size < 1) return []
|
||||
let i = 0
|
||||
const result: Array<Array<T>> = []
|
||||
while (i < array.length) {
|
||||
result.push(array.slice(i, i + size))
|
||||
i = i + size
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const Expander = (props: { expanded: boolean }) => {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
|
||||
return (
|
||||
<span
|
||||
class={cx(
|
||||
styles().expander,
|
||||
css`
|
||||
transform: rotate(${props.expanded ? 90 : 0}deg);
|
||||
`,
|
||||
props.expanded &&
|
||||
css`
|
||||
& svg {
|
||||
top: -1px;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 12L10 8L6 4"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
|
||||
const CopyButton = (props: { value: unknown }) => {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
const [copyState, setCopyState] = createSignal<CopyState>('NoCopy')
|
||||
|
||||
return (
|
||||
<button
|
||||
class={styles().actionButton}
|
||||
title="Copy object to clipboard"
|
||||
aria-label={`${
|
||||
copyState() === 'NoCopy'
|
||||
? 'Copy object to clipboard'
|
||||
: copyState() === 'SuccessCopy'
|
||||
? 'Object copied to clipboard'
|
||||
: 'Error copying object to clipboard'
|
||||
}`}
|
||||
onClick={
|
||||
copyState() === 'NoCopy'
|
||||
? () => {
|
||||
navigator.clipboard.writeText(stringify(props.value)).then(
|
||||
() => {
|
||||
setCopyState('SuccessCopy')
|
||||
setTimeout(() => {
|
||||
setCopyState('NoCopy')
|
||||
}, 1500)
|
||||
},
|
||||
(err) => {
|
||||
console.error('Failed to copy: ', err)
|
||||
setCopyState('ErrorCopy')
|
||||
setTimeout(() => {
|
||||
setCopyState('NoCopy')
|
||||
}, 1500)
|
||||
},
|
||||
)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={copyState() === 'NoCopy'}>
|
||||
<Copier />
|
||||
</Match>
|
||||
<Match when={copyState() === 'SuccessCopy'}>
|
||||
<CopiedCopier theme={theme()} />
|
||||
</Match>
|
||||
<Match when={copyState() === 'ErrorCopy'}>
|
||||
<ErrorCopier />
|
||||
</Match>
|
||||
</Switch>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const ClearArrayButton = (props: {
|
||||
dataPath: Array<string>
|
||||
activeQuery: Query
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
const queryClient = useQueryDevtoolsContext().client
|
||||
|
||||
return (
|
||||
<button
|
||||
class={styles().actionButton}
|
||||
title={'Remove all items'}
|
||||
aria-label={'Remove all items'}
|
||||
onClick={() => {
|
||||
const oldData = props.activeQuery.state.data
|
||||
const newData = updateNestedDataByPath(oldData, props.dataPath, [])
|
||||
queryClient.setQueryData(props.activeQuery.queryKey, newData)
|
||||
}}
|
||||
>
|
||||
<List />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const DeleteItemButton = (props: {
|
||||
dataPath: Array<string>
|
||||
activeQuery: Query
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
const queryClient = useQueryDevtoolsContext().client
|
||||
|
||||
return (
|
||||
<button
|
||||
class={cx(styles().actionButton)}
|
||||
title={'Delete item'}
|
||||
aria-label={'Delete item'}
|
||||
onClick={() => {
|
||||
const oldData = props.activeQuery.state.data
|
||||
const newData = deleteNestedDataByPath(oldData, props.dataPath)
|
||||
queryClient.setQueryData(props.activeQuery.queryKey, newData)
|
||||
}}
|
||||
>
|
||||
<Trash />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const ToggleValueButton = (props: {
|
||||
dataPath: Array<string>
|
||||
activeQuery: Query
|
||||
value: boolean
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
const queryClient = useQueryDevtoolsContext().client
|
||||
|
||||
return (
|
||||
<button
|
||||
class={cx(
|
||||
styles().actionButton,
|
||||
css`
|
||||
width: ${tokens.size[3.5]};
|
||||
height: ${tokens.size[3.5]};
|
||||
`,
|
||||
)}
|
||||
title={'Toggle value'}
|
||||
aria-label={'Toggle value'}
|
||||
onClick={() => {
|
||||
const oldData = props.activeQuery.state.data
|
||||
const newData = updateNestedDataByPath(
|
||||
oldData,
|
||||
props.dataPath,
|
||||
!props.value,
|
||||
)
|
||||
queryClient.setQueryData(props.activeQuery.queryKey, newData)
|
||||
}}
|
||||
>
|
||||
<Check theme={theme()} checked={props.value} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
type ExplorerProps = {
|
||||
editable?: boolean
|
||||
label: string
|
||||
value: unknown
|
||||
defaultExpanded?: Array<string>
|
||||
dataPath?: Array<string>
|
||||
activeQuery?: Query
|
||||
itemsDeletable?: boolean
|
||||
onEdit?: () => void
|
||||
}
|
||||
|
||||
function isIterable(x: any): x is Iterable<unknown> {
|
||||
return Symbol.iterator in x
|
||||
}
|
||||
|
||||
export default function Explorer(props: ExplorerProps) {
|
||||
const theme = useTheme()
|
||||
const css = useQueryDevtoolsContext().shadowDOMTarget
|
||||
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
|
||||
: goober.css
|
||||
const styles = createMemo(() => {
|
||||
return theme() === 'dark' ? darkStyles(css) : lightStyles(css)
|
||||
})
|
||||
const queryClient = useQueryDevtoolsContext().client
|
||||
|
||||
const [expanded, setExpanded] = createSignal(
|
||||
(props.defaultExpanded || []).includes(props.label),
|
||||
)
|
||||
const toggleExpanded = () => setExpanded((old) => !old)
|
||||
const [expandedPages, setExpandedPages] = createSignal<Array<number>>([])
|
||||
|
||||
const subEntries = createMemo(() => {
|
||||
if (Array.isArray(props.value)) {
|
||||
return props.value.map((d, i) => ({
|
||||
label: i.toString(),
|
||||
value: d,
|
||||
}))
|
||||
} else if (
|
||||
props.value !== null &&
|
||||
typeof props.value === 'object' &&
|
||||
isIterable(props.value) &&
|
||||
typeof props.value[Symbol.iterator] === 'function'
|
||||
) {
|
||||
if (props.value instanceof Map) {
|
||||
return Array.from(props.value, ([key, val]) => ({
|
||||
label: key,
|
||||
value: val,
|
||||
}))
|
||||
}
|
||||
return Array.from(props.value, (val, i) => ({
|
||||
label: i.toString(),
|
||||
value: val,
|
||||
}))
|
||||
} else if (typeof props.value === 'object' && props.value !== null) {
|
||||
return Object.entries(props.value).map(([key, val]) => ({
|
||||
label: key,
|
||||
value: val,
|
||||
}))
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const type = createMemo<string>(() => {
|
||||
if (Array.isArray(props.value)) {
|
||||
return 'array'
|
||||
} else if (
|
||||
props.value !== null &&
|
||||
typeof props.value === 'object' &&
|
||||
isIterable(props.value) &&
|
||||
typeof props.value[Symbol.iterator] === 'function'
|
||||
) {
|
||||
return 'Iterable'
|
||||
} else if (typeof props.value === 'object' && props.value !== null) {
|
||||
return 'object'
|
||||
}
|
||||
return typeof props.value
|
||||
})
|
||||
|
||||
const subEntryPages = createMemo(() => chunkArray(subEntries(), 100))
|
||||
|
||||
const currentDataPath = props.dataPath ?? []
|
||||
|
||||
const inputId = createUniqueId()
|
||||
|
||||
return (
|
||||
<div class={styles().entry}>
|
||||
<Show when={subEntryPages().length}>
|
||||
<div class={styles().expanderButtonContainer}>
|
||||
<button
|
||||
class={styles().expanderButton}
|
||||
onClick={() => toggleExpanded()}
|
||||
aria-expanded={expanded() ? 'true' : 'false'}
|
||||
>
|
||||
<Expander expanded={expanded()} /> <span>{props.label}</span>{' '}
|
||||
<span class={styles().info}>
|
||||
{String(type()).toLowerCase() === 'iterable' ? '(Iterable) ' : ''}
|
||||
{subEntries().length} {subEntries().length > 1 ? `items` : `item`}
|
||||
</span>
|
||||
</button>
|
||||
<Show when={props.editable}>
|
||||
<div class={styles().actions}>
|
||||
<CopyButton value={props.value} />
|
||||
|
||||
<Show
|
||||
when={props.itemsDeletable && props.activeQuery !== undefined}
|
||||
>
|
||||
<DeleteItemButton
|
||||
activeQuery={props.activeQuery!}
|
||||
dataPath={currentDataPath}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={type() === 'array' && props.activeQuery !== undefined}
|
||||
>
|
||||
<ClearArrayButton
|
||||
activeQuery={props.activeQuery!}
|
||||
dataPath={currentDataPath}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={!!props.onEdit && !serialize(props.value).meta}>
|
||||
<button
|
||||
class={styles().actionButton}
|
||||
title={'Bulk Edit Data'}
|
||||
aria-label={'Bulk Edit Data'}
|
||||
onClick={() => {
|
||||
props.onEdit?.()
|
||||
}}
|
||||
>
|
||||
<Pencil />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={expanded()}>
|
||||
<Show when={subEntryPages().length === 1}>
|
||||
<div class={styles().subEntry}>
|
||||
<Key each={subEntries()} by={(item) => item.label}>
|
||||
{(entry) => {
|
||||
return (
|
||||
<Explorer
|
||||
defaultExpanded={props.defaultExpanded}
|
||||
label={entry().label}
|
||||
value={entry().value}
|
||||
editable={props.editable}
|
||||
dataPath={[...currentDataPath, entry().label]}
|
||||
activeQuery={props.activeQuery}
|
||||
itemsDeletable={
|
||||
type() === 'array' ||
|
||||
type() === 'Iterable' ||
|
||||
type() === 'object'
|
||||
}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</Key>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={subEntryPages().length > 1}>
|
||||
<div class={styles().subEntry}>
|
||||
<Index each={subEntryPages()}>
|
||||
{(entries, index) => (
|
||||
<div>
|
||||
<div class={styles().entry}>
|
||||
<button
|
||||
onClick={() =>
|
||||
setExpandedPages((old) =>
|
||||
old.includes(index)
|
||||
? old.filter((d) => d !== index)
|
||||
: [...old, index],
|
||||
)
|
||||
}
|
||||
class={styles().expanderButton}
|
||||
>
|
||||
<Expander expanded={expandedPages().includes(index)} />{' '}
|
||||
[{index * 100}...
|
||||
{index * 100 + 100 - 1}]
|
||||
</button>
|
||||
<Show when={expandedPages().includes(index)}>
|
||||
<div class={styles().subEntry}>
|
||||
<Key each={entries()} by={(entry) => entry.label}>
|
||||
{(entry) => (
|
||||
<Explorer
|
||||
defaultExpanded={props.defaultExpanded}
|
||||
label={entry().label}
|
||||
value={entry().value}
|
||||
editable={props.editable}
|
||||
dataPath={[...currentDataPath, entry().label]}
|
||||
activeQuery={props.activeQuery}
|
||||
/>
|
||||
)}
|
||||
</Key>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Index>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={subEntryPages().length === 0}>
|
||||
<div class={styles().row}>
|
||||
<label for={inputId} class={styles().label}>
|
||||
{props.label}:
|
||||
</label>
|
||||
<Show
|
||||
when={
|
||||
props.editable &&
|
||||
props.activeQuery !== undefined &&
|
||||
(type() === 'string' ||
|
||||
type() === 'number' ||
|
||||
type() === 'boolean')
|
||||
}
|
||||
fallback={
|
||||
<span class={styles().value}>{displayValue(props.value)}</span>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={
|
||||
props.editable &&
|
||||
props.activeQuery !== undefined &&
|
||||
(type() === 'string' || type() === 'number')
|
||||
}
|
||||
>
|
||||
<input
|
||||
id={inputId}
|
||||
type={type() === 'number' ? 'number' : 'text'}
|
||||
class={cx(styles().value, styles().editableInput)}
|
||||
value={props.value as string | number}
|
||||
onChange={(changeEvent) => {
|
||||
const oldData = props.activeQuery!.state.data
|
||||
|
||||
const newData = updateNestedDataByPath(
|
||||
oldData,
|
||||
currentDataPath,
|
||||
type() === 'number'
|
||||
? changeEvent.target.valueAsNumber
|
||||
: changeEvent.target.value,
|
||||
)
|
||||
|
||||
queryClient.setQueryData(props.activeQuery!.queryKey, newData)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={type() === 'boolean'}>
|
||||
<span
|
||||
class={cx(
|
||||
styles().value,
|
||||
styles().actions,
|
||||
styles().editableInput,
|
||||
)}
|
||||
>
|
||||
<ToggleValueButton
|
||||
activeQuery={props.activeQuery!}
|
||||
dataPath={currentDataPath}
|
||||
value={props.value as boolean}
|
||||
/>
|
||||
{displayValue(props.value)}
|
||||
</span>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={
|
||||
props.editable &&
|
||||
props.itemsDeletable &&
|
||||
props.activeQuery !== undefined
|
||||
}
|
||||
>
|
||||
<DeleteItemButton
|
||||
activeQuery={props.activeQuery!}
|
||||
dataPath={currentDataPath}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const stylesFactory = (
|
||||
theme: 'light' | 'dark',
|
||||
css: (typeof goober)['css'],
|
||||
) => {
|
||||
const { colors, font, size, border } = tokens
|
||||
const t = (light: string, dark: string) => (theme === 'light' ? light : dark)
|
||||
return {
|
||||
entry: css`
|
||||
& * {
|
||||
font-size: ${font.size.xs};
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
position: relative;
|
||||
outline: none;
|
||||
word-break: break-word;
|
||||
`,
|
||||
subEntry: css`
|
||||
margin: 0 0 0 0.5em;
|
||||
padding-left: 0.75em;
|
||||
border-left: 2px solid ${t(colors.gray[300], colors.darkGray[400])};
|
||||
/* outline: 1px solid ${colors.teal[400]}; */
|
||||
`,
|
||||
expander: css`
|
||||
& path {
|
||||
stroke: ${colors.gray[400]};
|
||||
}
|
||||
& svg {
|
||||
width: ${size[3]};
|
||||
height: ${size[3]};
|
||||
}
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: all 0.1s ease;
|
||||
/* outline: 1px solid ${colors.blue[400]}; */
|
||||
`,
|
||||
expanderButtonContainer: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: ${size[4]};
|
||||
min-height: ${size[4]};
|
||||
gap: ${size[2]};
|
||||
`,
|
||||
expanderButton: css`
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
outline: inherit;
|
||||
height: ${size[5]};
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: ${size[1]};
|
||||
position: relative;
|
||||
/* outline: 1px solid ${colors.green[400]}; */
|
||||
|
||||
&:focus-visible {
|
||||
border-radius: ${border.radius.xs};
|
||||
outline: 2px solid ${colors.blue[800]};
|
||||
}
|
||||
|
||||
& svg {
|
||||
position: relative;
|
||||
left: 1px;
|
||||
}
|
||||
`,
|
||||
info: css`
|
||||
color: ${t(colors.gray[500], colors.gray[500])};
|
||||
font-size: ${font.size.xs};
|
||||
margin-left: ${size[1]};
|
||||
/* outline: 1px solid ${colors.yellow[400]}; */
|
||||
`,
|
||||
label: css`
|
||||
color: ${t(colors.gray[700], colors.gray[300])};
|
||||
white-space: nowrap;
|
||||
`,
|
||||
value: css`
|
||||
color: ${t(colors.purple[600], colors.purple[400])};
|
||||
flex-grow: 1;
|
||||
`,
|
||||
actions: css`
|
||||
display: inline-flex;
|
||||
gap: ${size[2]};
|
||||
align-items: center;
|
||||
`,
|
||||
row: css`
|
||||
display: inline-flex;
|
||||
gap: ${size[2]};
|
||||
width: 100%;
|
||||
margin: ${size[0.25]} 0px;
|
||||
line-height: ${size[4.5]};
|
||||
align-items: center;
|
||||
`,
|
||||
editableInput: css`
|
||||
border: none;
|
||||
padding: ${size[0.5]} ${size[1]} ${size[0.5]} ${size[1.5]};
|
||||
flex-grow: 1;
|
||||
border-radius: ${border.radius.xs};
|
||||
background-color: ${t(colors.gray[200], colors.darkGray[500])};
|
||||
|
||||
&:hover {
|
||||
background-color: ${t(colors.gray[300], colors.darkGray[600])};
|
||||
}
|
||||
`,
|
||||
actionButton: css`
|
||||
background-color: transparent;
|
||||
color: ${t(colors.gray[500], colors.gray[500])};
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
padding: 0px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
width: ${size[3]};
|
||||
height: ${size[3]};
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:hover svg {
|
||||
color: ${t(colors.gray[600], colors.gray[400])};
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-radius: ${border.radius.xs};
|
||||
outline: 2px solid ${colors.blue[800]};
|
||||
outline-offset: 2px;
|
||||
}
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
const lightStyles = (css: (typeof goober)['css']) => stylesFactory('light', css)
|
||||
const darkStyles = (css: (typeof goober)['css']) => stylesFactory('dark', css)
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { render } from 'solid-js/web'
|
||||
import { createSignal, lazy } from 'solid-js'
|
||||
import { setupStyleSheet } from './utils'
|
||||
import type {
|
||||
QueryClient,
|
||||
onlineManager as TOnlineManager,
|
||||
} from '@tanstack/query-core'
|
||||
import type { DevtoolsComponentType } from './Devtools'
|
||||
import type {
|
||||
DevtoolsButtonPosition,
|
||||
DevtoolsErrorType,
|
||||
DevtoolsPosition,
|
||||
QueryDevtoolsProps,
|
||||
Theme,
|
||||
} from './contexts'
|
||||
import type { Signal } from 'solid-js'
|
||||
|
||||
export interface TanstackQueryDevtoolsConfig extends QueryDevtoolsProps {
|
||||
styleNonce?: string
|
||||
shadowDOMTarget?: ShadowRoot
|
||||
}
|
||||
|
||||
class TanstackQueryDevtools {
|
||||
#client: Signal<QueryClient>
|
||||
#onlineManager: typeof TOnlineManager
|
||||
#queryFlavor: string
|
||||
#version: string
|
||||
#isMounted = false
|
||||
#styleNonce?: string
|
||||
#shadowDOMTarget?: ShadowRoot
|
||||
#buttonPosition: Signal<DevtoolsButtonPosition | undefined>
|
||||
#position: Signal<DevtoolsPosition | undefined>
|
||||
#initialIsOpen: Signal<boolean | undefined>
|
||||
#errorTypes: Signal<Array<DevtoolsErrorType> | undefined>
|
||||
#hideDisabledQueries: Signal<boolean | undefined>
|
||||
#Component: DevtoolsComponentType | undefined
|
||||
#theme: Signal<Theme | undefined>
|
||||
#dispose?: () => void
|
||||
|
||||
constructor(config: TanstackQueryDevtoolsConfig) {
|
||||
const {
|
||||
client,
|
||||
queryFlavor,
|
||||
version,
|
||||
onlineManager,
|
||||
buttonPosition,
|
||||
position,
|
||||
initialIsOpen,
|
||||
errorTypes,
|
||||
styleNonce,
|
||||
shadowDOMTarget,
|
||||
hideDisabledQueries,
|
||||
theme,
|
||||
} = config
|
||||
this.#client = createSignal(client)
|
||||
this.#queryFlavor = queryFlavor
|
||||
this.#version = version
|
||||
this.#onlineManager = onlineManager
|
||||
this.#styleNonce = styleNonce
|
||||
this.#shadowDOMTarget = shadowDOMTarget
|
||||
this.#buttonPosition = createSignal(buttonPosition)
|
||||
this.#position = createSignal(position)
|
||||
this.#initialIsOpen = createSignal(initialIsOpen)
|
||||
this.#errorTypes = createSignal(errorTypes)
|
||||
this.#hideDisabledQueries = createSignal(hideDisabledQueries)
|
||||
this.#theme = createSignal(theme)
|
||||
}
|
||||
|
||||
setButtonPosition(position: DevtoolsButtonPosition) {
|
||||
this.#buttonPosition[1](position)
|
||||
}
|
||||
|
||||
setPosition(position: DevtoolsPosition) {
|
||||
this.#position[1](position)
|
||||
}
|
||||
|
||||
setInitialIsOpen(isOpen: boolean) {
|
||||
this.#initialIsOpen[1](isOpen)
|
||||
}
|
||||
|
||||
setErrorTypes(errorTypes: Array<DevtoolsErrorType>) {
|
||||
this.#errorTypes[1](errorTypes)
|
||||
}
|
||||
|
||||
setClient(client: QueryClient) {
|
||||
this.#client[1](client)
|
||||
}
|
||||
|
||||
setTheme(theme?: Theme) {
|
||||
this.#theme[1](theme)
|
||||
}
|
||||
|
||||
mount<T extends HTMLElement>(el: T) {
|
||||
if (this.#isMounted) {
|
||||
throw new Error('Devtools is already mounted')
|
||||
}
|
||||
const dispose = render(() => {
|
||||
const [btnPosition] = this.#buttonPosition
|
||||
const [pos] = this.#position
|
||||
const [isOpen] = this.#initialIsOpen
|
||||
const [errors] = this.#errorTypes
|
||||
const [hideDisabledQueries] = this.#hideDisabledQueries
|
||||
const [queryClient] = this.#client
|
||||
const [theme] = this.#theme
|
||||
let Devtools: DevtoolsComponentType
|
||||
|
||||
if (this.#Component) {
|
||||
Devtools = this.#Component
|
||||
} else {
|
||||
Devtools = lazy(() => import('./DevtoolsComponent'))
|
||||
this.#Component = Devtools
|
||||
}
|
||||
|
||||
setupStyleSheet(this.#styleNonce, this.#shadowDOMTarget)
|
||||
return (
|
||||
<Devtools
|
||||
queryFlavor={this.#queryFlavor}
|
||||
version={this.#version}
|
||||
onlineManager={this.#onlineManager}
|
||||
shadowDOMTarget={this.#shadowDOMTarget}
|
||||
{...{
|
||||
get client() {
|
||||
return queryClient()
|
||||
},
|
||||
get buttonPosition() {
|
||||
return btnPosition()
|
||||
},
|
||||
get position() {
|
||||
return pos()
|
||||
},
|
||||
get initialIsOpen() {
|
||||
return isOpen()
|
||||
},
|
||||
get errorTypes() {
|
||||
return errors()
|
||||
},
|
||||
get hideDisabledQueries() {
|
||||
return hideDisabledQueries()
|
||||
},
|
||||
get theme() {
|
||||
return theme()
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}, el)
|
||||
this.#isMounted = true
|
||||
this.#dispose = dispose
|
||||
}
|
||||
|
||||
unmount() {
|
||||
if (!this.#isMounted) {
|
||||
throw new Error('Devtools is not mounted')
|
||||
}
|
||||
this.#dispose?.()
|
||||
this.#isMounted = false
|
||||
}
|
||||
}
|
||||
|
||||
export { TanstackQueryDevtools }
|
||||
Generated
Vendored
+172
@@ -0,0 +1,172 @@
|
||||
import { render } from 'solid-js/web'
|
||||
import { createSignal, lazy } from 'solid-js'
|
||||
import { setupStyleSheet } from './utils'
|
||||
import type {
|
||||
QueryClient,
|
||||
onlineManager as TOnlineManager,
|
||||
} from '@tanstack/query-core'
|
||||
import type { DevtoolsComponentType } from './Devtools'
|
||||
import type {
|
||||
DevtoolsButtonPosition,
|
||||
DevtoolsErrorType,
|
||||
DevtoolsPosition,
|
||||
QueryDevtoolsProps,
|
||||
Theme,
|
||||
} from './contexts'
|
||||
import type { Signal } from 'solid-js'
|
||||
|
||||
export interface TanstackQueryDevtoolsPanelConfig extends QueryDevtoolsProps {
|
||||
styleNonce?: string
|
||||
shadowDOMTarget?: ShadowRoot
|
||||
onClose?: () => unknown
|
||||
}
|
||||
|
||||
class TanstackQueryDevtoolsPanel {
|
||||
#client: Signal<QueryClient>
|
||||
#onlineManager: typeof TOnlineManager
|
||||
#queryFlavor: string
|
||||
#version: string
|
||||
#isMounted = false
|
||||
#styleNonce?: string
|
||||
#shadowDOMTarget?: ShadowRoot
|
||||
#buttonPosition: Signal<DevtoolsButtonPosition | undefined>
|
||||
#position: Signal<DevtoolsPosition | undefined>
|
||||
#initialIsOpen: Signal<boolean | undefined>
|
||||
#errorTypes: Signal<Array<DevtoolsErrorType> | undefined>
|
||||
#hideDisabledQueries: Signal<boolean | undefined>
|
||||
#onClose: Signal<(() => unknown) | undefined>
|
||||
#Component: DevtoolsComponentType | undefined
|
||||
#theme: Signal<Theme | undefined>
|
||||
#dispose?: () => void
|
||||
|
||||
constructor(config: TanstackQueryDevtoolsPanelConfig) {
|
||||
const {
|
||||
client,
|
||||
queryFlavor,
|
||||
version,
|
||||
onlineManager,
|
||||
buttonPosition,
|
||||
position,
|
||||
initialIsOpen,
|
||||
errorTypes,
|
||||
styleNonce,
|
||||
shadowDOMTarget,
|
||||
onClose,
|
||||
hideDisabledQueries,
|
||||
theme,
|
||||
} = config
|
||||
this.#client = createSignal(client)
|
||||
this.#queryFlavor = queryFlavor
|
||||
this.#version = version
|
||||
this.#onlineManager = onlineManager
|
||||
this.#styleNonce = styleNonce
|
||||
this.#shadowDOMTarget = shadowDOMTarget
|
||||
this.#buttonPosition = createSignal(buttonPosition)
|
||||
this.#position = createSignal(position)
|
||||
this.#initialIsOpen = createSignal(initialIsOpen)
|
||||
this.#errorTypes = createSignal(errorTypes)
|
||||
this.#hideDisabledQueries = createSignal(hideDisabledQueries)
|
||||
this.#onClose = createSignal(onClose)
|
||||
this.#theme = createSignal(theme)
|
||||
}
|
||||
|
||||
setButtonPosition(position: DevtoolsButtonPosition) {
|
||||
this.#buttonPosition[1](position)
|
||||
}
|
||||
|
||||
setPosition(position: DevtoolsPosition) {
|
||||
this.#position[1](position)
|
||||
}
|
||||
|
||||
setInitialIsOpen(isOpen: boolean) {
|
||||
this.#initialIsOpen[1](isOpen)
|
||||
}
|
||||
|
||||
setErrorTypes(errorTypes: Array<DevtoolsErrorType>) {
|
||||
this.#errorTypes[1](errorTypes)
|
||||
}
|
||||
|
||||
setClient(client: QueryClient) {
|
||||
this.#client[1](client)
|
||||
}
|
||||
|
||||
setOnClose(onClose: () => unknown) {
|
||||
this.#onClose[1](() => onClose)
|
||||
}
|
||||
|
||||
setTheme(theme?: Theme) {
|
||||
this.#theme[1](theme)
|
||||
}
|
||||
|
||||
mount<T extends HTMLElement>(el: T) {
|
||||
if (this.#isMounted) {
|
||||
throw new Error('Devtools is already mounted')
|
||||
}
|
||||
const dispose = render(() => {
|
||||
const [btnPosition] = this.#buttonPosition
|
||||
const [pos] = this.#position
|
||||
const [isOpen] = this.#initialIsOpen
|
||||
const [errors] = this.#errorTypes
|
||||
const [hideDisabledQueries] = this.#hideDisabledQueries
|
||||
const [queryClient] = this.#client
|
||||
const [onClose] = this.#onClose
|
||||
const [theme] = this.#theme
|
||||
let Devtools: DevtoolsComponentType
|
||||
|
||||
if (this.#Component) {
|
||||
Devtools = this.#Component
|
||||
} else {
|
||||
Devtools = lazy(() => import('./DevtoolsPanelComponent'))
|
||||
this.#Component = Devtools
|
||||
}
|
||||
|
||||
setupStyleSheet(this.#styleNonce, this.#shadowDOMTarget)
|
||||
return (
|
||||
<Devtools
|
||||
queryFlavor={this.#queryFlavor}
|
||||
version={this.#version}
|
||||
onlineManager={this.#onlineManager}
|
||||
shadowDOMTarget={this.#shadowDOMTarget}
|
||||
{...{
|
||||
get client() {
|
||||
return queryClient()
|
||||
},
|
||||
get buttonPosition() {
|
||||
return btnPosition()
|
||||
},
|
||||
get position() {
|
||||
return pos()
|
||||
},
|
||||
get initialIsOpen() {
|
||||
return isOpen()
|
||||
},
|
||||
get errorTypes() {
|
||||
return errors()
|
||||
},
|
||||
get hideDisabledQueries() {
|
||||
return hideDisabledQueries()
|
||||
},
|
||||
get onClose() {
|
||||
return onClose()
|
||||
},
|
||||
get theme() {
|
||||
return theme()
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}, el)
|
||||
this.#isMounted = true
|
||||
this.#dispose = dispose
|
||||
}
|
||||
|
||||
unmount() {
|
||||
if (!this.#isMounted) {
|
||||
throw new Error('Devtools is not mounted')
|
||||
}
|
||||
this.#dispose?.()
|
||||
this.#isMounted = false
|
||||
}
|
||||
}
|
||||
|
||||
export { TanstackQueryDevtoolsPanel }
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { mutationSortFns, sortFns } from './utils'
|
||||
import type { DevtoolsButtonPosition, DevtoolsPosition } from './contexts'
|
||||
|
||||
export const firstBreakpoint = 1024
|
||||
export const secondBreakpoint = 796
|
||||
export const thirdBreakpoint = 700
|
||||
|
||||
export const BUTTON_POSITION: DevtoolsButtonPosition = 'bottom-right'
|
||||
export const POSITION: DevtoolsPosition = 'bottom'
|
||||
export const THEME_PREFERENCE = 'system'
|
||||
export const INITIAL_IS_OPEN = false
|
||||
export const DEFAULT_HEIGHT = 500
|
||||
export const PIP_DEFAULT_HEIGHT = 500
|
||||
export const DEFAULT_WIDTH = 500
|
||||
export const DEFAULT_SORT_FN_NAME = Object.keys(sortFns)[0]
|
||||
export const DEFAULT_SORT_ORDER = 1
|
||||
export const DEFAULT_MUTATION_SORT_FN_NAME = Object.keys(mutationSortFns)[0]
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
useContext,
|
||||
} from 'solid-js'
|
||||
import { clearDelegatedEvents, delegateEvents } from 'solid-js/web'
|
||||
import { PIP_DEFAULT_HEIGHT } from '../constants'
|
||||
import { useQueryDevtoolsContext } from './QueryDevtoolsContext'
|
||||
import type { Accessor, JSX } from 'solid-js'
|
||||
import type { StorageObject, StorageSetter } from '@solid-primitives/storage'
|
||||
|
||||
interface PiPProviderProps {
|
||||
children: JSX.Element
|
||||
localStore: StorageObject<string>
|
||||
setLocalStore: StorageSetter<string, unknown>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type PiPContextType = {
|
||||
pipWindow: Window | null
|
||||
requestPipWindow: (width: number, height: number) => void
|
||||
closePipWindow: () => void
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
class PipOpenError extends Error {}
|
||||
|
||||
const PiPContext = createContext<Accessor<PiPContextType> | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
export const PiPProvider = (props: PiPProviderProps) => {
|
||||
// Expose pipWindow that is currently active
|
||||
const [pipWindow, setPipWindow] = createSignal<Window | null>(null)
|
||||
|
||||
// Close pipWindow programmatically
|
||||
const closePipWindow = () => {
|
||||
const w = pipWindow()
|
||||
if (w != null) {
|
||||
w.close()
|
||||
setPipWindow(null)
|
||||
}
|
||||
}
|
||||
|
||||
// Open new pipWindow
|
||||
const requestPipWindow = (width: number, height: number) => {
|
||||
// We don't want to allow multiple requests.
|
||||
if (pipWindow() != null) {
|
||||
return
|
||||
}
|
||||
|
||||
const pip = window.open(
|
||||
'',
|
||||
'TSQD-Devtools-Panel',
|
||||
`width=${width},height=${height},popup`,
|
||||
)
|
||||
|
||||
if (!pip) {
|
||||
throw new PipOpenError(
|
||||
'Failed to open popup. Please allow popups for this site to view the devtools in picture-in-picture mode.',
|
||||
)
|
||||
}
|
||||
|
||||
// Remove existing styles
|
||||
pip.document.head.innerHTML = ''
|
||||
// Remove existing body
|
||||
pip.document.body.innerHTML = ''
|
||||
// Clear Delegated Events
|
||||
clearDelegatedEvents(pip.document)
|
||||
|
||||
pip.document.title = 'TanStack Query Devtools'
|
||||
pip.document.body.style.margin = '0'
|
||||
|
||||
// Detect when window is closed by user
|
||||
pip.addEventListener('pagehide', () => {
|
||||
props.setLocalStore('pip_open', 'false')
|
||||
setPipWindow(null)
|
||||
})
|
||||
|
||||
// It is important to copy all parent window styles. Otherwise, there would be no CSS available at all
|
||||
// https://developer.chrome.com/docs/web-platform/document-picture-in-picture/#copy-style-sheets-to-the-picture-in-picture-window
|
||||
;[
|
||||
...(useQueryDevtoolsContext().shadowDOMTarget || document).styleSheets,
|
||||
].forEach((styleSheet) => {
|
||||
try {
|
||||
const cssRules = [...styleSheet.cssRules]
|
||||
.map((rule) => rule.cssText)
|
||||
.join('')
|
||||
const style = document.createElement('style')
|
||||
const style_node = styleSheet.ownerNode
|
||||
let style_id = ''
|
||||
|
||||
if (style_node && 'id' in style_node) {
|
||||
style_id = style_node.id
|
||||
}
|
||||
|
||||
if (style_id) {
|
||||
style.setAttribute('id', style_id)
|
||||
}
|
||||
style.textContent = cssRules
|
||||
pip.document.head.appendChild(style)
|
||||
} catch (e) {
|
||||
const link = document.createElement('link')
|
||||
if (styleSheet.href == null) {
|
||||
return
|
||||
}
|
||||
|
||||
link.rel = 'stylesheet'
|
||||
link.type = styleSheet.type
|
||||
link.media = styleSheet.media.toString()
|
||||
link.href = styleSheet.href
|
||||
pip.document.head.appendChild(link)
|
||||
}
|
||||
})
|
||||
delegateEvents(
|
||||
[
|
||||
'focusin',
|
||||
'focusout',
|
||||
'pointermove',
|
||||
'keydown',
|
||||
'pointerdown',
|
||||
'pointerup',
|
||||
'click',
|
||||
'mousedown',
|
||||
'input',
|
||||
],
|
||||
pip.document,
|
||||
)
|
||||
props.setLocalStore('pip_open', 'true')
|
||||
setPipWindow(pip)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const pip_open = (props.localStore.pip_open ?? 'false') as 'true' | 'false'
|
||||
if (pip_open === 'true' && !props.disabled) {
|
||||
try {
|
||||
requestPipWindow(
|
||||
Number(window.innerWidth),
|
||||
Number(props.localStore.height || PIP_DEFAULT_HEIGHT),
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof PipOpenError) {
|
||||
console.error(error.message)
|
||||
props.setLocalStore('pip_open', 'false')
|
||||
props.setLocalStore('open', 'false')
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
// Setup mutation observer for goober styles with id `_goober
|
||||
const gooberStyles = (
|
||||
useQueryDevtoolsContext().shadowDOMTarget || document
|
||||
).querySelector('#_goober')
|
||||
const w = pipWindow()
|
||||
if (gooberStyles && w) {
|
||||
const observer = new MutationObserver(() => {
|
||||
const pip_style = (
|
||||
useQueryDevtoolsContext().shadowDOMTarget || w.document
|
||||
).querySelector('#_goober')
|
||||
if (pip_style) {
|
||||
pip_style.textContent = gooberStyles.textContent
|
||||
}
|
||||
})
|
||||
observer.observe(gooberStyles, {
|
||||
childList: true, // observe direct children
|
||||
subtree: true, // and lower descendants too
|
||||
characterDataOldValue: true, // pass old data to callback
|
||||
})
|
||||
onCleanup(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const value = createMemo(() => ({
|
||||
pipWindow: pipWindow(),
|
||||
requestPipWindow,
|
||||
closePipWindow,
|
||||
disabled: props.disabled ?? false,
|
||||
}))
|
||||
|
||||
return (
|
||||
<PiPContext.Provider value={value}>{props.children}</PiPContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const usePiPWindow = () => {
|
||||
const context = createMemo(() => {
|
||||
const ctx = useContext(PiPContext)
|
||||
if (!ctx) {
|
||||
throw new Error('usePiPWindow must be used within a PiPProvider')
|
||||
}
|
||||
return ctx()
|
||||
})
|
||||
return context
|
||||
}
|
||||
Generated
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
import { createContext, useContext } from 'solid-js'
|
||||
import type { Query, QueryClient, onlineManager } from '@tanstack/query-core'
|
||||
|
||||
type XPosition = 'left' | 'right'
|
||||
type YPosition = 'top' | 'bottom'
|
||||
export type DevtoolsPosition = XPosition | YPosition
|
||||
export type DevtoolsButtonPosition = `${YPosition}-${XPosition}` | 'relative'
|
||||
export type Theme = 'dark' | 'light' | 'system'
|
||||
|
||||
export interface DevtoolsErrorType {
|
||||
/**
|
||||
* The name of the error.
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* How the error is initialized.
|
||||
*/
|
||||
initializer: (query: Query) => Error
|
||||
}
|
||||
|
||||
export interface QueryDevtoolsProps {
|
||||
readonly client: QueryClient
|
||||
queryFlavor: string
|
||||
version: string
|
||||
onlineManager: typeof onlineManager
|
||||
|
||||
buttonPosition?: DevtoolsButtonPosition
|
||||
position?: DevtoolsPosition
|
||||
initialIsOpen?: boolean
|
||||
errorTypes?: Array<DevtoolsErrorType>
|
||||
shadowDOMTarget?: ShadowRoot
|
||||
onClose?: () => unknown
|
||||
hideDisabledQueries?: boolean
|
||||
theme?: Theme
|
||||
}
|
||||
|
||||
export const QueryDevtoolsContext = createContext<QueryDevtoolsProps>({
|
||||
client: undefined as unknown as QueryClient,
|
||||
onlineManager: undefined as unknown as typeof onlineManager,
|
||||
queryFlavor: '',
|
||||
version: '',
|
||||
shadowDOMTarget: undefined,
|
||||
})
|
||||
|
||||
export function useQueryDevtoolsContext() {
|
||||
return useContext(QueryDevtoolsContext)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { createContext, useContext } from 'solid-js'
|
||||
import type { Accessor } from 'solid-js'
|
||||
|
||||
export const ThemeContext = createContext<Accessor<'light' | 'dark'>>(
|
||||
() => 'dark' as const,
|
||||
)
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext)
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export * from './PiPContext'
|
||||
export * from './QueryDevtoolsContext'
|
||||
export * from './ThemeContext'
|
||||
+1385
File diff suppressed because it is too large
Load Diff
+14
@@ -0,0 +1,14 @@
|
||||
export type {
|
||||
DevtoolsButtonPosition,
|
||||
DevtoolsErrorType,
|
||||
DevtoolsPosition,
|
||||
Theme,
|
||||
} from './contexts'
|
||||
export {
|
||||
TanstackQueryDevtools,
|
||||
type TanstackQueryDevtoolsConfig,
|
||||
} from './TanstackQueryDevtools'
|
||||
export {
|
||||
TanstackQueryDevtoolsPanel,
|
||||
type TanstackQueryDevtoolsPanelConfig,
|
||||
} from './TanstackQueryDevtoolsPanel'
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
export const tokens = {
|
||||
colors: {
|
||||
inherit: 'inherit',
|
||||
current: 'currentColor',
|
||||
transparent: 'transparent',
|
||||
black: '#000000',
|
||||
white: '#ffffff',
|
||||
neutral: {
|
||||
50: '#f9fafb',
|
||||
100: '#f2f4f7',
|
||||
200: '#eaecf0',
|
||||
300: '#d0d5dd',
|
||||
400: '#98a2b3',
|
||||
500: '#667085',
|
||||
600: '#475467',
|
||||
700: '#344054',
|
||||
800: '#1d2939',
|
||||
900: '#101828',
|
||||
},
|
||||
darkGray: {
|
||||
50: '#525c7a',
|
||||
100: '#49536e',
|
||||
200: '#414962',
|
||||
300: '#394056',
|
||||
400: '#313749',
|
||||
500: '#292e3d',
|
||||
600: '#212530',
|
||||
700: '#191c24',
|
||||
800: '#111318',
|
||||
900: '#0b0d10',
|
||||
},
|
||||
gray: {
|
||||
50: '#f9fafb',
|
||||
100: '#f2f4f7',
|
||||
200: '#eaecf0',
|
||||
300: '#d0d5dd',
|
||||
400: '#98a2b3',
|
||||
500: '#667085',
|
||||
600: '#475467',
|
||||
700: '#344054',
|
||||
800: '#1d2939',
|
||||
900: '#101828',
|
||||
},
|
||||
blue: {
|
||||
25: '#F5FAFF',
|
||||
50: '#EFF8FF',
|
||||
100: '#D1E9FF',
|
||||
200: '#B2DDFF',
|
||||
300: '#84CAFF',
|
||||
400: '#53B1FD',
|
||||
500: '#2E90FA',
|
||||
600: '#1570EF',
|
||||
700: '#175CD3',
|
||||
800: '#1849A9',
|
||||
900: '#194185',
|
||||
},
|
||||
green: {
|
||||
25: '#F6FEF9',
|
||||
50: '#ECFDF3',
|
||||
100: '#D1FADF',
|
||||
200: '#A6F4C5',
|
||||
300: '#6CE9A6',
|
||||
400: '#32D583',
|
||||
500: '#12B76A',
|
||||
600: '#039855',
|
||||
700: '#027A48',
|
||||
800: '#05603A',
|
||||
900: '#054F31',
|
||||
},
|
||||
red: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
950: '#450a0a',
|
||||
},
|
||||
yellow: {
|
||||
25: '#FFFCF5',
|
||||
50: '#FFFAEB',
|
||||
100: '#FEF0C7',
|
||||
200: '#FEDF89',
|
||||
300: '#FEC84B',
|
||||
400: '#FDB022',
|
||||
500: '#F79009',
|
||||
600: '#DC6803',
|
||||
700: '#B54708',
|
||||
800: '#93370D',
|
||||
900: '#7A2E0E',
|
||||
},
|
||||
purple: {
|
||||
25: '#FAFAFF',
|
||||
50: '#F4F3FF',
|
||||
100: '#EBE9FE',
|
||||
200: '#D9D6FE',
|
||||
300: '#BDB4FE',
|
||||
400: '#9B8AFB',
|
||||
500: '#7A5AF8',
|
||||
600: '#6938EF',
|
||||
700: '#5925DC',
|
||||
800: '#4A1FB8',
|
||||
900: '#3E1C96',
|
||||
},
|
||||
teal: {
|
||||
25: '#F6FEFC',
|
||||
50: '#F0FDF9',
|
||||
100: '#CCFBEF',
|
||||
200: '#99F6E0',
|
||||
300: '#5FE9D0',
|
||||
400: '#2ED3B7',
|
||||
500: '#15B79E',
|
||||
600: '#0E9384',
|
||||
700: '#107569',
|
||||
800: '#125D56',
|
||||
900: '#134E48',
|
||||
},
|
||||
pink: {
|
||||
25: '#fdf2f8',
|
||||
50: '#fce7f3',
|
||||
100: '#fbcfe8',
|
||||
200: '#f9a8d4',
|
||||
300: '#f472b6',
|
||||
400: '#ec4899',
|
||||
500: '#db2777',
|
||||
600: '#be185d',
|
||||
700: '#9d174d',
|
||||
800: '#831843',
|
||||
900: '#500724',
|
||||
},
|
||||
cyan: {
|
||||
25: '#ecfeff',
|
||||
50: '#cffafe',
|
||||
100: '#a5f3fc',
|
||||
200: '#67e8f9',
|
||||
300: '#22d3ee',
|
||||
400: '#06b6d4',
|
||||
500: '#0891b2',
|
||||
600: '#0e7490',
|
||||
700: '#155e75',
|
||||
800: '#164e63',
|
||||
900: '#083344',
|
||||
},
|
||||
},
|
||||
alpha: {
|
||||
100: 'ff',
|
||||
90: 'e5',
|
||||
80: 'cc',
|
||||
70: 'b3',
|
||||
60: '99',
|
||||
50: '80',
|
||||
40: '66',
|
||||
30: '4d',
|
||||
20: '33',
|
||||
10: '1a',
|
||||
0: '00',
|
||||
},
|
||||
font: {
|
||||
size: {
|
||||
'2xs': 'calc(var(--tsqd-font-size) * 0.625)',
|
||||
xs: 'calc(var(--tsqd-font-size) * 0.75)',
|
||||
sm: 'calc(var(--tsqd-font-size) * 0.875)',
|
||||
md: 'var(--tsqd-font-size)',
|
||||
lg: 'calc(var(--tsqd-font-size) * 1.125)',
|
||||
xl: 'calc(var(--tsqd-font-size) * 1.25)',
|
||||
'2xl': 'calc(var(--tsqd-font-size) * 1.5)',
|
||||
'3xl': 'calc(var(--tsqd-font-size) * 1.875)',
|
||||
'4xl': 'calc(var(--tsqd-font-size) * 2.25)',
|
||||
'5xl': 'calc(var(--tsqd-font-size) * 3)',
|
||||
'6xl': 'calc(var(--tsqd-font-size) * 3.75)',
|
||||
'7xl': 'calc(var(--tsqd-font-size) * 4.5)',
|
||||
'8xl': 'calc(var(--tsqd-font-size) * 6)',
|
||||
'9xl': 'calc(var(--tsqd-font-size) * 8)',
|
||||
},
|
||||
lineHeight: {
|
||||
xs: 'calc(var(--tsqd-font-size) * 1)',
|
||||
sm: 'calc(var(--tsqd-font-size) * 1.25)',
|
||||
md: 'calc(var(--tsqd-font-size) * 1.5)',
|
||||
lg: 'calc(var(--tsqd-font-size) * 1.75)',
|
||||
xl: 'calc(var(--tsqd-font-size) * 2)',
|
||||
'2xl': 'calc(var(--tsqd-font-size) * 2.25)',
|
||||
'3xl': 'calc(var(--tsqd-font-size) * 2.5)',
|
||||
'4xl': 'calc(var(--tsqd-font-size) * 2.75)',
|
||||
'5xl': 'calc(var(--tsqd-font-size) * 3)',
|
||||
'6xl': 'calc(var(--tsqd-font-size) * 3.25)',
|
||||
'7xl': 'calc(var(--tsqd-font-size) * 3.5)',
|
||||
'8xl': 'calc(var(--tsqd-font-size) * 3.75)',
|
||||
'9xl': 'calc(var(--tsqd-font-size) * 4)',
|
||||
},
|
||||
weight: {
|
||||
thin: '100',
|
||||
extralight: '200',
|
||||
light: '300',
|
||||
normal: '400',
|
||||
medium: '500',
|
||||
semibold: '600',
|
||||
bold: '700',
|
||||
extrabold: '800',
|
||||
black: '900',
|
||||
},
|
||||
},
|
||||
breakpoints: {
|
||||
xs: '320px',
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1536px',
|
||||
},
|
||||
border: {
|
||||
radius: {
|
||||
none: '0px',
|
||||
xs: 'calc(var(--tsqd-font-size) * 0.125)',
|
||||
sm: 'calc(var(--tsqd-font-size) * 0.25)',
|
||||
md: 'calc(var(--tsqd-font-size) * 0.375)',
|
||||
lg: 'calc(var(--tsqd-font-size) * 0.5)',
|
||||
xl: 'calc(var(--tsqd-font-size) * 0.75)',
|
||||
'2xl': 'calc(var(--tsqd-font-size) * 1)',
|
||||
'3xl': 'calc(var(--tsqd-font-size) * 1.5)',
|
||||
full: '9999px',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
0: '0px',
|
||||
0.25: 'calc(var(--tsqd-font-size) * 0.0625)',
|
||||
0.5: 'calc(var(--tsqd-font-size) * 0.125)',
|
||||
1: 'calc(var(--tsqd-font-size) * 0.25)',
|
||||
1.5: 'calc(var(--tsqd-font-size) * 0.375)',
|
||||
2: 'calc(var(--tsqd-font-size) * 0.5)',
|
||||
2.5: 'calc(var(--tsqd-font-size) * 0.625)',
|
||||
3: 'calc(var(--tsqd-font-size) * 0.75)',
|
||||
3.5: 'calc(var(--tsqd-font-size) * 0.875)',
|
||||
4: 'calc(var(--tsqd-font-size) * 1)',
|
||||
4.5: 'calc(var(--tsqd-font-size) * 1.125)',
|
||||
5: 'calc(var(--tsqd-font-size) * 1.25)',
|
||||
5.5: 'calc(var(--tsqd-font-size) * 1.375)',
|
||||
6: 'calc(var(--tsqd-font-size) * 1.5)',
|
||||
6.5: 'calc(var(--tsqd-font-size) * 1.625)',
|
||||
7: 'calc(var(--tsqd-font-size) * 1.75)',
|
||||
8: 'calc(var(--tsqd-font-size) * 2)',
|
||||
9: 'calc(var(--tsqd-font-size) * 2.25)',
|
||||
10: 'calc(var(--tsqd-font-size) * 2.5)',
|
||||
11: 'calc(var(--tsqd-font-size) * 2.75)',
|
||||
12: 'calc(var(--tsqd-font-size) * 3)',
|
||||
14: 'calc(var(--tsqd-font-size) * 3.5)',
|
||||
16: 'calc(var(--tsqd-font-size) * 4)',
|
||||
20: 'calc(var(--tsqd-font-size) * 5)',
|
||||
24: 'calc(var(--tsqd-font-size) * 6)',
|
||||
28: 'calc(var(--tsqd-font-size) * 7)',
|
||||
32: 'calc(var(--tsqd-font-size) * 8)',
|
||||
36: 'calc(var(--tsqd-font-size) * 9)',
|
||||
40: 'calc(var(--tsqd-font-size) * 10)',
|
||||
44: 'calc(var(--tsqd-font-size) * 11)',
|
||||
48: 'calc(var(--tsqd-font-size) * 12)',
|
||||
52: 'calc(var(--tsqd-font-size) * 13)',
|
||||
56: 'calc(var(--tsqd-font-size) * 14)',
|
||||
60: 'calc(var(--tsqd-font-size) * 15)',
|
||||
64: 'calc(var(--tsqd-font-size) * 16)',
|
||||
72: 'calc(var(--tsqd-font-size) * 18)',
|
||||
80: 'calc(var(--tsqd-font-size) * 20)',
|
||||
96: 'calc(var(--tsqd-font-size) * 24)',
|
||||
},
|
||||
shadow: {
|
||||
xs: (_: string = 'rgb(0 0 0 / 0.1)') =>
|
||||
`0 1px 2px 0 rgb(0 0 0 / 0.05)` as const,
|
||||
sm: (color: string = 'rgb(0 0 0 / 0.1)') =>
|
||||
`0 1px 3px 0 ${color}, 0 1px 2px -1px ${color}` as const,
|
||||
md: (color: string = 'rgb(0 0 0 / 0.1)') =>
|
||||
`0 4px 6px -1px ${color}, 0 2px 4px -2px ${color}` as const,
|
||||
lg: (color: string = 'rgb(0 0 0 / 0.1)') =>
|
||||
`0 10px 15px -3px ${color}, 0 4px 6px -4px ${color}` as const,
|
||||
xl: (color: string = 'rgb(0 0 0 / 0.1)') =>
|
||||
`0 20px 25px -5px ${color}, 0 8px 10px -6px ${color}` as const,
|
||||
'2xl': (color: string = 'rgb(0 0 0 / 0.25)') =>
|
||||
`0 25px 50px -12px ${color}` as const,
|
||||
inner: (color: string = 'rgb(0 0 0 / 0.05)') =>
|
||||
`inset 0 2px 4px 0 ${color}` as const,
|
||||
none: () => `none` as const,
|
||||
},
|
||||
zIndices: {
|
||||
hide: -1,
|
||||
auto: 'auto',
|
||||
base: 0,
|
||||
docked: 10,
|
||||
dropdown: 1000,
|
||||
sticky: 1100,
|
||||
banner: 1200,
|
||||
overlay: 1300,
|
||||
modal: 1400,
|
||||
popover: 1500,
|
||||
skipLink: 1600,
|
||||
toast: 1700,
|
||||
tooltip: 1800,
|
||||
},
|
||||
} as const
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
import { serialize } from 'superjson'
|
||||
import { createSignal, onCleanup, onMount } from 'solid-js'
|
||||
import type { Mutation, Query } from '@tanstack/query-core'
|
||||
import type { DevtoolsPosition } from './contexts'
|
||||
|
||||
export function getQueryStatusLabel(query: Query) {
|
||||
return query.state.fetchStatus === 'fetching'
|
||||
? 'fetching'
|
||||
: !query.getObserversCount()
|
||||
? 'inactive'
|
||||
: query.state.fetchStatus === 'paused'
|
||||
? 'paused'
|
||||
: query.isStale()
|
||||
? 'stale'
|
||||
: 'fresh'
|
||||
}
|
||||
|
||||
type QueryStatusLabel = 'fresh' | 'stale' | 'paused' | 'inactive' | 'fetching'
|
||||
|
||||
export function getSidedProp<T extends string>(
|
||||
prop: T,
|
||||
side: DevtoolsPosition,
|
||||
) {
|
||||
return `${prop}${
|
||||
side.charAt(0).toUpperCase() + side.slice(1)
|
||||
}` as `${T}${Capitalize<DevtoolsPosition>}`
|
||||
}
|
||||
|
||||
export function getQueryStatusColor({
|
||||
queryState,
|
||||
observerCount,
|
||||
isStale,
|
||||
}: {
|
||||
queryState: Query['state']
|
||||
observerCount: number
|
||||
isStale: boolean
|
||||
}) {
|
||||
return queryState.fetchStatus === 'fetching'
|
||||
? 'blue'
|
||||
: !observerCount
|
||||
? 'gray'
|
||||
: queryState.fetchStatus === 'paused'
|
||||
? 'purple'
|
||||
: isStale
|
||||
? 'yellow'
|
||||
: 'green'
|
||||
}
|
||||
|
||||
export function getMutationStatusColor({
|
||||
status,
|
||||
isPaused,
|
||||
}: {
|
||||
status: Mutation['state']['status']
|
||||
isPaused: boolean
|
||||
}) {
|
||||
return isPaused
|
||||
? 'purple'
|
||||
: status === 'error'
|
||||
? 'red'
|
||||
: status === 'pending'
|
||||
? 'yellow'
|
||||
: status === 'success'
|
||||
? 'green'
|
||||
: 'gray'
|
||||
}
|
||||
|
||||
export function getQueryStatusColorByLabel(label: QueryStatusLabel) {
|
||||
return label === 'fresh'
|
||||
? 'green'
|
||||
: label === 'stale'
|
||||
? 'yellow'
|
||||
: label === 'paused'
|
||||
? 'purple'
|
||||
: label === 'inactive'
|
||||
? 'gray'
|
||||
: 'blue'
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a string regardless the type of the data
|
||||
* @param {unknown} value Value to be stringified
|
||||
* @param {boolean} beautify Formats json to multiline
|
||||
*/
|
||||
export const displayValue = (value: unknown, beautify: boolean = false) => {
|
||||
const { json } = serialize(value)
|
||||
|
||||
return JSON.stringify(json, null, beautify ? 2 : undefined)
|
||||
}
|
||||
|
||||
// Sorting functions
|
||||
type SortFn = (a: Query, b: Query) => number
|
||||
|
||||
const getStatusRank = (q: Query) =>
|
||||
q.state.fetchStatus !== 'idle'
|
||||
? 0
|
||||
: !q.getObserversCount()
|
||||
? 3
|
||||
: q.isStale()
|
||||
? 2
|
||||
: 1
|
||||
|
||||
const queryHashSort: SortFn = (a, b) => a.queryHash.localeCompare(b.queryHash)
|
||||
|
||||
const dateSort: SortFn = (a, b) =>
|
||||
a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1
|
||||
|
||||
const statusAndDateSort: SortFn = (a, b) => {
|
||||
if (getStatusRank(a) === getStatusRank(b)) {
|
||||
return dateSort(a, b)
|
||||
}
|
||||
|
||||
return getStatusRank(a) > getStatusRank(b) ? 1 : -1
|
||||
}
|
||||
|
||||
export const sortFns: Record<string, SortFn> = {
|
||||
status: statusAndDateSort,
|
||||
'query hash': queryHashSort,
|
||||
'last updated': dateSort,
|
||||
}
|
||||
|
||||
type MutationSortFn = (a: Mutation, b: Mutation) => number
|
||||
|
||||
const getMutationStatusRank = (m: Mutation) =>
|
||||
m.state.isPaused
|
||||
? 0
|
||||
: m.state.status === 'error'
|
||||
? 2
|
||||
: m.state.status === 'pending'
|
||||
? 1
|
||||
: 3
|
||||
|
||||
const mutationDateSort: MutationSortFn = (a, b) =>
|
||||
a.state.submittedAt < b.state.submittedAt ? 1 : -1
|
||||
|
||||
const mutationStatusSort: MutationSortFn = (a, b) => {
|
||||
if (getMutationStatusRank(a) === getMutationStatusRank(b)) {
|
||||
return mutationDateSort(a, b)
|
||||
}
|
||||
|
||||
return getMutationStatusRank(a) > getMutationStatusRank(b) ? 1 : -1
|
||||
}
|
||||
|
||||
export const mutationSortFns: Record<string, MutationSortFn> = {
|
||||
status: mutationStatusSort,
|
||||
'last updated': mutationDateSort,
|
||||
}
|
||||
|
||||
export const convertRemToPixels = (rem: number) => {
|
||||
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
}
|
||||
|
||||
export const getPreferredColorScheme = () => {
|
||||
const [colorScheme, setColorScheme] = createSignal<'light' | 'dark'>('dark')
|
||||
|
||||
onMount(() => {
|
||||
const query = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
setColorScheme(query.matches ? 'dark' : 'light')
|
||||
const listener = (e: MediaQueryListEvent) => {
|
||||
setColorScheme(e.matches ? 'dark' : 'light')
|
||||
}
|
||||
query.addEventListener('change', listener)
|
||||
onCleanup(() => query.removeEventListener('change', listener))
|
||||
})
|
||||
|
||||
return colorScheme
|
||||
}
|
||||
|
||||
/**
|
||||
* updates nested data by path
|
||||
*
|
||||
* @param {unknown} oldData Data to be updated
|
||||
* @param {Array<string>} updatePath Path to the data to be updated
|
||||
* @param {unknown} value New value
|
||||
*/
|
||||
export const updateNestedDataByPath = (
|
||||
oldData: unknown,
|
||||
updatePath: Array<string>,
|
||||
value: unknown,
|
||||
): any => {
|
||||
if (updatePath.length === 0) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (oldData instanceof Map) {
|
||||
const newData = new Map(oldData)
|
||||
|
||||
if (updatePath.length === 1) {
|
||||
newData.set(updatePath[0], value)
|
||||
return newData
|
||||
}
|
||||
|
||||
const [head, ...tail] = updatePath
|
||||
newData.set(head, updateNestedDataByPath(newData.get(head), tail, value))
|
||||
return newData
|
||||
}
|
||||
|
||||
if (oldData instanceof Set) {
|
||||
const setAsArray = updateNestedDataByPath(
|
||||
Array.from(oldData),
|
||||
updatePath,
|
||||
value,
|
||||
)
|
||||
|
||||
return new Set(setAsArray)
|
||||
}
|
||||
|
||||
if (Array.isArray(oldData)) {
|
||||
const newData = [...oldData]
|
||||
|
||||
if (updatePath.length === 1) {
|
||||
// @ts-expect-error
|
||||
newData[updatePath[0]] = value
|
||||
return newData
|
||||
}
|
||||
|
||||
const [head, ...tail] = updatePath
|
||||
// @ts-expect-error
|
||||
newData[head] = updateNestedDataByPath(newData[head], tail, value)
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
if (oldData instanceof Object) {
|
||||
const newData = { ...oldData }
|
||||
|
||||
if (updatePath.length === 1) {
|
||||
// @ts-expect-error
|
||||
newData[updatePath[0]] = value
|
||||
return newData
|
||||
}
|
||||
|
||||
const [head, ...tail] = updatePath
|
||||
// @ts-expect-error
|
||||
newData[head] = updateNestedDataByPath(newData[head], tail, value)
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
return oldData
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes nested data by path
|
||||
*
|
||||
* @param {unknown} oldData Data to be updated
|
||||
* @param {Array<string>} deletePath Path to the data to be deleted
|
||||
* @returns newData without the deleted items by path
|
||||
*/
|
||||
export const deleteNestedDataByPath = (
|
||||
oldData: unknown,
|
||||
deletePath: Array<string>,
|
||||
): any => {
|
||||
if (oldData instanceof Map) {
|
||||
const newData = new Map(oldData)
|
||||
|
||||
if (deletePath.length === 1) {
|
||||
newData.delete(deletePath[0])
|
||||
return newData
|
||||
}
|
||||
|
||||
const [head, ...tail] = deletePath
|
||||
newData.set(head, deleteNestedDataByPath(newData.get(head), tail))
|
||||
return newData
|
||||
}
|
||||
|
||||
if (oldData instanceof Set) {
|
||||
const setAsArray = deleteNestedDataByPath(Array.from(oldData), deletePath)
|
||||
return new Set(setAsArray)
|
||||
}
|
||||
|
||||
if (Array.isArray(oldData)) {
|
||||
const newData = [...oldData]
|
||||
|
||||
if (deletePath.length === 1) {
|
||||
return newData.filter((_, idx) => idx.toString() !== deletePath[0])
|
||||
}
|
||||
|
||||
const [head, ...tail] = deletePath
|
||||
|
||||
// @ts-expect-error
|
||||
newData[head] = deleteNestedDataByPath(newData[head], tail)
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
if (oldData instanceof Object) {
|
||||
const newData = { ...oldData }
|
||||
|
||||
if (deletePath.length === 1) {
|
||||
// @ts-expect-error
|
||||
delete newData[deletePath[0]]
|
||||
return newData
|
||||
}
|
||||
|
||||
const [head, ...tail] = deletePath
|
||||
// @ts-expect-error
|
||||
newData[head] = deleteNestedDataByPath(newData[head], tail)
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
return oldData
|
||||
}
|
||||
|
||||
// Sets up the goober stylesheet
|
||||
// Adds a nonce to the style tag if needed
|
||||
export const setupStyleSheet = (nonce?: string, target?: ShadowRoot) => {
|
||||
if (!nonce) return
|
||||
const styleExists =
|
||||
document.querySelector('#_goober') || target?.querySelector('#_goober')
|
||||
|
||||
if (styleExists) return
|
||||
const styleTag = document.createElement('style')
|
||||
const textNode = document.createTextNode('')
|
||||
styleTag.appendChild(textNode)
|
||||
styleTag.id = '_goober'
|
||||
styleTag.setAttribute('nonce', nonce)
|
||||
if (target) {
|
||||
target.appendChild(styleTag)
|
||||
} else {
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user