mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 22:10:15 +03:00
Fixes for GUI2 (#8310)
Addresses several issues found during book club. - Add placeholder icons for visualizations (see screenshot) - Also add the corresponding export to visualizations, and relevant support in the visualization store and visualization metadata DB. - Show icon both on the visualization selector button, and in each visualization selector entry - Adjust gaps between visualization selector entries - Port table viz fixes (#8102) to Vue - Fix height of table rows to avoid cutting off descenders on letters like `y` and `g` - Make the space and enter keys work on visualization toolbar buttons, if they are selected - `.blur()` code editor when clicking outside of it - And similarly `.blur()` visualization selector - Move selection brush on scroll by scaling the delta of `scrollTop` and `scrollLeft` - Note that mouse position is technically still incorrect when scrolling past the end. I think this is fine - the new behavior is less broken, plus I am not aware of any way to fix this. # Important Notes None
This commit is contained in:
parent
1c936cc69f
commit
062992bb8b
@ -33,6 +33,7 @@
|
|||||||
"@ag-grid-community/core": "^30.2.0",
|
"@ag-grid-community/core": "^30.2.0",
|
||||||
"@ag-grid-community/styles": "^30.2.0",
|
"@ag-grid-community/styles": "^30.2.0",
|
||||||
"@ag-grid-enterprise/core": "^30.2.0",
|
"@ag-grid-enterprise/core": "^30.2.0",
|
||||||
|
"@ag-grid-enterprise/range-selection": "^30.2.1",
|
||||||
"@babel/parser": "^7.22.16",
|
"@babel/parser": "^7.22.16",
|
||||||
"@fast-check/vitest": "^0.0.8",
|
"@fast-check/vitest": "^0.0.8",
|
||||||
"@lezer/common": "^1.1.0",
|
"@lezer/common": "^1.1.0",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Please run `npm run generate` whenever this file is edited. -->
|
<!-- Please run `npm run generate-metadata` whenever this file is edited. -->
|
||||||
<g id="add_column" fill="none">
|
<g id="add_column" fill="none">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g id="icon/add_column">
|
<g id="icon/add_column">
|
||||||
@ -904,6 +904,22 @@
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</g>
|
</g>
|
||||||
|
<g id="table" fill="none">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="4" y="4.5" width="4" height="3" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="4" y="8.5" width="4" height="3" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="4" y="12.5" width="4" height="3" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="9" y="4.5" width="3" height="3" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="9" y="8.5" width="3" height="3" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="9" y="12.5" width="3" height="3" fill="black" fill-opacity="0.6" />
|
||||||
|
<path d="M0 4.5H3V15.5H2C0.895431 15.5 0 14.6046 0 13.5V4.5Z" fill="black" fill-opacity="0.6" />
|
||||||
|
<path d="M0 2.5C0 1.39543 0.895431 0.5 2 0.5H3V3.5H0V2.5Z" fill="black" fill-opacity="0.6" />
|
||||||
|
<path d="M4 0.5H14C15.1046 0.5 16 1.39543 16 2.5V3.5H4V0.5Z" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="13" y="4.5" width="3" height="3" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="13" y="8.5" width="3" height="3" fill="black" fill-opacity="0.6" />
|
||||||
|
<path d="M13 12.5H16V13.5C16 14.6046 15.1046 15.5 14 15.5H13V12.5Z" fill="black" fill-opacity="0.6" />
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
<g id="table_edit" fill="none">
|
<g id="table_edit" fill="none">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g id="icon/table_edit">
|
<g id="icon/table_edit">
|
||||||
@ -1271,5 +1287,50 @@
|
|||||||
fill="black" fill-opacity="0.6" />
|
fill="black" fill-opacity="0.6" />
|
||||||
</svg>
|
</svg>
|
||||||
</g>
|
</g>
|
||||||
<!-- Please run `npm run generate` whenever this file is edited. -->
|
<g id="braces" fill="none">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 1C4.5 1 3.5 2 3.5 3V6C3.5 6.5 3.49999 8 1 8C3.5 8 3.5 9.5 3.5 10V13C3.5 14 4.5 15 6 15" stroke="black" stroke-width="2" stroke-opacity="0.6" />
|
||||||
|
<path d="M10 1C11.5 1 12.5 2 12.5 3V6C12.5 6.5 12.5 8 15 8C12.5 8 12.5 9.5 12.5 10V13C12.5 14 11.5 15 10 15" stroke="black" stroke-width="2" stroke-opacity="0.6" />
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
<g id="columns_increasing" fill="none">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="1" y="10" width="4" height="6" rx="1" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="6" y="5" width="4" height="11" rx="1" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="11" width="4" height="16" rx="1" fill="black" fill-opacity="0.6" />
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
<g id="exclamation" fill="none">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="6.5" y="1" width="3" height="10" rx="1" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="6.5" y="12" width="3" height="3" rx="1" fill="black" fill-opacity="0.6" />
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
<g id="points" fill="none">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="5" y="2" width="3" height="3" rx="1.5" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="1" y="11" width="3" height="3" rx="1.5" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="12" y="4" width="3" height="3" rx="1.5" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="10" y="10" width="3" height="3" rx="1.5" fill="black" fill-opacity="0.6" />
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
<g id="heatmap" fill="none">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="5.75" y="0.75" width="4.5" height="4.5" rx="1" fill="black" fill-opacity="0.3" />
|
||||||
|
<rect x="0.75" y="10.75" width="4.5" height="4.5" rx="1" fill="black" fill-opacity="0.45" />
|
||||||
|
<rect x="10.75" y="5.75" width="4.5" height="4.5" rx="1" fill="black" fill-opacity="0.45" />
|
||||||
|
<rect x="5.75" y="10.75" width="4.5" height="4.5" rx="1" fill="black" fill-opacity="0.6" />
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
<g id="image" fill="none">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 3C1 2.44772 1.44772 2 2 2H14C14.5523 2 15 2.44772 15 3V4H1V3Z" fill="black" fill-opacity="0.6" />
|
||||||
|
<path d="M1 12H15V13C15 13.5523 14.5523 14 14 14H2C1.44772 14 1 13.5523 1 13V12Z" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="1" y="4" width="2" height="8" fill="black" fill-opacity="0.6" />
|
||||||
|
<rect x="13" y="4" width="2" height="8" fill="black" fill-opacity="0.6" />
|
||||||
|
<circle cx="10.5" cy="6.5" r="1.5" fill="black" fill-opacity="0.6" />
|
||||||
|
<path d="M4 11.0237V8.02371C6 5.5237 7 6 8.5 7.50003L12 11.0237L4 11.0237Z" fill="black" fill-opacity="0.6" />
|
||||||
|
</svg>
|
||||||
|
</g>
|
||||||
|
<!-- Please run `npm run generate-metadata` whenever this file is edited. -->
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 106 KiB |
@ -2,6 +2,7 @@
|
|||||||
import { useGraphStore } from '@/stores/graph'
|
import { useGraphStore } from '@/stores/graph'
|
||||||
import { useProjectStore } from '@/stores/project'
|
import { useProjectStore } from '@/stores/project'
|
||||||
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||||
|
import { useAutoBlur } from '@/util/autoBlur'
|
||||||
import type { Highlighter } from '@/util/codemirror'
|
import type { Highlighter } from '@/util/codemirror'
|
||||||
import { usePointer } from '@/util/events'
|
import { usePointer } from '@/util/events'
|
||||||
import { useLocalStorage } from '@vueuse/core'
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
@ -30,6 +31,7 @@ const projectStore = useProjectStore()
|
|||||||
const graphStore = useGraphStore()
|
const graphStore = useGraphStore()
|
||||||
const suggestionDbStore = useSuggestionDbStore()
|
const suggestionDbStore = useSuggestionDbStore()
|
||||||
const rootElement = ref<HTMLElement>()
|
const rootElement = ref<HTMLElement>()
|
||||||
|
useAutoBlur(rootElement)
|
||||||
|
|
||||||
// == CodeMirror editor setup ==
|
// == CodeMirror editor setup ==
|
||||||
|
|
||||||
|
@ -9,7 +9,9 @@ import {
|
|||||||
useVisualizationStore,
|
useVisualizationStore,
|
||||||
type Visualization,
|
type Visualization,
|
||||||
} from '@/stores/visualization'
|
} from '@/stores/visualization'
|
||||||
|
import type { URLString } from '@/stores/visualization/compilerMessaging'
|
||||||
import { toError } from '@/util/error'
|
import { toError } from '@/util/error'
|
||||||
|
import type { Icon } from '@/util/iconName'
|
||||||
import type { Opt } from '@/util/opt'
|
import type { Opt } from '@/util/opt'
|
||||||
import type { Vec2 } from '@/util/vec2'
|
import type { Vec2 } from '@/util/vec2'
|
||||||
import type { ExprId, VisualizationIdentifier } from 'shared/yjsModel'
|
import type { ExprId, VisualizationIdentifier } from 'shared/yjsModel'
|
||||||
@ -35,6 +37,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const visualization = shallowRef<Visualization>()
|
const visualization = shallowRef<Visualization>()
|
||||||
|
const icon = ref<Icon | URLString>()
|
||||||
|
|
||||||
onErrorCaptured((vueError) => {
|
onErrorCaptured((vueError) => {
|
||||||
error.value = vueError
|
error.value = vueError
|
||||||
@ -76,6 +79,7 @@ watch(
|
|||||||
watchEffect(async () => {
|
watchEffect(async () => {
|
||||||
if (props.currentType == null) return
|
if (props.currentType == null) return
|
||||||
visualization.value = undefined
|
visualization.value = undefined
|
||||||
|
icon.value = undefined
|
||||||
try {
|
try {
|
||||||
const module = await visualizationStore.get(props.currentType).value
|
const module = await visualizationStore.get(props.currentType).value
|
||||||
if (module) {
|
if (module) {
|
||||||
@ -85,6 +89,7 @@ watchEffect(async () => {
|
|||||||
switchToDefaultPreprocessor()
|
switchToDefaultPreprocessor()
|
||||||
}
|
}
|
||||||
visualization.value = module.default
|
visualization.value = module.default
|
||||||
|
icon.value = module.icon
|
||||||
} else {
|
} else {
|
||||||
switch (props.currentType.module.kind) {
|
switch (props.currentType.module.kind) {
|
||||||
case 'Builtin': {
|
case 'Builtin': {
|
||||||
@ -128,6 +133,9 @@ provideVisualizationConfig({
|
|||||||
get currentType() {
|
get currentType() {
|
||||||
return props.currentType ?? DEFAULT_VISUALIZATION_IDENTIFIER
|
return props.currentType ?? DEFAULT_VISUALIZATION_IDENTIFIER
|
||||||
},
|
},
|
||||||
|
get icon() {
|
||||||
|
return icon.value
|
||||||
|
},
|
||||||
hide: () => emit('setVisualizationVisible', false),
|
hide: () => emit('setVisualizationVisible', false),
|
||||||
updateType: (id) => emit('setVisualizationId', id),
|
updateType: (id) => emit('setVisualizationId', id),
|
||||||
})
|
})
|
||||||
|
@ -5,14 +5,15 @@
|
|||||||
* It displays one group defined in `@/assets/icons.svg` file, specified by `variant` property.
|
* It displays one group defined in `@/assets/icons.svg` file, specified by `variant` property.
|
||||||
*/
|
*/
|
||||||
import icons from '@/assets/icons.svg'
|
import icons from '@/assets/icons.svg'
|
||||||
|
import type { URLString } from '@/stores/visualization/compilerMessaging'
|
||||||
import type { Icon } from '@/util/iconName'
|
import type { Icon } from '@/util/iconName'
|
||||||
|
|
||||||
const props = defineProps<{ name: Icon; width?: number; height?: number }>()
|
const props = defineProps<{ name: Icon | URLString; width?: number; height?: number }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<svg :style="{ '--width': `${width ?? 16}px`, '--height': `${height ?? 16}px` }">
|
<svg :style="{ '--width': `${width ?? 16}px`, '--height': `${height ?? 16}px` }">
|
||||||
<use :href="`${icons}#${props.name}`"></use>
|
<use :href="props.name.includes(':') ? props.name : `${icons}#${props.name}`"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import SvgIcon from '@/components/SvgIcon.vue'
|
import SvgIcon from '@/components/SvgIcon.vue'
|
||||||
import VisualizationSelector from '@/components/VisualizationSelector.vue'
|
import VisualizationSelector from '@/components/VisualizationSelector.vue'
|
||||||
import { useVisualizationConfig } from '@/providers/visualizationConfig'
|
import { useVisualizationConfig } from '@/providers/visualizationConfig'
|
||||||
import { PointerButtonMask, usePointer } from '@/util/events'
|
import { PointerButtonMask, isClick, usePointer } from '@/util/events'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -28,6 +28,17 @@ function onWheel(event: WheelEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function blur(event: Event) {
|
||||||
|
const target = event.target
|
||||||
|
if (
|
||||||
|
!(target instanceof HTMLElement) &&
|
||||||
|
!(target instanceof SVGElement) &&
|
||||||
|
!(target instanceof MathMLElement)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
setTimeout(() => target.blur(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
const rootNode = ref<HTMLElement>()
|
const rootNode = ref<HTMLElement>()
|
||||||
const contentNode = ref<HTMLElement>()
|
const contentNode = ref<HTMLElement>()
|
||||||
|
|
||||||
@ -102,14 +113,19 @@ const resizeBottomRight = usePointer((pos, _, type) => {
|
|||||||
hidden: config.fullscreen,
|
hidden: config.fullscreen,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<button class="image-button active" @pointerdown.stop="config.hide()">
|
<button
|
||||||
|
class="image-button active"
|
||||||
|
@pointerdown.stop="config.hide()"
|
||||||
|
@click="config.hide()"
|
||||||
|
>
|
||||||
<SvgIcon class="icon" name="eye" />
|
<SvgIcon class="icon" name="eye" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button
|
<button
|
||||||
class="image-button active"
|
class="image-button active"
|
||||||
@pointerdown.stop="config.fullscreen = !config.fullscreen"
|
@pointerdown.stop="(config.fullscreen = !config.fullscreen), blur($event)"
|
||||||
|
@click.prevent="!isClick($event) && (config.fullscreen = !config.fullscreen)"
|
||||||
>
|
>
|
||||||
<SvgIcon class="icon" :name="config.fullscreen ? 'exit_fullscreen' : 'fullscreen'" />
|
<SvgIcon class="icon" :name="config.fullscreen ? 'exit_fullscreen' : 'fullscreen'" />
|
||||||
</button>
|
</button>
|
||||||
@ -117,8 +133,9 @@ const resizeBottomRight = usePointer((pos, _, type) => {
|
|||||||
<button
|
<button
|
||||||
class="image-button active"
|
class="image-button active"
|
||||||
@pointerdown.stop="isSelectorVisible = !isSelectorVisible"
|
@pointerdown.stop="isSelectorVisible = !isSelectorVisible"
|
||||||
|
@click.prevent="!isClick($event) && (isSelectorVisible = !isSelectorVisible)"
|
||||||
>
|
>
|
||||||
<SvgIcon class="icon" name="compass" />
|
<SvgIcon class="icon" :name="config.icon ?? 'columns_increasing'" />
|
||||||
</button>
|
</button>
|
||||||
<VisualizationSelector
|
<VisualizationSelector
|
||||||
v-if="isSelectorVisible"
|
v-if="isSelectorVisible"
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import SvgIcon from '@/components/SvgIcon.vue'
|
||||||
|
import { useVisualizationStore } from '@/stores/visualization'
|
||||||
|
import { useAutoBlur } from '@/util/autoBlur'
|
||||||
import { visIdentifierEquals, type VisualizationIdentifier } from 'shared/yjsModel'
|
import { visIdentifierEquals, type VisualizationIdentifier } from 'shared/yjsModel'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
@ -8,7 +11,10 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{ hide: []; 'update:modelValue': [type: VisualizationIdentifier] }>()
|
const emit = defineEmits<{ hide: []; 'update:modelValue': [type: VisualizationIdentifier] }>()
|
||||||
|
|
||||||
|
const visualizationStore = useVisualizationStore()
|
||||||
|
|
||||||
const rootNode = ref<HTMLElement>()
|
const rootNode = ref<HTMLElement>()
|
||||||
|
useAutoBlur(rootNode)
|
||||||
|
|
||||||
function visIdLabel(id: VisualizationIdentifier) {
|
function visIdLabel(id: VisualizationIdentifier) {
|
||||||
switch (id.module.kind) {
|
switch (id.module.kind) {
|
||||||
@ -26,9 +32,7 @@ function visIdKey(id: VisualizationIdentifier) {
|
|||||||
return `${kindKey}::${id.name}`
|
return `${kindKey}::${id.name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => setTimeout(() => rootNode.value?.focus(), 0))
|
||||||
setTimeout(() => rootNode.value?.focus(), 0)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -40,8 +44,10 @@ onMounted(() => {
|
|||||||
:key="visIdKey(type_)"
|
:key="visIdKey(type_)"
|
||||||
:class="{ selected: visIdentifierEquals(props.modelValue, type_) }"
|
:class="{ selected: visIdentifierEquals(props.modelValue, type_) }"
|
||||||
@pointerdown.stop="emit('update:modelValue', type_)"
|
@pointerdown.stop="emit('update:modelValue', type_)"
|
||||||
v-text="visIdLabel(type_)"
|
>
|
||||||
></li>
|
<SvgIcon class="icon" :name="visualizationStore.icon(type_) ?? 'columns_increasing'" />
|
||||||
|
<span v-text="visIdLabel(type_)"></span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -72,14 +78,18 @@ onMounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.VisualizationSelector > ul {
|
ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
|
gap: 2px;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export const name = 'Geo Map'
|
export const name = 'Geo Map'
|
||||||
|
export const icon = 'compass'
|
||||||
export const inputType = 'Standard.Table.Data.Table.Table'
|
export const inputType = 'Standard.Table.Data.Table.Table'
|
||||||
export const scripts = [
|
export const scripts = [
|
||||||
// mapbox-gl does not have an ESM release.
|
// mapbox-gl does not have an ESM release.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export const name = 'Heatmap'
|
export const name = 'Heatmap'
|
||||||
|
export const icon = 'heatmap'
|
||||||
export const inputType = 'Standard.Table.Data.Table.Table | Standard.Base.Data.Vector.Vector'
|
export const inputType = 'Standard.Table.Data.Table.Table | Standard.Base.Data.Vector.Vector'
|
||||||
export const defaultPreprocessor = [
|
export const defaultPreprocessor = [
|
||||||
'Standard.Visualization.Table.Visualization',
|
'Standard.Visualization.Table.Visualization',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export const name = 'Image'
|
export const name = 'Image'
|
||||||
|
export const icon = 'image'
|
||||||
export const inputType = 'Standard.Image.Data.Image.Image'
|
export const inputType = 'Standard.Image.Data.Image.Image'
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export const name = 'JSON'
|
export const name = 'JSON'
|
||||||
|
export const icon = 'braces'
|
||||||
export const inputType = 'Any'
|
export const inputType = 'Any'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export const name = 'SQL Query'
|
export const name = 'SQL Query'
|
||||||
|
export const icon = 'braces'
|
||||||
export const inputType = 'Standard.Database.Data.Table.Table | Standard.Database.Data.Column.Column'
|
export const inputType = 'Standard.Database.Data.Table.Table | Standard.Database.Data.Column.Column'
|
||||||
export const defaultPreprocessor = [
|
export const defaultPreprocessor = [
|
||||||
'Standard.Visualization.SQL.Visualization',
|
'Standard.Visualization.SQL.Visualization',
|
||||||
|
@ -7,6 +7,7 @@ import { VisualizationContainer, defineKeybinds } from '@/util/visualizationBuil
|
|||||||
import { computed, ref, watch, watchEffect, watchPostEffect } from 'vue'
|
import { computed, ref, watch, watchEffect, watchPostEffect } from 'vue'
|
||||||
|
|
||||||
export const name = 'Scatter Plot'
|
export const name = 'Scatter Plot'
|
||||||
|
export const icon = 'points'
|
||||||
export const inputType = 'Standard.Table.Data.Table.Table | Standard.Base.Data.Vector.Vector'
|
export const inputType = 'Standard.Table.Data.Table.Table | Standard.Base.Data.Vector.Vector'
|
||||||
const DEFAULT_LIMIT = 1024
|
const DEFAULT_LIMIT = 1024
|
||||||
export const defaultPreprocessor = [
|
export const defaultPreprocessor = [
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export const name = 'Table'
|
export const name = 'Table'
|
||||||
|
export const icon = 'table'
|
||||||
export const inputType =
|
export const inputType =
|
||||||
'Standard.Table.Data.Table.Table | Standard.Table.Data.Column.Column | Standard.Table.Data.Row.Row |Standard.Base.Data.Vector.Vector | Standard.Base.Data.Array.Array | Standard.Base.Data.Map.Map | Any'
|
'Standard.Table.Data.Table.Table | Standard.Table.Data.Column.Column | Standard.Table.Data.Row.Row |Standard.Base.Data.Vector.Vector | Standard.Base.Data.Array.Array | Standard.Base.Data.Map.Map | Any'
|
||||||
export const defaultPreprocessor = [
|
export const defaultPreprocessor = [
|
||||||
@ -60,7 +61,6 @@ interface UnknownTable {
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import VisualizationContainer from '@/components/VisualizationContainer.vue'
|
import VisualizationContainer from '@/components/VisualizationContainer.vue'
|
||||||
import { useVisualizationConfig } from '@/providers/visualizationConfig'
|
|
||||||
import type {
|
import type {
|
||||||
ColDef,
|
ColDef,
|
||||||
ColumnResizedEvent,
|
ColumnResizedEvent,
|
||||||
@ -69,26 +69,27 @@ import type {
|
|||||||
} from '@ag-grid-community/core'
|
} from '@ag-grid-community/core'
|
||||||
import '@ag-grid-community/styles/ag-grid.css'
|
import '@ag-grid-community/styles/ag-grid.css'
|
||||||
import '@ag-grid-community/styles/ag-theme-alpine.css'
|
import '@ag-grid-community/styles/ag-theme-alpine.css'
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
import { computed, onMounted, reactive, ref, watchEffect, type Ref } from 'vue'
|
||||||
import { computed, onMounted, ref, watch, watchEffect, type Ref } from 'vue'
|
const [
|
||||||
const [{ ClientSideRowModelModule }, { Grid, ModuleRegistry }, { LicenseManager }] =
|
{ ClientSideRowModelModule },
|
||||||
await Promise.all([
|
{ RangeSelectionModule },
|
||||||
import('@ag-grid-community/client-side-row-model'),
|
{ Grid, ModuleRegistry },
|
||||||
import('@ag-grid-community/core'),
|
{ LicenseManager },
|
||||||
import('@ag-grid-enterprise/core'),
|
] = await Promise.all([
|
||||||
])
|
import('@ag-grid-community/client-side-row-model'),
|
||||||
|
import('@ag-grid-enterprise/range-selection'),
|
||||||
|
import('@ag-grid-community/core'),
|
||||||
|
import('@ag-grid-enterprise/core'),
|
||||||
|
])
|
||||||
|
|
||||||
ModuleRegistry.registerModules([ClientSideRowModelModule])
|
ModuleRegistry.registerModules([ClientSideRowModelModule, RangeSelectionModule])
|
||||||
|
|
||||||
const props = defineProps<{ data: Data }>()
|
const props = defineProps<{ data: Data }>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:preprocessor': [module: string, method: string, ...args: string[]]
|
'update:preprocessor': [module: string, method: string, ...args: string[]]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const config = useVisualizationConfig()
|
|
||||||
|
|
||||||
const INDEX_FIELD_NAME = '#'
|
const INDEX_FIELD_NAME = '#'
|
||||||
const SIDE_MARGIN = 20
|
|
||||||
|
|
||||||
const rowLimit = ref(0)
|
const rowLimit = ref(0)
|
||||||
const page = ref(0)
|
const page = ref(0)
|
||||||
@ -96,6 +97,7 @@ const pageLimit = ref(0)
|
|||||||
const rowCount = ref(0)
|
const rowCount = ref(0)
|
||||||
const isTruncated = ref(false)
|
const isTruncated = ref(false)
|
||||||
const tableNode = ref<HTMLElement>()
|
const tableNode = ref<HTMLElement>()
|
||||||
|
const widths = reactive(new Map<string, number>())
|
||||||
const defaultColDef = {
|
const defaultColDef = {
|
||||||
editable: false,
|
editable: false,
|
||||||
sortable: true as boolean,
|
sortable: true as boolean,
|
||||||
@ -106,27 +108,27 @@ const defaultColDef = {
|
|||||||
cellRenderer: cellRenderer,
|
cellRenderer: cellRenderer,
|
||||||
}
|
}
|
||||||
const agGridOptions: Ref<GridOptions & Required<Pick<GridOptions, 'defaultColDef'>>> = ref({
|
const agGridOptions: Ref<GridOptions & Required<Pick<GridOptions, 'defaultColDef'>>> = ref({
|
||||||
headerHeight: 20,
|
headerHeight: 26,
|
||||||
rowHeight: 20,
|
rowHeight: 22,
|
||||||
rowData: [],
|
rowData: [],
|
||||||
columnDefs: [],
|
columnDefs: [],
|
||||||
defaultColDef: defaultColDef as typeof defaultColDef & { manuallySized: boolean },
|
defaultColDef: defaultColDef as typeof defaultColDef & { manuallySized: boolean },
|
||||||
|
onFirstDataRendered: updateColumnWidths,
|
||||||
|
onRowDataUpdated: updateColumnWidths,
|
||||||
onColumnResized: lockColumnSize,
|
onColumnResized: lockColumnSize,
|
||||||
suppressFieldDotNotation: true,
|
suppressFieldDotNotation: true,
|
||||||
|
enableRangeSelection: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const isFirstPage = computed(() => page.value === 0)
|
|
||||||
const isLastPage = computed(() => page.value === pageLimit.value - 1)
|
|
||||||
const isRowCountSelectorVisible = computed(() => rowCount.value >= 1000)
|
const isRowCountSelectorVisible = computed(() => rowCount.value >= 1000)
|
||||||
const selectableRowLimits = computed(() =>
|
const selectableRowLimits = computed(() =>
|
||||||
[1000, 2500, 5000, 10000, 25000, 50000, 100000].filter((r) => r <= rowCount.value),
|
[1000, 2500, 5000, 10000, 25000, 50000, 100000].filter((r) => r <= rowCount.value),
|
||||||
)
|
)
|
||||||
const wasAutomaticallyAutosized = ref(false)
|
const wasAutomaticallyAutosized = ref(false)
|
||||||
|
|
||||||
function setRowLimitAndPage(newRowLimit: number, newPage: number) {
|
function setRowLimit(newRowLimit: number) {
|
||||||
if (newRowLimit !== rowLimit.value || newPage !== page.value) {
|
if (newRowLimit !== rowLimit.value) {
|
||||||
rowLimit.value = newRowLimit
|
rowLimit.value = newRowLimit
|
||||||
page.value = newPage
|
|
||||||
emit(
|
emit(
|
||||||
'update:preprocessor',
|
'update:preprocessor',
|
||||||
'Standard.Visualization.Table.Visualization',
|
'Standard.Visualization.Table.Visualization',
|
||||||
@ -148,14 +150,10 @@ function escapeHTML(str: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cellRenderer(params: { value: string | null }) {
|
function cellRenderer(params: { value: string | null }) {
|
||||||
if (params.value === null) {
|
if (params.value === null) return '<span style="color:grey; font-style: italic;">Nothing</span>'
|
||||||
return '<span style="color:grey; font-style: italic;">Nothing</span>'
|
else if (params.value === undefined) return ''
|
||||||
} else if (params.value === undefined) {
|
else if (params.value === '') return '<span style="color:grey; font-style: italic;">Empty</span>'
|
||||||
return ''
|
else return escapeHTML(params.value.toString())
|
||||||
} else if (params.value === '') {
|
|
||||||
return '<span style="color:grey; font-style: italic;">Empty</span>'
|
|
||||||
}
|
|
||||||
return escapeHTML(params.value.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRowIndex(data: object[]): object[] {
|
function addRowIndex(data: object[]): object[] {
|
||||||
@ -200,7 +198,7 @@ function isMatrix(data: object): data is LegacyMatrix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toField(name: string): ColDef {
|
function toField(name: string): ColDef {
|
||||||
return { field: name, manuallySized: false }
|
return { field: name }
|
||||||
}
|
}
|
||||||
|
|
||||||
function indexField(): ColDef {
|
function indexField(): ColDef {
|
||||||
@ -228,7 +226,7 @@ function toRender(content: unknown) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(content)
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@ -242,36 +240,33 @@ watchEffect(() => {
|
|||||||
let rowData: object[] = []
|
let rowData: object[] = []
|
||||||
|
|
||||||
if ('error' in data_) {
|
if ('error' in data_) {
|
||||||
options.api.setColumnDefs([
|
columnDefs = [
|
||||||
{
|
{
|
||||||
field: 'Error',
|
field: 'Error',
|
||||||
cellStyle: { 'white-space': 'normal' },
|
cellStyle: { 'white-space': 'normal' },
|
||||||
manuallySized: false,
|
|
||||||
},
|
},
|
||||||
])
|
]
|
||||||
options.api.setRowData([{ Error: data_.error }])
|
rowData = [{ Error: data_.error }]
|
||||||
} else if (data_.type === 'Matrix') {
|
} else if (data_.type === 'Matrix') {
|
||||||
let defs: ColDef[] = [indexField()]
|
columnDefs.push(indexField())
|
||||||
for (let i = 0; i < data_.column_count; i++) {
|
for (let i = 0; i < data_.column_count; i++) {
|
||||||
defs.push(toField(i.toString()))
|
columnDefs.push(toField(i.toString()))
|
||||||
}
|
}
|
||||||
columnDefs = defs
|
|
||||||
rowData = addRowIndex(data_.json)
|
rowData = addRowIndex(data_.json)
|
||||||
isTruncated.value = data_.all_rows_count !== data_.json.length
|
isTruncated.value = data_.all_rows_count !== data_.json.length
|
||||||
} else if (data_.type === 'Object_Matrix') {
|
} else if (data_.type === 'Object_Matrix') {
|
||||||
let defs: ColDef[] = [indexField()]
|
columnDefs.push(indexField())
|
||||||
let keys = new Set<string>()
|
let keys = new Set<string>()
|
||||||
for (const val of data_.json) {
|
for (const val of data_.json) {
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
Object.keys(val).forEach((k) => {
|
Object.keys(val).forEach((k) => {
|
||||||
if (!keys.has(k)) {
|
if (!keys.has(k)) {
|
||||||
keys.add(k)
|
keys.add(k)
|
||||||
defs.push(toField(k))
|
columnDefs.push(toField(k))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
columnDefs = defs
|
|
||||||
rowData = addRowIndex(data_.json)
|
rowData = addRowIndex(data_.json)
|
||||||
isTruncated.value = data_.all_rows_count !== data_.json.length
|
isTruncated.value = data_.all_rows_count !== data_.json.length
|
||||||
} else if (isMatrix(data_)) {
|
} else if (isMatrix(data_)) {
|
||||||
@ -295,7 +290,6 @@ watchEffect(() => {
|
|||||||
const indicesHeader = ('indices_header' in data_ ? data_.indices_header : []).map(toField)
|
const indicesHeader = ('indices_header' in data_ ? data_.indices_header : []).map(toField)
|
||||||
const dataHeader = ('header' in data_ ? data_.header : [])?.map(toField) ?? []
|
const dataHeader = ('header' in data_ ? data_.header : [])?.map(toField) ?? []
|
||||||
columnDefs = [...indicesHeader, ...dataHeader]
|
columnDefs = [...indicesHeader, ...dataHeader]
|
||||||
|
|
||||||
const rows =
|
const rows =
|
||||||
data_.data && data_.data.length > 0
|
data_.data && data_.data.length > 0
|
||||||
? data_.data[0]?.length ?? 0
|
? data_.data[0]?.length ?? 0
|
||||||
@ -323,104 +317,73 @@ watchEffect(() => {
|
|||||||
page.value = newPageLimit
|
page.value = newPageLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If an existing grid, merge width from manually sized columns.
|
||||||
|
const newWidths = new Map<string, number>()
|
||||||
|
const mergedColumnDefs = columnDefs.map((columnDef) => {
|
||||||
|
if (!columnDef.field) return columnDef
|
||||||
|
const width = widths.get(columnDef.field)
|
||||||
|
if (width != null) newWidths.set(columnDef.field, (columnDef.width = width))
|
||||||
|
return columnDef
|
||||||
|
})
|
||||||
|
widths.clear()
|
||||||
|
for (const [key, value] of newWidths) widths.set(key, value)
|
||||||
|
|
||||||
// If data is truncated, we cannot rely on sorting/filtering so will disable.
|
// If data is truncated, we cannot rely on sorting/filtering so will disable.
|
||||||
options.defaultColDef.filter = !isTruncated.value
|
options.defaultColDef.filter = !isTruncated.value
|
||||||
options.defaultColDef.sortable = !isTruncated.value
|
options.defaultColDef.sortable = !isTruncated.value
|
||||||
options.api.setColumnDefs(columnDefs)
|
options.api.setColumnDefs(mergedColumnDefs)
|
||||||
options.api.setRowData(rowData)
|
options.api.setRowData(rowData)
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateTableSize(clientWidth: number | undefined) {
|
function updateColumnWidths() {
|
||||||
clientWidth ??= tableNode.value?.getBoundingClientRect().width ?? 0
|
|
||||||
const columnApi = agGridOptions.value.columnApi
|
const columnApi = agGridOptions.value.columnApi
|
||||||
if (columnApi == null) {
|
if (columnApi == null) {
|
||||||
console.warn('AG Grid column API does not exist.')
|
console.warn('AG Grid column API does not exist.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Resize columns to fit the table width unless the user manually resized them.
|
const cols = columnApi.getAllGridColumns().filter((c) => {
|
||||||
const cols = columnApi.getAllGridColumns().filter((c) => !c.getColDef().manuallySized)
|
const field = c.getColDef().field
|
||||||
// Compute the maximum width of a column: the client width minus a small margin.
|
return field && !widths.has(field)
|
||||||
const maxWidth = clientWidth - SIDE_MARGIN
|
})
|
||||||
// Resize based on the data and then shrink any columns that are too wide.
|
columnApi.autoSizeColumns(cols)
|
||||||
wasAutomaticallyAutosized.value = true
|
|
||||||
columnApi.autoSizeColumns(cols, true)
|
|
||||||
const bigCols = cols
|
|
||||||
.filter((c) => c.getActualWidth() > maxWidth)
|
|
||||||
.map((c) => ({ key: c, newWidth: maxWidth, manuallySized: false }))
|
|
||||||
columnApi.setColumnWidths(bigCols)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function lockColumnSize(e: ColumnResizedEvent) {
|
function lockColumnSize(e: ColumnResizedEvent) {
|
||||||
// Check if the resize is finished, and it's not from the API (which is triggered by us).
|
// Check if the resize is finished, and it's not from the API (which is triggered by us).
|
||||||
if (!e.finished || e.source === 'api') {
|
if (!e.finished || e.source === 'api') return
|
||||||
return
|
|
||||||
}
|
|
||||||
// If the user manually resized (or manually autosized) a column, we don't want to auto-size it
|
// If the user manually resized (or manually autosized) a column, we don't want to auto-size it
|
||||||
// on a resize.
|
// on a resize.
|
||||||
const manuallySized = e.source !== 'autosizeColumns' || !wasAutomaticallyAutosized.value
|
const manuallySized = e.source !== 'autosizeColumns' || !wasAutomaticallyAutosized.value
|
||||||
wasAutomaticallyAutosized.value = false
|
wasAutomaticallyAutosized.value = false
|
||||||
for (const column of e.columns ?? []) {
|
for (const column of e.columns ?? []) {
|
||||||
column.getColDef().manuallySized = manuallySized
|
const field = column.getColDef().field
|
||||||
|
if (field && manuallySized) widths.set(field, column.getActualWidth())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToFirstPage() {
|
|
||||||
setRowLimitAndPage(rowLimit.value, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToPreviousPage() {
|
|
||||||
setRowLimitAndPage(rowLimit.value, page.value - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToNextPage() {
|
|
||||||
setRowLimitAndPage(rowLimit.value, page.value + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToLastPage() {
|
|
||||||
setRowLimitAndPage(rowLimit.value, pageLimit.value - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===============
|
// ===============
|
||||||
// === Updates ===
|
// === Updates ===
|
||||||
// ===============
|
// ===============
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setRowLimitAndPage(1000, 0)
|
setRowLimit(1000)
|
||||||
if ('AG_GRID_LICENSE_KEY' in window && typeof window.AG_GRID_LICENSE_KEY === 'string') {
|
if ('AG_GRID_LICENSE_KEY' in window && typeof window.AG_GRID_LICENSE_KEY === 'string') {
|
||||||
LicenseManager.setLicenseKey(window.AG_GRID_LICENSE_KEY)
|
LicenseManager.setLicenseKey(window.AG_GRID_LICENSE_KEY)
|
||||||
} else {
|
} else {
|
||||||
console.warn('The AG_GRID_LICENSE_KEY is not defined.')
|
console.warn('The AG_GRID_LICENSE_KEY is not defined.')
|
||||||
}
|
}
|
||||||
new Grid(tableNode.value!, agGridOptions.value)
|
new Grid(tableNode.value!, agGridOptions.value)
|
||||||
setTimeout(() => updateTableSize(undefined), 0)
|
updateColumnWidths()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
|
||||||
() => config.fullscreen,
|
|
||||||
() => queueMicrotask(() => updateTableSize(undefined)),
|
|
||||||
)
|
|
||||||
|
|
||||||
const debouncedUpdateTableSize = useDebounceFn((...args: Parameters<typeof updateTableSize>) => {
|
|
||||||
queueMicrotask(() => {
|
|
||||||
updateTableSize(...args)
|
|
||||||
})
|
|
||||||
}, 500)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [props.data, config.width],
|
|
||||||
() => debouncedUpdateTableSize(undefined),
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VisualizationContainer :belowToolbar="true" :overflow="true">
|
<VisualizationContainer :belowToolbar="true" :overflow="true">
|
||||||
<div ref="rootNode" class="TableVisualization" @wheel.stop @pointerdown.stop>
|
<div ref="rootNode" class="TableVisualization" @wheel.stop @pointerdown.stop>
|
||||||
<div class="table-visualization-status-bar">
|
<div class="table-visualization-status-bar">
|
||||||
<button :disabled="isFirstPage" @click="goToFirstPage">«</button>
|
|
||||||
<button :disabled="isFirstPage" @click="goToPreviousPage">‹</button>
|
|
||||||
<select
|
<select
|
||||||
v-if="isRowCountSelectorVisible"
|
v-if="isRowCountSelectorVisible"
|
||||||
@change="setRowLimitAndPage(Number(($event.target as HTMLOptionElement).value), page)"
|
@change="setRowLimit(Number(($event.target as HTMLOptionElement).value))"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="limit in selectableRowLimits"
|
v-for="limit in selectableRowLimits"
|
||||||
@ -436,8 +399,6 @@ watch(
|
|||||||
<span v-else-if="isRowCountSelectorVisible" v-text="' rows.'"></span>
|
<span v-else-if="isRowCountSelectorVisible" v-text="' rows.'"></span>
|
||||||
<span v-else-if="rowCount === 1" v-text="'1 row.'"></span>
|
<span v-else-if="rowCount === 1" v-text="'1 row.'"></span>
|
||||||
<span v-else v-text="`${rowCount} rows.`"></span>
|
<span v-else v-text="`${rowCount} rows.`"></span>
|
||||||
<button :disabled="isLastPage" @click="goToNextPage">›</button>
|
|
||||||
<button :disabled="isLastPage" @click="goToLastPage">»</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div ref="tableNode" class="scrollable ag-theme-alpine"></div>
|
<div ref="tableNode" class="scrollable ag-theme-alpine"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -466,12 +427,6 @@ watch(
|
|||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-visualization-status-bar > button {
|
|
||||||
width: 12px;
|
|
||||||
margin: 0 2px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export const name = 'Warnings'
|
export const name = 'Warnings'
|
||||||
|
export const icon = 'exclamation'
|
||||||
export const inputType = 'Any'
|
export const inputType = 'Any'
|
||||||
export const defaultPreprocessor = [
|
export const defaultPreprocessor = [
|
||||||
'Standard.Visualization.Warnings',
|
'Standard.Visualization.Warnings',
|
||||||
|
@ -14,14 +14,9 @@ declare module 'd3' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@ag-grid-community/core' {
|
import '@ag-grid-community/core'
|
||||||
// These type parameters are defined on the original interface.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
interface ColDef<TData, TValue> {
|
|
||||||
/** Custom user-defined value. */
|
|
||||||
manuallySized: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
|
declare module '@ag-grid-community/core' {
|
||||||
// These type parameters are defined on the original interface.
|
// These type parameters are defined on the original interface.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
interface AbstractColDef<TData, TValue> {
|
interface AbstractColDef<TData, TValue> {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import type { URLString } from '@/stores/visualization/compilerMessaging'
|
||||||
|
import type { Icon } from '@/util/iconName'
|
||||||
import { Vec2 } from '@/util/vec2'
|
import { Vec2 } from '@/util/vec2'
|
||||||
import type { VisualizationIdentifier } from 'shared/yjsModel'
|
import type { VisualizationIdentifier } from 'shared/yjsModel'
|
||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
@ -8,6 +10,7 @@ export interface VisualizationConfig {
|
|||||||
background?: string
|
background?: string
|
||||||
readonly types: Iterable<VisualizationIdentifier>
|
readonly types: Iterable<VisualizationIdentifier>
|
||||||
readonly currentType: VisualizationIdentifier
|
readonly currentType: VisualizationIdentifier
|
||||||
|
readonly icon: Icon | URLString | undefined
|
||||||
readonly isCircularMenuVisible: boolean
|
readonly isCircularMenuVisible: boolean
|
||||||
readonly nodeSize: Vec2
|
readonly nodeSize: Vec2
|
||||||
width: number | null
|
width: number | null
|
||||||
|
@ -20,6 +20,8 @@ import type {
|
|||||||
import Compiler from '@/stores/visualization/compiler?worker'
|
import Compiler from '@/stores/visualization/compiler?worker'
|
||||||
import { assertNever } from '@/util/assert'
|
import { assertNever } from '@/util/assert'
|
||||||
import { toError } from '@/util/error'
|
import { toError } from '@/util/error'
|
||||||
|
import iconNames from '@/util/iconList.json'
|
||||||
|
import type { Icon } from '@/util/iconName'
|
||||||
import type { Opt } from '@/util/opt'
|
import type { Opt } from '@/util/opt'
|
||||||
import { defineKeybinds } from '@/util/shortcuts'
|
import { defineKeybinds } from '@/util/shortcuts'
|
||||||
import { Error as DataError } from 'shared/binaryProtocol'
|
import { Error as DataError } from 'shared/binaryProtocol'
|
||||||
@ -32,12 +34,24 @@ import { z } from 'zod'
|
|||||||
export const currentProjectProtocol = 'enso-current-project:'
|
export const currentProjectProtocol = 'enso-current-project:'
|
||||||
export const stylePathAttribute = 'data-style-path'
|
export const stylePathAttribute = 'data-style-path'
|
||||||
|
|
||||||
|
export type URLString = `${string}:${string}`
|
||||||
|
|
||||||
const VisualizationModule = z.object({
|
const VisualizationModule = z.object({
|
||||||
// This is UNSAFE, but unavoiable as the type of `Visualization` is too difficult to statically
|
// This is UNSAFE, but unavoiable as the type of `Visualization` is too difficult to statically
|
||||||
// check. Insteead it will be caught by Vue when trying to mount the visualization, and replaced
|
// check. Instead it will be caught by Vue when trying to mount the visualization, and replaced
|
||||||
// with a 'Loading Error' visualization.
|
// with a 'Loading Error' visualization.
|
||||||
default: z.custom<Visualization>(() => true),
|
default: z.custom<Visualization>(() => true),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
// The name of an icon, or a URL or data URL. If it contains `:`, it is assumed to be a URL.
|
||||||
|
icon: z
|
||||||
|
.string()
|
||||||
|
.transform((s) => {
|
||||||
|
if (iconNames.includes(s)) return s as Icon
|
||||||
|
else if (s.includes(':')) return s as URLString
|
||||||
|
console.warn(`Invalid icon name '${s}'`)
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
inputType: z.string().optional(),
|
inputType: z.string().optional(),
|
||||||
defaultPreprocessor: (
|
defaultPreprocessor: (
|
||||||
z.string().array().min(2) as unknown as z.ZodType<
|
z.string().array().min(2) as unknown as z.ZodType<
|
||||||
|
@ -95,10 +95,11 @@ export const useVisualizationStore = defineStore('visualization', () => {
|
|||||||
(roots) => roots.find((root) => root.type === 'Project')?.id,
|
(roots) => roots.find((root) => root.type === 'Project')?.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const { name, inputType } of builtinVisualizations) {
|
for (const { name, inputType, icon } of builtinVisualizations) {
|
||||||
metadata.set(toVisualizationId({ module: { kind: 'Builtin' }, name }), {
|
metadata.set(toVisualizationId({ module: { kind: 'Builtin' }, name }), {
|
||||||
name,
|
name,
|
||||||
inputType,
|
inputType,
|
||||||
|
icon,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +183,11 @@ export const useVisualizationStore = defineStore('visualization', () => {
|
|||||||
id = { module: { kind: 'CurrentProject' }, name: viz.name }
|
id = { module: { kind: 'CurrentProject' }, name: viz.name }
|
||||||
cache.set(toVisualizationId(id), vizPromise)
|
cache.set(toVisualizationId(id), vizPromise)
|
||||||
}
|
}
|
||||||
metadata.set(toVisualizationId(id), { name: viz.name, inputType: viz.inputType })
|
metadata.set(toVisualizationId(id), {
|
||||||
|
name: viz.name,
|
||||||
|
inputType: viz.inputType,
|
||||||
|
icon: viz.icon,
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (key) cache.delete(key)
|
if (key) cache.delete(key)
|
||||||
if (error instanceof InvalidVisualizationModuleError) {
|
if (error instanceof InvalidVisualizationModuleError) {
|
||||||
@ -225,6 +230,10 @@ export const useVisualizationStore = defineStore('visualization', () => {
|
|||||||
for (const type of types) yield fromVisualizationId(type)
|
for (const type of types) yield fromVisualizationId(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function icon(type: VisualizationIdentifier) {
|
||||||
|
return metadata.get(toVisualizationId(type))?.icon
|
||||||
|
}
|
||||||
|
|
||||||
function get(meta: VisualizationIdentifier, ignoreCache = false) {
|
function get(meta: VisualizationIdentifier, ignoreCache = false) {
|
||||||
const key = toVisualizationId(meta)
|
const key = toVisualizationId(meta)
|
||||||
if (!cache.get(key) || ignoreCache) {
|
if (!cache.get(key) || ignoreCache) {
|
||||||
@ -253,5 +262,5 @@ export const useVisualizationStore = defineStore('visualization', () => {
|
|||||||
return module
|
return module
|
||||||
}
|
}
|
||||||
|
|
||||||
return { types, get }
|
return { types, get, icon }
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,8 @@ import { ReactiveDb, ReactiveIndex } from '@/util/database/reactiveDb'
|
|||||||
import type { Opt } from '@/util/opt'
|
import type { Opt } from '@/util/opt'
|
||||||
import type { VisualizationIdentifier } from 'shared/yjsModel'
|
import type { VisualizationIdentifier } from 'shared/yjsModel'
|
||||||
|
|
||||||
export interface VisualizationMetadata extends Pick<VisualizationModule, 'name' | 'inputType'> {}
|
export interface VisualizationMetadata
|
||||||
|
extends Pick<VisualizationModule, 'name' | 'inputType' | 'icon'> {}
|
||||||
|
|
||||||
function getTypesFromUnion(inputType: Opt<string>) {
|
function getTypesFromUnion(inputType: Opt<string>) {
|
||||||
return inputType?.split('|').map((type) => type.trim()) ?? ['Any']
|
return inputType?.split('|').map((type) => type.trim()) ?? ['Any']
|
||||||
|
25
app/gui2/src/util/autoBlur.ts
Normal file
25
app/gui2/src/util/autoBlur.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useEvent } from '@/util/events'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
export function useAutoBlur(root: Ref<HTMLElement | SVGElement | MathMLElement | undefined>) {
|
||||||
|
useEvent(
|
||||||
|
window,
|
||||||
|
'pointerdown',
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
!root.value?.contains(document.activeElement) ||
|
||||||
|
!(event.target instanceof Element) ||
|
||||||
|
root.value.contains(event.target)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
!(document.activeElement instanceof HTMLElement) &&
|
||||||
|
!(document.activeElement instanceof SVGElement) &&
|
||||||
|
!(document.activeElement instanceof MathMLElement)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
document.activeElement.blur()
|
||||||
|
},
|
||||||
|
{ capture: true },
|
||||||
|
)
|
||||||
|
}
|
@ -12,6 +12,11 @@ import {
|
|||||||
type WatchSource,
|
type WatchSource,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
|
|
||||||
|
export function isClick(e: MouseEvent | PointerEvent) {
|
||||||
|
if (e instanceof PointerEvent) return e.pointerId !== -1
|
||||||
|
else return e.buttons !== 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an event listener for the duration of the component's lifetime.
|
* Add an event listener for the duration of the component's lifetime.
|
||||||
* @param target element on which to register the event
|
* @param target element on which to register the event
|
||||||
|
@ -20,7 +20,7 @@ export function useNavigator(viewportNode: Ref<Element | undefined>) {
|
|||||||
center.value = center.value.addScaled(pos.delta, -1 / scale.value)
|
center.value = center.value.addScaled(pos.delta, -1 / scale.value)
|
||||||
}, PointerButtonMask.Auxiliary)
|
}, PointerButtonMask.Auxiliary)
|
||||||
|
|
||||||
function eventScreenPos(e: PointerEvent): Vec2 {
|
function eventScreenPos(e: { clientX: number; clientY: number }): Vec2 {
|
||||||
return new Vec2(e.clientX, e.clientY)
|
return new Vec2(e.clientX, e.clientY)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,11 +103,51 @@ export function useNavigator(viewportNode: Ref<Element | undefined>) {
|
|||||||
{ capture: true },
|
{ capture: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let isPointerDown = false
|
||||||
|
let scrolledThisFrame = false
|
||||||
const eventMousePos = ref<Vec2 | null>(null)
|
const eventMousePos = ref<Vec2 | null>(null)
|
||||||
|
let eventTargetScrollPos: Vec2 | null = null
|
||||||
const sceneMousePos = computed(() =>
|
const sceneMousePos = computed(() =>
|
||||||
eventMousePos.value ? clientToScenePos(eventMousePos.value) : null,
|
eventMousePos.value ? clientToScenePos(eventMousePos.value) : null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEvent(
|
||||||
|
window,
|
||||||
|
'scroll',
|
||||||
|
(e) => {
|
||||||
|
if (
|
||||||
|
!isPointerDown ||
|
||||||
|
scrolledThisFrame ||
|
||||||
|
!eventMousePos.value ||
|
||||||
|
!(e.target instanceof Element)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
scrolledThisFrame = true
|
||||||
|
requestAnimationFrame(() => (scrolledThisFrame = false))
|
||||||
|
if (!(e.target instanceof Element)) return
|
||||||
|
const newScrollPos = new Vec2(e.target.scrollLeft, e.target.scrollTop)
|
||||||
|
if (eventTargetScrollPos !== null) {
|
||||||
|
const delta = newScrollPos.sub(eventTargetScrollPos)
|
||||||
|
const mouseDelta = new Vec2(
|
||||||
|
(delta.x * e.target.clientWidth) / e.target.scrollWidth,
|
||||||
|
(delta.y * e.target.clientHeight) / e.target.scrollHeight,
|
||||||
|
)
|
||||||
|
eventMousePos.value = eventMousePos.value?.add(mouseDelta) ?? null
|
||||||
|
}
|
||||||
|
eventTargetScrollPos = newScrollPos
|
||||||
|
},
|
||||||
|
{ capture: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
useEvent(
|
||||||
|
window,
|
||||||
|
'scrollend',
|
||||||
|
() => {
|
||||||
|
eventTargetScrollPos = null
|
||||||
|
},
|
||||||
|
{ capture: true },
|
||||||
|
)
|
||||||
|
|
||||||
return proxyRefs({
|
return proxyRefs({
|
||||||
events: {
|
events: {
|
||||||
pointermove(e: PointerEvent) {
|
pointermove(e: PointerEvent) {
|
||||||
@ -119,10 +159,12 @@ export function useNavigator(viewportNode: Ref<Element | undefined>) {
|
|||||||
eventMousePos.value = null
|
eventMousePos.value = null
|
||||||
},
|
},
|
||||||
pointerup(e: PointerEvent) {
|
pointerup(e: PointerEvent) {
|
||||||
|
isPointerDown = false
|
||||||
panPointer.events.pointerup(e)
|
panPointer.events.pointerup(e)
|
||||||
zoomPointer.events.pointerup(e)
|
zoomPointer.events.pointerup(e)
|
||||||
},
|
},
|
||||||
pointerdown(e: PointerEvent) {
|
pointerdown(e: PointerEvent) {
|
||||||
|
isPointerDown = true
|
||||||
panPointer.events.pointerdown(e)
|
panPointer.events.pointerdown(e)
|
||||||
zoomPointer.events.pointerdown(e)
|
zoomPointer.events.pointerdown(e)
|
||||||
},
|
},
|
||||||
|
24
package-lock.json
generated
24
package-lock.json
generated
@ -30,6 +30,7 @@
|
|||||||
"@ag-grid-community/core": "^30.2.0",
|
"@ag-grid-community/core": "^30.2.0",
|
||||||
"@ag-grid-community/styles": "^30.2.0",
|
"@ag-grid-community/styles": "^30.2.0",
|
||||||
"@ag-grid-enterprise/core": "^30.2.0",
|
"@ag-grid-enterprise/core": "^30.2.0",
|
||||||
|
"@ag-grid-enterprise/range-selection": "^30.2.1",
|
||||||
"@babel/parser": "^7.22.16",
|
"@babel/parser": "^7.22.16",
|
||||||
"@fast-check/vitest": "^0.0.8",
|
"@fast-check/vitest": "^0.0.8",
|
||||||
"@lezer/common": "^1.1.0",
|
"@lezer/common": "^1.1.0",
|
||||||
@ -444,9 +445,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ag-grid-community/core": {
|
"node_modules/@ag-grid-community/core": {
|
||||||
"version": "30.2.0",
|
"version": "30.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@ag-grid-community/core/-/core-30.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ag-grid-community/core/-/core-30.2.1.tgz",
|
||||||
"integrity": "sha512-BdRavyYxyl0rx3w4VKQlV9KHRR9TIJ9RFt4FIsWUxC+VDC+VEL64J1PzgJqXqgkM7Zmb+rYbluA2UPmRrQQJ9A=="
|
"integrity": "sha512-jGRBfRFsLwxch8GJGjbVI2FVbB+/fy1s4mm8//+kOkcPFlq4BbLFELvU4Kupwk1YkhduxUC50GTYyFzmF0rSIQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@ag-grid-community/styles": {
|
"node_modules/@ag-grid-community/styles": {
|
||||||
"version": "30.2.0",
|
"version": "30.2.0",
|
||||||
@ -454,11 +455,20 @@
|
|||||||
"integrity": "sha512-ipI/weI0jgvhZI/PooNkKUJlJQ21uiyLKmzeQkSOTBPxSqpszvBm5BGkeaw+WmTc6dF/dF2yRVcFz8uTcwveLA=="
|
"integrity": "sha512-ipI/weI0jgvhZI/PooNkKUJlJQ21uiyLKmzeQkSOTBPxSqpszvBm5BGkeaw+WmTc6dF/dF2yRVcFz8uTcwveLA=="
|
||||||
},
|
},
|
||||||
"node_modules/@ag-grid-enterprise/core": {
|
"node_modules/@ag-grid-enterprise/core": {
|
||||||
"version": "30.2.0",
|
"version": "30.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@ag-grid-enterprise/core/-/core-30.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ag-grid-enterprise/core/-/core-30.2.1.tgz",
|
||||||
"integrity": "sha512-0XZCBVoOCUmtOkaDr2g+r8ORbT+dV1UqA5dZMwPSC6P537RKPFFCe9YPjwGxPS06YB7VpQcnUF3Pw3TY6ibnyw==",
|
"integrity": "sha512-POVdYwMho+WPQ1wrNwQwX4ihGvdq+UbLIHQfWnnZn5c2FLFiUmlxAhCZAXXiOeXq5MDHBKqKrTRemb9TUbBRcg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ag-grid-community/core": "~30.2.0"
|
"@ag-grid-community/core": "~30.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ag-grid-enterprise/range-selection": {
|
||||||
|
"version": "30.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ag-grid-enterprise/range-selection/-/range-selection-30.2.1.tgz",
|
||||||
|
"integrity": "sha512-zY/yHnrWd5RLXP2QfCYyUV6VZK76NrnHwKZrsnN4SBpHSI3dV2yO4GVJGu6SVEJM+6H1yNqTriJjA/hTPfmuLQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@ag-grid-community/core": "~30.2.1",
|
||||||
|
"@ag-grid-enterprise/core": "~30.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@akryum/tinypool": {
|
"node_modules/@akryum/tinypool": {
|
||||||
|
Loading…
Reference in New Issue
Block a user