mirror of
https://github.com/enso-org/enso.git
synced 2024-11-26 08:52:58 +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/styles": "^30.2.0",
|
||||
"@ag-grid-enterprise/core": "^30.2.0",
|
||||
"@ag-grid-enterprise/range-selection": "^30.2.1",
|
||||
"@babel/parser": "^7.22.16",
|
||||
"@fast-check/vitest": "^0.0.8",
|
||||
"@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">
|
||||
<!-- 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">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon/add_column">
|
||||
@ -904,6 +904,22 @@
|
||||
</g>
|
||||
</svg>
|
||||
</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">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon/table_edit">
|
||||
@ -1271,5 +1287,50 @@
|
||||
fill="black" fill-opacity="0.6" />
|
||||
</svg>
|
||||
</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>
|
||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 106 KiB |
@ -2,6 +2,7 @@
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||
import { useAutoBlur } from '@/util/autoBlur'
|
||||
import type { Highlighter } from '@/util/codemirror'
|
||||
import { usePointer } from '@/util/events'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
@ -30,6 +31,7 @@ const projectStore = useProjectStore()
|
||||
const graphStore = useGraphStore()
|
||||
const suggestionDbStore = useSuggestionDbStore()
|
||||
const rootElement = ref<HTMLElement>()
|
||||
useAutoBlur(rootElement)
|
||||
|
||||
// == CodeMirror editor setup ==
|
||||
|
||||
|
@ -9,7 +9,9 @@ import {
|
||||
useVisualizationStore,
|
||||
type Visualization,
|
||||
} from '@/stores/visualization'
|
||||
import type { URLString } from '@/stores/visualization/compilerMessaging'
|
||||
import { toError } from '@/util/error'
|
||||
import type { Icon } from '@/util/iconName'
|
||||
import type { Opt } from '@/util/opt'
|
||||
import type { Vec2 } from '@/util/vec2'
|
||||
import type { ExprId, VisualizationIdentifier } from 'shared/yjsModel'
|
||||
@ -35,6 +37,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const visualization = shallowRef<Visualization>()
|
||||
const icon = ref<Icon | URLString>()
|
||||
|
||||
onErrorCaptured((vueError) => {
|
||||
error.value = vueError
|
||||
@ -76,6 +79,7 @@ watch(
|
||||
watchEffect(async () => {
|
||||
if (props.currentType == null) return
|
||||
visualization.value = undefined
|
||||
icon.value = undefined
|
||||
try {
|
||||
const module = await visualizationStore.get(props.currentType).value
|
||||
if (module) {
|
||||
@ -85,6 +89,7 @@ watchEffect(async () => {
|
||||
switchToDefaultPreprocessor()
|
||||
}
|
||||
visualization.value = module.default
|
||||
icon.value = module.icon
|
||||
} else {
|
||||
switch (props.currentType.module.kind) {
|
||||
case 'Builtin': {
|
||||
@ -128,6 +133,9 @@ provideVisualizationConfig({
|
||||
get currentType() {
|
||||
return props.currentType ?? DEFAULT_VISUALIZATION_IDENTIFIER
|
||||
},
|
||||
get icon() {
|
||||
return icon.value
|
||||
},
|
||||
hide: () => emit('setVisualizationVisible', false),
|
||||
updateType: (id) => emit('setVisualizationId', id),
|
||||
})
|
||||
|
@ -5,14 +5,15 @@
|
||||
* It displays one group defined in `@/assets/icons.svg` file, specified by `variant` property.
|
||||
*/
|
||||
import icons from '@/assets/icons.svg'
|
||||
import type { URLString } from '@/stores/visualization/compilerMessaging'
|
||||
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>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import VisualizationSelector from '@/components/VisualizationSelector.vue'
|
||||
import { useVisualizationConfig } from '@/providers/visualizationConfig'
|
||||
import { PointerButtonMask, usePointer } from '@/util/events'
|
||||
import { PointerButtonMask, isClick, usePointer } from '@/util/events'
|
||||
import { ref } from 'vue'
|
||||
|
||||
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 contentNode = ref<HTMLElement>()
|
||||
|
||||
@ -102,14 +113,19 @@ const resizeBottomRight = usePointer((pos, _, type) => {
|
||||
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" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button
|
||||
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'" />
|
||||
</button>
|
||||
@ -117,8 +133,9 @@ const resizeBottomRight = usePointer((pos, _, type) => {
|
||||
<button
|
||||
class="image-button active"
|
||||
@pointerdown.stop="isSelectorVisible = !isSelectorVisible"
|
||||
@click.prevent="!isClick($event) && (isSelectorVisible = !isSelectorVisible)"
|
||||
>
|
||||
<SvgIcon class="icon" name="compass" />
|
||||
<SvgIcon class="icon" :name="config.icon ?? 'columns_increasing'" />
|
||||
</button>
|
||||
<VisualizationSelector
|
||||
v-if="isSelectorVisible"
|
||||
|
@ -1,4 +1,7 @@
|
||||
<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 { onMounted, ref } from 'vue'
|
||||
|
||||
@ -8,7 +11,10 @@ const props = defineProps<{
|
||||
}>()
|
||||
const emit = defineEmits<{ hide: []; 'update:modelValue': [type: VisualizationIdentifier] }>()
|
||||
|
||||
const visualizationStore = useVisualizationStore()
|
||||
|
||||
const rootNode = ref<HTMLElement>()
|
||||
useAutoBlur(rootNode)
|
||||
|
||||
function visIdLabel(id: VisualizationIdentifier) {
|
||||
switch (id.module.kind) {
|
||||
@ -26,9 +32,7 @@ function visIdKey(id: VisualizationIdentifier) {
|
||||
return `${kindKey}::${id.name}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => rootNode.value?.focus(), 0)
|
||||
})
|
||||
onMounted(() => setTimeout(() => rootNode.value?.focus(), 0))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -40,8 +44,10 @@ onMounted(() => {
|
||||
:key="visIdKey(type_)"
|
||||
:class="{ selected: visIdentifierEquals(props.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>
|
||||
</div>
|
||||
</template>
|
||||
@ -72,14 +78,18 @@ onMounted(() => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.VisualizationSelector > ul {
|
||||
ul {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 2px;
|
||||
list-style-type: none;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0 8px;
|
||||
border-radius: 12px;
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
export const name = 'Geo Map'
|
||||
export const icon = 'compass'
|
||||
export const inputType = 'Standard.Table.Data.Table.Table'
|
||||
export const scripts = [
|
||||
// mapbox-gl does not have an ESM release.
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
export const name = 'Heatmap'
|
||||
export const icon = 'heatmap'
|
||||
export const inputType = 'Standard.Table.Data.Table.Table | Standard.Base.Data.Vector.Vector'
|
||||
export const defaultPreprocessor = [
|
||||
'Standard.Visualization.Table.Visualization',
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
export const name = 'Image'
|
||||
export const icon = 'image'
|
||||
export const inputType = 'Standard.Image.Data.Image.Image'
|
||||
|
||||
interface Data {
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
export const name = 'JSON'
|
||||
export const icon = 'braces'
|
||||
export const inputType = 'Any'
|
||||
</script>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
export const name = 'SQL Query'
|
||||
export const icon = 'braces'
|
||||
export const inputType = 'Standard.Database.Data.Table.Table | Standard.Database.Data.Column.Column'
|
||||
export const defaultPreprocessor = [
|
||||
'Standard.Visualization.SQL.Visualization',
|
||||
|
@ -7,6 +7,7 @@ import { VisualizationContainer, defineKeybinds } from '@/util/visualizationBuil
|
||||
import { computed, ref, watch, watchEffect, watchPostEffect } from 'vue'
|
||||
|
||||
export const name = 'Scatter Plot'
|
||||
export const icon = 'points'
|
||||
export const inputType = 'Standard.Table.Data.Table.Table | Standard.Base.Data.Vector.Vector'
|
||||
const DEFAULT_LIMIT = 1024
|
||||
export const defaultPreprocessor = [
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
export const name = 'Table'
|
||||
export const icon = 'table'
|
||||
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'
|
||||
export const defaultPreprocessor = [
|
||||
@ -60,7 +61,6 @@ interface UnknownTable {
|
||||
|
||||
<script setup lang="ts">
|
||||
import VisualizationContainer from '@/components/VisualizationContainer.vue'
|
||||
import { useVisualizationConfig } from '@/providers/visualizationConfig'
|
||||
import type {
|
||||
ColDef,
|
||||
ColumnResizedEvent,
|
||||
@ -69,26 +69,27 @@ import type {
|
||||
} from '@ag-grid-community/core'
|
||||
import '@ag-grid-community/styles/ag-grid.css'
|
||||
import '@ag-grid-community/styles/ag-theme-alpine.css'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, watch, watchEffect, type Ref } from 'vue'
|
||||
const [{ ClientSideRowModelModule }, { Grid, ModuleRegistry }, { LicenseManager }] =
|
||||
await Promise.all([
|
||||
import { computed, onMounted, reactive, ref, watchEffect, type Ref } from 'vue'
|
||||
const [
|
||||
{ ClientSideRowModelModule },
|
||||
{ RangeSelectionModule },
|
||||
{ Grid, ModuleRegistry },
|
||||
{ LicenseManager },
|
||||
] = 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 emit = defineEmits<{
|
||||
'update:preprocessor': [module: string, method: string, ...args: string[]]
|
||||
}>()
|
||||
|
||||
const config = useVisualizationConfig()
|
||||
|
||||
const INDEX_FIELD_NAME = '#'
|
||||
const SIDE_MARGIN = 20
|
||||
|
||||
const rowLimit = ref(0)
|
||||
const page = ref(0)
|
||||
@ -96,6 +97,7 @@ const pageLimit = ref(0)
|
||||
const rowCount = ref(0)
|
||||
const isTruncated = ref(false)
|
||||
const tableNode = ref<HTMLElement>()
|
||||
const widths = reactive(new Map<string, number>())
|
||||
const defaultColDef = {
|
||||
editable: false,
|
||||
sortable: true as boolean,
|
||||
@ -106,27 +108,27 @@ const defaultColDef = {
|
||||
cellRenderer: cellRenderer,
|
||||
}
|
||||
const agGridOptions: Ref<GridOptions & Required<Pick<GridOptions, 'defaultColDef'>>> = ref({
|
||||
headerHeight: 20,
|
||||
rowHeight: 20,
|
||||
headerHeight: 26,
|
||||
rowHeight: 22,
|
||||
rowData: [],
|
||||
columnDefs: [],
|
||||
defaultColDef: defaultColDef as typeof defaultColDef & { manuallySized: boolean },
|
||||
onFirstDataRendered: updateColumnWidths,
|
||||
onRowDataUpdated: updateColumnWidths,
|
||||
onColumnResized: lockColumnSize,
|
||||
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 selectableRowLimits = computed(() =>
|
||||
[1000, 2500, 5000, 10000, 25000, 50000, 100000].filter((r) => r <= rowCount.value),
|
||||
)
|
||||
const wasAutomaticallyAutosized = ref(false)
|
||||
|
||||
function setRowLimitAndPage(newRowLimit: number, newPage: number) {
|
||||
if (newRowLimit !== rowLimit.value || newPage !== page.value) {
|
||||
function setRowLimit(newRowLimit: number) {
|
||||
if (newRowLimit !== rowLimit.value) {
|
||||
rowLimit.value = newRowLimit
|
||||
page.value = newPage
|
||||
emit(
|
||||
'update:preprocessor',
|
||||
'Standard.Visualization.Table.Visualization',
|
||||
@ -148,14 +150,10 @@ function escapeHTML(str: string) {
|
||||
}
|
||||
|
||||
function cellRenderer(params: { value: string | null }) {
|
||||
if (params.value === null) {
|
||||
return '<span style="color:grey; font-style: italic;">Nothing</span>'
|
||||
} else if (params.value === undefined) {
|
||||
return ''
|
||||
} else if (params.value === '') {
|
||||
return '<span style="color:grey; font-style: italic;">Empty</span>'
|
||||
}
|
||||
return escapeHTML(params.value.toString())
|
||||
if (params.value === null) return '<span style="color:grey; font-style: italic;">Nothing</span>'
|
||||
else if (params.value === undefined) return ''
|
||||
else if (params.value === '') return '<span style="color:grey; font-style: italic;">Empty</span>'
|
||||
else return escapeHTML(params.value.toString())
|
||||
}
|
||||
|
||||
function addRowIndex(data: object[]): object[] {
|
||||
@ -200,7 +198,7 @@ function isMatrix(data: object): data is LegacyMatrix {
|
||||
}
|
||||
|
||||
function toField(name: string): ColDef {
|
||||
return { field: name, manuallySized: false }
|
||||
return { field: name }
|
||||
}
|
||||
|
||||
function indexField(): ColDef {
|
||||
@ -228,7 +226,7 @@ function toRender(content: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
return String(content)
|
||||
return content
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
@ -242,36 +240,33 @@ watchEffect(() => {
|
||||
let rowData: object[] = []
|
||||
|
||||
if ('error' in data_) {
|
||||
options.api.setColumnDefs([
|
||||
columnDefs = [
|
||||
{
|
||||
field: 'Error',
|
||||
cellStyle: { 'white-space': 'normal' },
|
||||
manuallySized: false,
|
||||
},
|
||||
])
|
||||
options.api.setRowData([{ Error: data_.error }])
|
||||
]
|
||||
rowData = [{ Error: data_.error }]
|
||||
} else if (data_.type === 'Matrix') {
|
||||
let defs: ColDef[] = [indexField()]
|
||||
columnDefs.push(indexField())
|
||||
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)
|
||||
isTruncated.value = data_.all_rows_count !== data_.json.length
|
||||
} else if (data_.type === 'Object_Matrix') {
|
||||
let defs: ColDef[] = [indexField()]
|
||||
columnDefs.push(indexField())
|
||||
let keys = new Set<string>()
|
||||
for (const val of data_.json) {
|
||||
if (val != null) {
|
||||
Object.keys(val).forEach((k) => {
|
||||
if (!keys.has(k)) {
|
||||
keys.add(k)
|
||||
defs.push(toField(k))
|
||||
columnDefs.push(toField(k))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
columnDefs = defs
|
||||
rowData = addRowIndex(data_.json)
|
||||
isTruncated.value = data_.all_rows_count !== data_.json.length
|
||||
} else if (isMatrix(data_)) {
|
||||
@ -295,7 +290,6 @@ watchEffect(() => {
|
||||
const indicesHeader = ('indices_header' in data_ ? data_.indices_header : []).map(toField)
|
||||
const dataHeader = ('header' in data_ ? data_.header : [])?.map(toField) ?? []
|
||||
columnDefs = [...indicesHeader, ...dataHeader]
|
||||
|
||||
const rows =
|
||||
data_.data && data_.data.length > 0
|
||||
? data_.data[0]?.length ?? 0
|
||||
@ -323,104 +317,73 @@ watchEffect(() => {
|
||||
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.
|
||||
options.defaultColDef.filter = !isTruncated.value
|
||||
options.defaultColDef.sortable = !isTruncated.value
|
||||
options.api.setColumnDefs(columnDefs)
|
||||
options.api.setColumnDefs(mergedColumnDefs)
|
||||
options.api.setRowData(rowData)
|
||||
})
|
||||
|
||||
function updateTableSize(clientWidth: number | undefined) {
|
||||
clientWidth ??= tableNode.value?.getBoundingClientRect().width ?? 0
|
||||
function updateColumnWidths() {
|
||||
const columnApi = agGridOptions.value.columnApi
|
||||
if (columnApi == null) {
|
||||
console.warn('AG Grid column API does not exist.')
|
||||
return
|
||||
}
|
||||
// Resize columns to fit the table width unless the user manually resized them.
|
||||
const cols = columnApi.getAllGridColumns().filter((c) => !c.getColDef().manuallySized)
|
||||
// Compute the maximum width of a column: the client width minus a small margin.
|
||||
const maxWidth = clientWidth - SIDE_MARGIN
|
||||
// Resize based on the data and then shrink any columns that are too wide.
|
||||
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)
|
||||
const cols = columnApi.getAllGridColumns().filter((c) => {
|
||||
const field = c.getColDef().field
|
||||
return field && !widths.has(field)
|
||||
})
|
||||
columnApi.autoSizeColumns(cols)
|
||||
}
|
||||
|
||||
function lockColumnSize(e: ColumnResizedEvent) {
|
||||
// Check if the resize is finished, and it's not from the API (which is triggered by us).
|
||||
if (!e.finished || e.source === 'api') {
|
||||
return
|
||||
}
|
||||
if (!e.finished || e.source === 'api') return
|
||||
// If the user manually resized (or manually autosized) a column, we don't want to auto-size it
|
||||
// on a resize.
|
||||
const manuallySized = e.source !== 'autosizeColumns' || !wasAutomaticallyAutosized.value
|
||||
wasAutomaticallyAutosized.value = false
|
||||
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 ===
|
||||
// ===============
|
||||
|
||||
onMounted(() => {
|
||||
setRowLimitAndPage(1000, 0)
|
||||
setRowLimit(1000)
|
||||
if ('AG_GRID_LICENSE_KEY' in window && typeof window.AG_GRID_LICENSE_KEY === 'string') {
|
||||
LicenseManager.setLicenseKey(window.AG_GRID_LICENSE_KEY)
|
||||
} else {
|
||||
console.warn('The AG_GRID_LICENSE_KEY is not defined.')
|
||||
}
|
||||
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>
|
||||
|
||||
<template>
|
||||
<VisualizationContainer :belowToolbar="true" :overflow="true">
|
||||
<div ref="rootNode" class="TableVisualization" @wheel.stop @pointerdown.stop>
|
||||
<div class="table-visualization-status-bar">
|
||||
<button :disabled="isFirstPage" @click="goToFirstPage">«</button>
|
||||
<button :disabled="isFirstPage" @click="goToPreviousPage">‹</button>
|
||||
<select
|
||||
v-if="isRowCountSelectorVisible"
|
||||
@change="setRowLimitAndPage(Number(($event.target as HTMLOptionElement).value), page)"
|
||||
@change="setRowLimit(Number(($event.target as HTMLOptionElement).value))"
|
||||
>
|
||||
<option
|
||||
v-for="limit in selectableRowLimits"
|
||||
@ -436,8 +399,6 @@ watch(
|
||||
<span v-else-if="isRowCountSelectorVisible" v-text="' rows.'"></span>
|
||||
<span v-else-if="rowCount === 1" v-text="'1 row.'"></span>
|
||||
<span v-else v-text="`${rowCount} rows.`"></span>
|
||||
<button :disabled="isLastPage" @click="goToNextPage">›</button>
|
||||
<button :disabled="isLastPage" @click="goToLastPage">»</button>
|
||||
</div>
|
||||
<div ref="tableNode" class="scrollable ag-theme-alpine"></div>
|
||||
</div>
|
||||
@ -466,12 +427,6 @@ watch(
|
||||
padding: 0 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-visualization-status-bar > button {
|
||||
width: 12px;
|
||||
margin: 0 2px;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
export const name = 'Warnings'
|
||||
export const icon = 'exclamation'
|
||||
export const inputType = 'Any'
|
||||
export const defaultPreprocessor = [
|
||||
'Standard.Visualization.Warnings',
|
||||
|
@ -14,14 +14,9 @@ declare module 'd3' {
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@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
|
||||
}
|
||||
import '@ag-grid-community/core'
|
||||
|
||||
declare module '@ag-grid-community/core' {
|
||||
// These type parameters are defined on the original interface.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
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 type { VisualizationIdentifier } from 'shared/yjsModel'
|
||||
import { reactive } from 'vue'
|
||||
@ -8,6 +10,7 @@ export interface VisualizationConfig {
|
||||
background?: string
|
||||
readonly types: Iterable<VisualizationIdentifier>
|
||||
readonly currentType: VisualizationIdentifier
|
||||
readonly icon: Icon | URLString | undefined
|
||||
readonly isCircularMenuVisible: boolean
|
||||
readonly nodeSize: Vec2
|
||||
width: number | null
|
||||
|
@ -20,6 +20,8 @@ import type {
|
||||
import Compiler from '@/stores/visualization/compiler?worker'
|
||||
import { assertNever } from '@/util/assert'
|
||||
import { toError } from '@/util/error'
|
||||
import iconNames from '@/util/iconList.json'
|
||||
import type { Icon } from '@/util/iconName'
|
||||
import type { Opt } from '@/util/opt'
|
||||
import { defineKeybinds } from '@/util/shortcuts'
|
||||
import { Error as DataError } from 'shared/binaryProtocol'
|
||||
@ -32,12 +34,24 @@ import { z } from 'zod'
|
||||
export const currentProjectProtocol = 'enso-current-project:'
|
||||
export const stylePathAttribute = 'data-style-path'
|
||||
|
||||
export type URLString = `${string}:${string}`
|
||||
|
||||
const VisualizationModule = z.object({
|
||||
// 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.
|
||||
default: z.custom<Visualization>(() => true),
|
||||
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(),
|
||||
defaultPreprocessor: (
|
||||
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,
|
||||
)
|
||||
|
||||
for (const { name, inputType } of builtinVisualizations) {
|
||||
for (const { name, inputType, icon } of builtinVisualizations) {
|
||||
metadata.set(toVisualizationId({ module: { kind: 'Builtin' }, name }), {
|
||||
name,
|
||||
inputType,
|
||||
icon,
|
||||
})
|
||||
}
|
||||
|
||||
@ -182,7 +183,11 @@ export const useVisualizationStore = defineStore('visualization', () => {
|
||||
id = { module: { kind: 'CurrentProject' }, name: viz.name }
|
||||
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) {
|
||||
if (key) cache.delete(key)
|
||||
if (error instanceof InvalidVisualizationModuleError) {
|
||||
@ -225,6 +230,10 @@ export const useVisualizationStore = defineStore('visualization', () => {
|
||||
for (const type of types) yield fromVisualizationId(type)
|
||||
}
|
||||
|
||||
function icon(type: VisualizationIdentifier) {
|
||||
return metadata.get(toVisualizationId(type))?.icon
|
||||
}
|
||||
|
||||
function get(meta: VisualizationIdentifier, ignoreCache = false) {
|
||||
const key = toVisualizationId(meta)
|
||||
if (!cache.get(key) || ignoreCache) {
|
||||
@ -253,5 +262,5 @@ export const useVisualizationStore = defineStore('visualization', () => {
|
||||
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 { 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>) {
|
||||
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,
|
||||
} 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.
|
||||
* @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)
|
||||
}, PointerButtonMask.Auxiliary)
|
||||
|
||||
function eventScreenPos(e: PointerEvent): Vec2 {
|
||||
function eventScreenPos(e: { clientX: number; clientY: number }): Vec2 {
|
||||
return new Vec2(e.clientX, e.clientY)
|
||||
}
|
||||
|
||||
@ -103,11 +103,51 @@ export function useNavigator(viewportNode: Ref<Element | undefined>) {
|
||||
{ capture: true },
|
||||
)
|
||||
|
||||
let isPointerDown = false
|
||||
let scrolledThisFrame = false
|
||||
const eventMousePos = ref<Vec2 | null>(null)
|
||||
let eventTargetScrollPos: Vec2 | null = null
|
||||
const sceneMousePos = computed(() =>
|
||||
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({
|
||||
events: {
|
||||
pointermove(e: PointerEvent) {
|
||||
@ -119,10 +159,12 @@ export function useNavigator(viewportNode: Ref<Element | undefined>) {
|
||||
eventMousePos.value = null
|
||||
},
|
||||
pointerup(e: PointerEvent) {
|
||||
isPointerDown = false
|
||||
panPointer.events.pointerup(e)
|
||||
zoomPointer.events.pointerup(e)
|
||||
},
|
||||
pointerdown(e: PointerEvent) {
|
||||
isPointerDown = true
|
||||
panPointer.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/styles": "^30.2.0",
|
||||
"@ag-grid-enterprise/core": "^30.2.0",
|
||||
"@ag-grid-enterprise/range-selection": "^30.2.1",
|
||||
"@babel/parser": "^7.22.16",
|
||||
"@fast-check/vitest": "^0.0.8",
|
||||
"@lezer/common": "^1.1.0",
|
||||
@ -444,9 +445,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ag-grid-community/core": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ag-grid-community/core/-/core-30.2.0.tgz",
|
||||
"integrity": "sha512-BdRavyYxyl0rx3w4VKQlV9KHRR9TIJ9RFt4FIsWUxC+VDC+VEL64J1PzgJqXqgkM7Zmb+rYbluA2UPmRrQQJ9A=="
|
||||
"version": "30.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@ag-grid-community/core/-/core-30.2.1.tgz",
|
||||
"integrity": "sha512-jGRBfRFsLwxch8GJGjbVI2FVbB+/fy1s4mm8//+kOkcPFlq4BbLFELvU4Kupwk1YkhduxUC50GTYyFzmF0rSIQ=="
|
||||
},
|
||||
"node_modules/@ag-grid-community/styles": {
|
||||
"version": "30.2.0",
|
||||
@ -454,11 +455,20 @@
|
||||
"integrity": "sha512-ipI/weI0jgvhZI/PooNkKUJlJQ21uiyLKmzeQkSOTBPxSqpszvBm5BGkeaw+WmTc6dF/dF2yRVcFz8uTcwveLA=="
|
||||
},
|
||||
"node_modules/@ag-grid-enterprise/core": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ag-grid-enterprise/core/-/core-30.2.0.tgz",
|
||||
"integrity": "sha512-0XZCBVoOCUmtOkaDr2g+r8ORbT+dV1UqA5dZMwPSC6P537RKPFFCe9YPjwGxPS06YB7VpQcnUF3Pw3TY6ibnyw==",
|
||||
"version": "30.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@ag-grid-enterprise/core/-/core-30.2.1.tgz",
|
||||
"integrity": "sha512-POVdYwMho+WPQ1wrNwQwX4ihGvdq+UbLIHQfWnnZn5c2FLFiUmlxAhCZAXXiOeXq5MDHBKqKrTRemb9TUbBRcg==",
|
||||
"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": {
|
||||
|
Loading…
Reference in New Issue
Block a user