Skip to content

Commit

Permalink
Table widget: reordering rows and columns (#11271)
Browse files Browse the repository at this point in the history
Fixes #10759

[Screencast From 2024-10-14 13-51-24.webm](https://github.com/user-attachments/assets/372c6f1b-8281-4bdc-9f46-fa90c3e32764)

# Important Notes
In this task, I also fixed some badly looking spacing after editing vector.
  • Loading branch information
farmaazon authored Oct 17, 2024
1 parent d15a585 commit a45e233
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 72 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

- [Rows and Columns may be now removed in Table Input Widget][11151]. The option
is available in right-click context menu.
- [Rows and Columns may be now reordered by dragging in Table Input
Widget][11271]

[11151]: https://github.com/enso-org/enso/pull/11151
[11271]: https://github.com/enso-org/enso/pull/11271

#### Enso Standard Library

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import { Rect } from '@/util/data/rect'
import { Vec2 } from '@/util/data/vec2'
import '@ag-grid-community/styles/ag-grid.css'
import '@ag-grid-community/styles/ag-theme-alpine.css'
import type { CellEditingStartedEvent, CellEditingStoppedEvent, Column } from 'ag-grid-enterprise'
import type {
CellEditingStartedEvent,
CellEditingStoppedEvent,
Column,
ColumnMovedEvent,
RowDragEndEvent,
} from 'ag-grid-enterprise'
import { computed, ref } from 'vue'
import type { ComponentExposed } from 'vue-component-type-helpers'
Expand All @@ -26,7 +32,7 @@ const graph = useGraphStore()
const suggestionDb = useSuggestionDbStore()
const grid = ref<ComponentExposed<typeof AgGridTableView<RowData, any>>>()
const { rowData, columnDefs } = useTableNewArgument(
const { rowData, columnDefs, moveColumn, moveRow } = useTableNewArgument(
() => props.input,
graph,
suggestionDb.entries,
Expand Down Expand Up @@ -141,11 +147,27 @@ const widgetStyle = computed(() => {
}
})
// === Column and Row Dragging ===
function onColumnMoved(event: ColumnMovedEvent<RowData>) {
if (event.column && event.toIndex != null && event.finished) {
moveColumn(event.column.getColId(), event.toIndex)
}
}
function onRowDragEnd(event: RowDragEndEvent<RowData>) {
if (event.node.data != null) {
moveRow(event.node.data?.index, event.overIndex)
}
}
// === Column Default Definition ===
const defaultColDef = {
editable: true,
resizable: true,
sortable: false,
lockPinned: true,
headerComponentParams: {
onHeaderEditingStarted: headerEditHandler.headerEditedInGrid.bind(headerEditHandler),
onHeaderEditingStopped: headerEditHandler.headerEditingStoppedInGrid.bind(headerEditHandler),
Expand Down Expand Up @@ -184,6 +206,8 @@ export const widgetDefinition = defineWidget(
:components="{ agColumnHeader: TableHeader }"
:singleClickEdit="true"
:stopEditingWhenCellsLoseFocus="true"
:suppressDragLeaveHidesColumns="true"
:suppressMoveWhenColumnDragging="true"
@keydown.enter.stop
@keydown.arrow-left.stop
@keydown.arrow-right.stop
Expand All @@ -196,6 +220,8 @@ export const widgetDefinition = defineWidget(
@rowDataUpdated="cellEditHandler.rowDataChanged()"
@pointerdown.stop
@click.stop
@columnMoved="onColumnMoved"
@rowDragEnd="onRowDragEnd"
/>
</Suspense>
<ResizeHandles v-model="clientBounds" bottom right />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,35 @@ test.each([
expect(tableNewCallMayBeHandled(ast)).toBeFalsy()
})

function tableEditFixture(code: string, expectedCode: string) {
const ast = Ast.parseBlock(code)
const inputAst = [...ast.statements()][0]
assert(inputAst != null)
const input = WidgetInput.FromAst(inputAst)
const startEdit = vi.fn(() => ast.module.edit())
const onUpdate = vi.fn((update) => {
const inputAst = [...update.edit.getVersion(ast).statements()][0]
expect(inputAst?.code()).toBe(expectedCode)
})
const addMissingImports = vi.fn((_, imports) => {
// the only import we're going to add is Nothing.
expect(imports).toEqual([
{
kind: 'Unqualified',
from: 'Standard.Base.Nothing',
import: 'Nothing',
},
])
})
const tableNewArgs = useTableNewArgument(
input,
{ startEdit, addMissingImports },
suggestionDbWithNothing(),
onUpdate,
)
return { tableNewArgs, startEdit, onUpdate, addMissingImports }
}

test.each([
{
code: "Table.new [['a', [1, 2, 3]], ['b', [4, 5, 6]]]",
Expand Down Expand Up @@ -211,29 +240,7 @@ test.each([
expected: "Table.new [['a', [1, 5]], ['a', [3, 4]]]",
},
])('Editing table $code: $description', ({ code, edit, expected, importExpected }) => {
const ast = Ast.parseBlock(code)
const inputAst = [...ast.statements()][0]
assert(inputAst != null)
const input = WidgetInput.FromAst(inputAst)
const onUpdate = vi.fn((update) => {
const inputAst = [...update.edit.getVersion(ast).statements()][0]
expect(inputAst?.code()).toBe(expected)
})
const addMissingImports = vi.fn((_, imports) => {
expect(imports).toEqual([
{
kind: 'Unqualified',
from: 'Standard.Base.Nothing',
import: 'Nothing',
},
])
})
const tableNewArgs = useTableNewArgument(
input,
{ startEdit: () => ast.module.edit(), addMissingImports },
suggestionDbWithNothing(),
onUpdate,
)
const { tableNewArgs, onUpdate, addMissingImports } = tableEditFixture(code, expected)
const editedRow = tableNewArgs.rowData.value[edit.row]
assert(editedRow != null)
tableNewArgs.columnDefs.value[edit.column]?.valueSetter?.({
Expand Down Expand Up @@ -262,7 +269,7 @@ test.each([
{
code: "Table.new [['a', [1, 2, 3]], ['b', [4, 5, 6]]]",
removedRowIndex: 0,
expected: "Table.new [['a', [ 2, 3]], ['b', [ 5, 6]]]",
expected: "Table.new [['a', [2, 3]], ['b', [5, 6]]]",
},
{
code: "Table.new [['a', [1, 2, 3]], ['b', [4, 5, 6]]]",
Expand All @@ -280,21 +287,8 @@ test.each([
expected: "Table.new [['a', []], ['b', []]]",
},
])('Removing $removedRowIndex row in $code', ({ code, removedRowIndex, expected }) => {
const ast = Ast.parseBlock(code)
const inputAst = [...ast.statements()][0]
assert(inputAst != null)
const addMissingImports = vi.fn()
const onUpdate = vi.fn((update) => {
const inputAst = [...update.edit.getVersion(ast).statements()][0]
expect(inputAst?.code()).toBe(expected)
})
const input = WidgetInput.FromAst(inputAst)
const tableNewArgs = useTableNewArgument(
input,
{ startEdit: () => ast.module.edit(), addMissingImports },
new SuggestionDb(),
onUpdate,
)
const { tableNewArgs, onUpdate, addMissingImports } = tableEditFixture(code, expected)

const removedRow = tableNewArgs.rowData.value[removedRowIndex]
assert(removedRow != null)
// Context menu of all cells in given row should work (even the "virtual" columns).
Expand All @@ -312,7 +306,7 @@ test.each([
{
code: "Table.new [['a', [1, 2]], ['b', [3, 4]], ['c', [5, 6]]]",
removedColIndex: 1,
expected: "Table.new [ ['b', [3, 4]], ['c', [5, 6]]]",
expected: "Table.new [['b', [3, 4]], ['c', [5, 6]]]",
},
{
code: "Table.new [['a', [1, 2]], ['b', [3, 4]], ['c', [5, 6]]]",
Expand All @@ -330,21 +324,7 @@ test.each([
expected: 'Table.new []',
},
])('Removing $removedColIndex column in $code', ({ code, removedColIndex, expected }) => {
const ast = Ast.parseBlock(code)
const inputAst = [...ast.statements()][0]
assert(inputAst != null)
const addMissingImports = vi.fn()
const onUpdate = vi.fn((update) => {
const inputAst = [...update.edit.getVersion(ast).statements()][0]
expect(inputAst?.code()).toBe(expected)
})
const input = WidgetInput.FromAst(inputAst)
const tableNewArgs = useTableNewArgument(
input,
{ startEdit: () => ast.module.edit(), addMissingImports },
new SuggestionDb(),
onUpdate,
)
const { tableNewArgs, onUpdate, addMissingImports } = tableEditFixture(code, expected)
const removedCol = tableNewArgs.columnDefs.value[removedColIndex]
assert(removedCol != null)
const removeAction = getCustomMenuItemByName('Remove Column', removedCol.mainMenuItems)
Expand All @@ -359,3 +339,56 @@ test.each([

expect(addMissingImports).not.toHaveBeenCalled()
})

test.each([
{
code: "Table.new [['a', [1, 2]], ['b', [3, 4]], ['c', [5, 6]]]",
fromIndex: 1,
toIndex: 3,
expected: "Table.new [['b', [3, 4]], ['c', [5, 6]], ['a', [1, 2]]]",
},
{
code: "Table.new [['a', [1, 2]], ['b', [3, 4]], ['c', [5, 6]]]",
fromIndex: 3,
toIndex: 2,
expected: "Table.new [['a', [1, 2]], ['c', [5, 6]], ['b', [3, 4]]]",
},
])(
'Moving column $fromIndex to $toIndex in table $code',
({ code, fromIndex, toIndex, expected }) => {
const { tableNewArgs, onUpdate, addMissingImports } = tableEditFixture(code, expected)
const movedColumnDef = tableNewArgs.columnDefs.value[fromIndex]
assert(movedColumnDef?.colId != null)
tableNewArgs.moveColumn(movedColumnDef.colId, toIndex)
expect(onUpdate).toHaveBeenCalledOnce()
expect(addMissingImports).not.toHaveBeenCalled()
},
)

test.each([
{
code: "Table.new [['a', [1, 2, 3]], ['b', [4, 5, 6]]]",
fromIndex: 1,
toIndex: 2,
expected: "Table.new [['a', [1, 3, 2]], ['b', [4, 6, 5]]]",
},
{
code: "Table.new [['a', [1, 2, 3]], ['b', [4, 5, 6]]]",
fromIndex: 2,
toIndex: 0,
expected: "Table.new [['a', [3, 1, 2]], ['b', [6, 4, 5]]]",
},
{
code: "Table.new [['a', [1, 2, 3]], ['b', [4, 5, 6]]]",
fromIndex: 1,
toIndex: -1,
expected: "Table.new [['a', [1, 2, 3]], ['b', [4, 5, 6]]]",
},
])('Moving row $fromIndex to $toIndex in table $code', ({ code, fromIndex, toIndex, expected }) => {
const { tableNewArgs, onUpdate, addMissingImports } = tableEditFixture(code, expected)
tableNewArgs.moveRow(fromIndex, toIndex)
if (code !== expected) {
expect(onUpdate).toHaveBeenCalledOnce()
}
expect(addMissingImports).not.toHaveBeenCalled()
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { SuggestionDb } from '@/stores/suggestionDatabase'
import { assert } from '@/util/assert'
import { Ast } from '@/util/ast'
import { tryEnsoToNumber, tryNumberToEnso } from '@/util/ast/abstract'
import * as iterable from '@/util/data/iterable'
import { Err, Ok, transposeResult, unwrapOrWithLog, type Result } from '@/util/data/result'
import { qnLastSegment, type QualifiedName } from '@/util/qualifiedName'
import type { ToValue } from '@/util/reactivity'
Expand Down Expand Up @@ -40,6 +41,7 @@ export interface ColumnDef extends ColDef<RowData> {
valueSetter?: ({ data, newValue }: { data: RowData; newValue: any }) => boolean
mainMenuItems: (string | MenuItem)[]
contextMenuItems: (string | MenuItem)[]
rowDrag?: ({ data }: { data: RowData | undefined }) => boolean
}

namespace cellValueConversion {
Expand Down Expand Up @@ -281,6 +283,7 @@ export function useTableNewArgument(
},
mainMenuItems: ['autoSizeThis', 'autoSizeAll'],
contextMenuItems: ['paste', 'separator', removeRowMenuItem],
lockPosition: 'right',
}))

const rowIndexColumnDef = computed<ColumnDef>(() => ({
Expand All @@ -291,6 +294,8 @@ export function useTableNewArgument(
mainMenuItems: ['autoSizeThis', 'autoSizeAll'],
contextMenuItems: [removeRowMenuItem],
cellStyle: { color: 'rgba(0, 0, 0, 0.4)' },
lockPosition: 'left',
rowDrag: ({ data }) => data?.index != null && data.index < rowCount.value,
}))

const columnDefs = computed(() => {
Expand Down Expand Up @@ -382,5 +387,63 @@ export function useTableNewArgument(
return ast
}

return { columnDefs, rowData }
function moveColumn(colId: string, toIndex: number) {
if (!columnsAst.value.ok) {
columnsAst.value.error.log('Cannot reorder columns: The table AST is not available')
return
}
if (!columnsAst.value.value) {
console.error('Cannot reorder columns on placeholders! This should not be possible in the UI')
return
}
const edit = graph.startEdit()
const columns = edit.getVersion(columnsAst.value.value)
const fromIndex = iterable.find(columns.enumerate(), ([, ast]) => ast?.id === colId)?.[0]
if (fromIndex != null) {
columns.move(fromIndex, toIndex - 1)
onUpdate({ edit })
}
}

function moveRow(rowIndex: number, overIndex: number) {
if (!columnsAst.value.ok) {
columnsAst.value.error.log('Cannot reorder rows: The table AST is not available')
return
}
if (!columnsAst.value.value) {
console.error('Cannot reorder rows on placeholders! This should not be possible in the UI')
return
}
// If dragged out of grid, we do nothing.
if (overIndex === -1) return
const edit = graph.startEdit()
for (const col of columns.value) {
const editedCol = edit.getVersion(col.data)
editedCol.move(rowIndex, overIndex)
}
onUpdate({ edit })
}

return {
/** All column definitions, to be passed to AgGrid component. */
columnDefs,
/**
* Row Data, to be passed to AgGrid component. They do not contain values, but AstIds.
* The column definitions have proper getters for obtaining value from AST.
*/
rowData,
/**
* Move column in AST. Do not change colunDefs, they are updated upon expected widgetInput change.
* @param colId the id of moved column (as got from `getColId()`)
* @param toIndex the new index of column as in view (counting in the row index column).
*/
moveColumn,
/**
* Move row in AST. Do not change rowData, its updated upon expected widgetInput change.
* @param rowIndex the index of moved row.
* @param overIndex the index of row over which this row was dropped, as in RowDragEndEvent's
* `overIndex` (the -1 case is handled)
*/
moveRow,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const _props = defineProps<{
components?: Record<string, unknown>
singleClickEdit?: boolean
stopEditingWhenCellsLoseFocus?: boolean
suppressDragLeaveHidesColumns?: boolean
suppressMoveWhenColumnDragging?: boolean
textFormatOption?: TextFormatOptions
}>()
const emit = defineEmits<{
Expand Down Expand Up @@ -190,6 +192,8 @@ const { AgGridVue } = await import('ag-grid-vue3')
:components="components"
:singleClickEdit="singleClickEdit"
:stopEditingWhenCellsLoseFocus="stopEditingWhenCellsLoseFocus"
:suppressDragLeaveHidesColumns="suppressDragLeaveHidesColumns"
:suppressMoveWhenColumnDragging="suppressMoveWhenColumnDragging"
@gridReady="onGridReady"
@firstDataRendered="updateColumnWidths"
@rowDataUpdated="updateColumnWidths($event), emit('rowDataUpdated', $event)"
Expand Down
Loading

0 comments on commit a45e233

Please sign in to comment.