Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table Input Widget: Add column with plus #11388

Merged
merged 11 commits into from
Oct 25, 2024
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
- [Copying and pasting in Table Editor Widget now works properly][11332]
- [Fix invisible selection in Table Input Widget][11358]
- [Enable cloud file browser in local projects][11383]
- [Changed the way of adding new column in Table Input Widget][11388]. The
"virtual column" is replaced with an explicit (+) button.

[11151]: https://github.com/enso-org/enso/pull/11151
[11271]: https://github.com/enso-org/enso/pull/11271
[11332]: https://github.com/enso-org/enso/pull/11332
[11358]: https://github.com/enso-org/enso/pull/11358
[11383]: https://github.com/enso-org/enso/pull/11383
[11388]: https://github.com/enso-org/enso/pull/11388

#### Enso Standard Library

Expand Down
11 changes: 6 additions & 5 deletions app/gui/e2e/project-view/rightPanel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@ test('Doc panel focus (regression #10471)', async ({ page }) => {
await page.keyboard.press(`${CONTROL_KEY}+D`)
await page.keyboard.press(`${CONTROL_KEY}+\``)
await expect(locate.rightDock(page)).toBeVisible()
await expect(locate.bottomDock(page)).toBeVisible()
const codeEditor = page.locator('.CodeEditor')
await expect(codeEditor).toBeVisible()

// Focus code editor.
await locate.bottomDock(page).click()
await codeEditor.click()

await page.evaluate(() => {
const codeEditor = (window as any).__codeEditorApi
const docStart = codeEditor.indexOf('The main method')
codeEditor.placeCursor(docStart + 8)
const codeEditorApi = (window as any).__codeEditorApi
const docStart = codeEditorApi.indexOf('The main method')
codeEditorApi.placeCursor(docStart + 8)
})
await page.keyboard.press('Space')
await page.keyboard.press('T')
Expand Down
1 change: 1 addition & 0 deletions app/gui/e2e/project-view/tableVisualisation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ test('Copy from Table Visualization', async ({ page, context }) => {
const node = await actions.createTableNode(page)
const widget = node.locator('.WidgetTableEditor')
await expect(widget).toBeVisible()
await widget.getByRole('button', { name: 'Add new column' }).click()
await widget.locator('.ag-cell', { hasNotText: /0/ }).first().click()
await page.keyboard.press('Control+V')
await expect(widget.locator('.ag-cell')).toHaveText([
Expand Down
29 changes: 17 additions & 12 deletions app/gui/e2e/project-view/widgets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,27 +544,32 @@ test('Table widget', async ({ page }) => {
const node = await actions.createTableNode(page)
const widget = node.locator('.WidgetTableEditor')
await expect(widget).toBeVisible()
await expect(widget.locator('.ag-header-cell-text')).toHaveText(['#', 'New Column'])
await expect(widget.locator('.ag-header-cell-text', { hasText: 'New Column' })).toHaveClass(
/(?<=^| )virtualColumn(?=$| )/,
)
// There are two cells, one with row number, second allowing creating first row and column
await expect(widget.locator('.ag-cell')).toHaveCount(2)
await expect(widget.locator('.ag-header-cell-text')).toHaveText(['#'])
await expect(widget.getByRole('button', { name: 'Add new column' })).toExist()
await expect(widget.locator('.ag-cell')).toHaveText(['0', ''])

// Create first column
await widget.getByRole('button', { name: 'Add new column' }).click()
await expect(widget.locator('.ag-header-cell-text')).toHaveText(['#', 'Column #1'])
await expect(widget.locator('.ag-cell')).toHaveText(['0', '', ''])

// Putting first value
await widget.locator('.ag-cell', { hasNotText: '0' }).click()
await widget.locator('.ag-cell', { hasNotText: '0' }).first().click()
await page.keyboard.type('Value')
await page.keyboard.press('Enter')
// There will be new blank column and new blank row allowing adding new columns and rows
// (so 4 cells in total)
await expect(widget.locator('.ag-header-cell-text')).toHaveText(['#', 'Column #0', 'New Column'])
// There will be new blank row allowing adding new rows.
await expect(widget.locator('.ag-cell')).toHaveText(['0', 'Value', '', '1', '', ''])

// Renaming column
await widget.locator('.ag-header-cell-text', { hasText: 'Column #0' }).first().click()
await widget.locator('.ag-header-cell-text', { hasText: 'Column #1' }).first().click()
await page.keyboard.type('Header')
await page.keyboard.press('Enter')
await expect(widget.locator('.ag-header-cell-text')).toHaveText(['#', 'Header', 'New Column'])
await expect(widget.locator('.ag-header-cell-text')).toHaveText(['#', 'Header'])

// Adding next column
await widget.getByRole('button', { name: 'Add new column' }).click()
await expect(widget.locator('.ag-header-cell-text')).toHaveText(['#', 'Header', 'Column #2'])
await expect(widget.locator('.ag-cell')).toHaveText(['0', 'Value', '', '', '1', '', '', ''])

// Switching edit between cells and headers - check we will never edit two things at once.
await expect(widget.locator('.ag-text-field-input')).toHaveCount(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import ResizeHandles from '@/components/ResizeHandles.vue'
import AgGridTableView from '@/components/shared/AgGridTableView.vue'
import { injectGraphNavigator } from '@/providers/graphNavigator'
import { useTooltipRegistry } from '@/providers/tooltipState'
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
import { WidgetEditHandler } from '@/providers/widgetRegistry/editHandler'
import { useGraphStore } from '@/stores/graph'
Expand All @@ -20,12 +21,13 @@ import '@ag-grid-community/styles/ag-theme-alpine.css'
import type {
CellEditingStartedEvent,
CellEditingStoppedEvent,
ColDef,
Column,
ColumnMovedEvent,
ProcessDataFromClipboardParams,
RowDragEndEvent,
} from 'ag-grid-enterprise'
import { computed, ref } from 'vue'
import { computed, markRaw, ref } from 'vue'
import type { ComponentExposed } from 'vue-component-type-helpers'

const props = defineProps(widgetProps(widgetDefinition))
Expand Down Expand Up @@ -178,14 +180,22 @@ function processDataFromClipboard({ data, api }: ProcessDataFromClipboardParams<

// === Column Default Definition ===

const defaultColDef = {
const tooltipRegistry = useTooltipRegistry()
const defaultColDef: ColDef<RowData> = {
editable: true,
resizable: true,
sortable: false,
lockPinned: true,
menuTabs: ['generalMenuTab'],
headerComponentParams: {
onHeaderEditingStarted: headerEditHandler.headerEditedInGrid.bind(headerEditHandler),
onHeaderEditingStopped: headerEditHandler.headerEditingStoppedInGrid.bind(headerEditHandler),
// TODO[ao]: we mark raw, because otherwise any change _inside_ tooltipRegistry causes the grid
// to be refreshed. Technically, shallowReactive should work here, but it does not,
// I don't know why
tooltipRegistry: markRaw(tooltipRegistry),
editHandlers: {
onHeaderEditingStarted: headerEditHandler.headerEditedInGrid.bind(headerEditHandler),
onHeaderEditingStopped: headerEditHandler.headerEditingStoppedInGrid.bind(headerEditHandler),
},
},
}
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
<script lang="ts">
import SvgButton from '@/components/SvgButton.vue'
import { provideTooltipRegistry, TooltipRegistry } from '@/providers/tooltipState'
import type { IHeaderParams } from 'ag-grid-community'
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'

export interface HeaderEditHandlers {
/** Setter called when column name is changed by the user. */
nameSetter: (newName: string) => void
onHeaderEditingStarted?: (stop: (cancel: boolean) => void) => void
onHeaderEditingStopped?: () => void
}

/**
* A subset of {@link HeaderParams} which is meant to be specified for columns separately
* (not in defaultColumnDef).
*/
export type ColumnSpecificHeaderParams =
| {
type: 'astColumn'
editHandlers: HeaderEditHandlers
}
| { type: 'newColumn'; newColumnRequested: () => void }
| { type: 'rowIndexColumn' }

/**
* Parameters recognized by this header component.
*
* They are set through `headerComponentParams` option in AGGrid column definition.
*/
export interface HeaderParams {
/** Setter called when column name is changed by the user. */
nameSetter?: (newName: string) => void
export type HeaderParams = ColumnSpecificHeaderParams & {
/**
* Column is virtual if it is not represented in the AST. Such column might be used
* to create new one.
* AgGrid mounts header components as separate "App", so we don't have access to any context.
* Threfore the tooltip registry must be provided by props.
*/
virtualColumn?: boolean
onHeaderEditingStarted?: (stop: (cancel: boolean) => void) => void
onHeaderEditingStopped?: () => void
tooltipRegistry: TooltipRegistry
}
</script>

Expand All @@ -25,17 +42,23 @@ const props = defineProps<{
params: IHeaderParams & HeaderParams
}>()

/** Re-provide tooltipRegistry. See `tooltipRegistry` docs in {@link HeaderParams} */
provideTooltipRegistry.provideConstructed(props.params.tooltipRegistry)

const editing = ref(false)
const inputElement = ref<HTMLInputElement>()
const editHandlers = computed(() =>
props.params.type === 'astColumn' ? props.params.editHandlers : undefined,
)

watch(editing, (newVal) => {
if (newVal) {
props.params.onHeaderEditingStarted?.((cancel: boolean) => {
editHandlers.value?.onHeaderEditingStarted?.((cancel: boolean) => {
if (cancel) editing.value = false
else acceptNewName()
})
} else {
props.params.onHeaderEditingStopped?.()
editHandlers.value?.onHeaderEditingStopped?.()
}
})

Expand All @@ -48,33 +71,47 @@ watch(inputElement, (newVal, oldVal) => {
})

function acceptNewName() {
if (editHandlers.value == null) {
console.error("Tried to accept header new name where it's not editable!")
return
}
if (inputElement.value == null) {
console.error('Tried to accept header new name without input element!')
return
}
props.params.nameSetter?.(inputElement.value.value)
editHandlers.value.nameSetter(inputElement.value.value)
editing.value = false
}

function onMouseClick() {
if (!editing.value && props.params.nameSetter != null) {
function onMouseClick(event: MouseEvent) {
if (!editing.value && props.params.type === 'astColumn') {
editing.value = true
event.stopPropagation()
}
}

function onMouseRightClick(event: MouseEvent) {
if (!editing.value) {
props.params.showColumnMenuAfterMouseClick(event)
event.preventDefault()
event.stopPropagation()
}
}
</script>

<template>
<SvgButton
v-if="params.type === 'newColumn'"
class="addColumnButton"
name="add"
title="Add new column"
@click.stop="params.newColumnRequested()"
/>
<div
v-else
class="ag-cell-label-container"
role="presentation"
@pointerdown.stop
@click.stop
@click="onMouseClick"
@click.right="onMouseRightClick"
>
Expand All @@ -93,14 +130,18 @@ function onMouseRightClick(event: MouseEvent) {
<span
v-else
class="ag-header-cell-text"
:class="{ virtualColumn: params.virtualColumn === true }"
:class="{ virtualColumn: params.type !== 'astColumn' }"
>{{ params.displayName }}</span
>
</div>
</div>
</template>

<style>
<style scoped>
.addColumnButton {
margin-left: 10px;
}

.virtualColumn {
color: rgba(0, 0, 0, 0.5);
}
Expand Down
Loading
Loading