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:
somebody1234 2023-11-20 23:23:50 +10:00 committed by GitHub
parent 1c936cc69f
commit 062992bb8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 304 additions and 138 deletions

View File

@ -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",

View File

@ -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

View File

@ -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 ==

View File

@ -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),
}) })

View File

@ -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>

View File

@ -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"

View File

@ -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;

View File

@ -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.

View File

@ -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',

View File

@ -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 {

View File

@ -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>

View File

@ -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',

View File

@ -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 = [

View File

@ -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">&lsaquo;</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">&rsaquo;</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>

View File

@ -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',

View File

@ -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> {

View File

@ -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

View File

@ -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<

View File

@ -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 }
}) })

View File

@ -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']

View 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 },
)
}

View File

@ -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

View File

@ -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
View File

@ -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": {