Skip to content

Commit 71c5958

Browse files
authored
Merge pull request #5075 from mathesar-foundation/4921_resize_columns
Allow multiple columns to be resized together
2 parents 9769e35 + 58ce552 commit 71c5958

File tree

10 files changed

+118
-78
lines changed

10 files changed

+118
-78
lines changed

mathesar_ui/src/components/sheet/Sheet.svelte

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
<script lang="ts">
22
import { onMount, tick } from 'svelte';
3-
import { writable } from 'svelte/store';
3+
import { derived, readable, writable } from 'svelte/store';
44
55
import {
66
type ClipboardHandler,
77
getClipboardHandlerStoreFromContext,
88
} from '@mathesar/stores/clipboard';
99
import { getModifierKeyCombo } from '@mathesar/utils/pointerUtils';
1010
import type { ClientPosition } from '@mathesar-component-library';
11-
import { ImmutableMap } from '@mathesar-component-library/types';
11+
import {
12+
ImmutableMap,
13+
ImmutableSet,
14+
defined,
15+
} from '@mathesar-component-library/types';
1216
1317
import {
1418
type SheetCellDetails,
@@ -51,6 +55,11 @@
5155
5256
export let sheetElement: HTMLElement | undefined = undefined;
5357
58+
/** [columnId, width] pairs to persist to the database. */
59+
export let persistColumnWidths: (
60+
widthsMap: [string, number | null][],
61+
) => void = () => {};
62+
5463
interface SheetContextMenuCallbackArgs {
5564
targetCell: SheetCellDetails;
5665
position: ClientPosition;
@@ -61,6 +70,25 @@
6170
| ((p: SheetContextMenuCallbackArgs) => 'opened' | 'empty')
6271
| undefined = undefined;
6372
73+
$: fullySelectedColumnIds =
74+
defined(selection, (s) => derived(s, (v) => v.fullySelectedColumnIds)) ??
75+
readable(new ImmutableSet<string>());
76+
77+
/**
78+
* Returns a mapping of column ids to widths so that the user can resize
79+
* multiple columns via a single column resizer. If the given column id is
80+
* part of the selection, this returns a mapping for all selected columns.
81+
* Otherwise, it just uses the given column id.
82+
*/
83+
function getSmartColumnWidthsMap(
84+
columnId: string,
85+
width: number | null,
86+
): [string, number | null][] {
87+
return $fullySelectedColumnIds.has(columnId)
88+
? [...$fullySelectedColumnIds].map((id) => [id, width])
89+
: [[columnId, width]];
90+
}
91+
6492
$: ({ columnStyleMap, rowWidth } = calculateColumnStyleMapAndRowWidth(
6593
columns,
6694
getColumnIdentifier,
@@ -89,11 +117,18 @@
89117
stores,
90118
api: {
91119
getColumnWidth: (id) => normalizeColumnWidth(columnWidths.get(id)),
92-
setColumnWidth: (key, width) => {
93-
columnWidths = columnWidths.with(key, width);
120+
handleDraggingColumnWidth: (id, width) => {
121+
const widthsMap = getSmartColumnWidthsMap(id, width);
122+
columnWidths = widthsMap.reduce(
123+
(newColumnWidths, [columnId, newWidth]) =>
124+
newWidth === null
125+
? newColumnWidths.without(columnId)
126+
: newColumnWidths.with(columnId, newWidth),
127+
columnWidths,
128+
);
94129
},
95-
resetColumnWidth: (key) => {
96-
columnWidths = columnWidths.without(key);
130+
handleReleaseColumnWidth: (id, width) => {
131+
persistColumnWidths(getSmartColumnWidthsMap(id, width));
97132
},
98133
setHorizontalScrollOffset: (offset) => {
99134
horizontalScrollOffset = offset;

mathesar_ui/src/components/sheet/SheetCellResizer.svelte

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,7 @@
88
99
export let minColumnWidth = MIN_COLUMN_WIDTH_PX;
1010
export let maxColumnWidth = MAX_COLUMN_WIDTH_PX;
11-
export let columnIdentifierKey: string;
12-
13-
/**
14-
* Runs once — when the resize is finished. Does NOT run in realtime on each
15-
* pixel movement.
16-
*/
17-
export let afterResize: (width: number) => void = () => {};
18-
19-
/**
20-
* Runs after the user double-clicks the resizer (to reset the column width).
21-
*/
22-
export let onReset: () => void = () => {};
11+
export let columnId: string;
2312
2413
let startingWidth: number | undefined = undefined;
2514
@@ -31,23 +20,22 @@
3120
class:selection-in-progress={$selectionInProgress}
3221
class:is-resizing={!!startingWidth}
3322
use:slider={{
34-
getStartingValue: () => api.getColumnWidth(columnIdentifierKey) ?? 0,
35-
onMove: (width) => api.setColumnWidth(columnIdentifierKey, width),
23+
getStartingValue: () => api.getColumnWidth(columnId) ?? 0,
24+
onMove: (width) => api.handleDraggingColumnWidth(columnId, width),
3625
onStart: (startingValue) => {
3726
startingWidth = startingValue;
3827
},
3928
onStop: (width) => {
4029
if (width !== startingWidth) {
41-
afterResize(width);
30+
api.handleReleaseColumnWidth(columnId, width);
4231
}
4332
startingWidth = undefined;
4433
},
4534
min: minColumnWidth,
4635
max: maxColumnWidth,
4736
}}
4837
on:dblclick={() => {
49-
api.resetColumnWidth(columnIdentifierKey);
50-
onReset();
38+
api.handleReleaseColumnWidth(columnId, null);
5139
}}
5240
>
5341
<div class="indicator" />

mathesar_ui/src/components/sheet/utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ export interface SheetContextStores {
2626
export interface SheetContext {
2727
stores: SheetContextStores;
2828
api: {
29-
setColumnWidth: (columnIdentifierKey: string, width: number) => void;
30-
getColumnWidth: (columnIdentifierKey: string) => number;
31-
resetColumnWidth: (columnIdentifierKey: string) => void;
29+
getColumnWidth: (columnId: string) => number;
30+
/** Called continuously — while the user is resizing a column. */
31+
handleDraggingColumnWidth: (columnId: string, width: number | null) => void;
32+
/** Called once — when the user is finished resizing a column */
33+
handleReleaseColumnWidth: (columnId: string, width: number | null) => void;
3234
setHorizontalScrollOffset: (offset: number) => void;
3335
setScrollOffset: (offset: number) => void;
3436
};

mathesar_ui/src/pages/import/preview/ImportPreviewSheet.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
{renamedIdColumn}
5252
/>
5353
<SheetCellResizer
54-
columnIdentifierKey={String(column.id)}
54+
columnId={String(column.id)}
5555
minColumnWidth={MIN_IMPORT_COLUMN_WIDTH_PX}
5656
/>
5757
</SheetColumnHeaderCell>

mathesar_ui/src/stores/table-data/columns.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
} from '@mathesar/api/rpc/columns';
1212
import type { Database } from '@mathesar/models/Database';
1313
import type { Table } from '@mathesar/models/Table';
14+
import { batchRun } from '@mathesar/packages/json-rpc-client-builder';
1415
import { getErrorMessage } from '@mathesar/utils/errors';
1516
import {
1617
type CancellablePromise,
@@ -162,25 +163,29 @@ export class ColumnsDataStore extends EventHandler<{
162163
}
163164

164165
async setDisplayOptions(
165-
column: Pick<RawColumnWithMetadata, 'id'>,
166-
displayOptions: ColumnMetadata | null,
166+
/** Key is column id, value is display options */
167+
changes: Map<number, ColumnMetadata | null>,
167168
): Promise<void> {
168-
await api.columns.metadata
169-
.set({
170-
...this.apiContext,
171-
column_meta_data_list: [{ attnum: column.id, ...displayOptions }],
172-
})
173-
.run();
169+
if (!changes.size) {
170+
return;
171+
}
172+
173+
const { apiContext } = this;
174+
function* getApiRequests() {
175+
for (const [columnId, displayOptions] of changes.entries()) {
176+
yield api.columns.metadata.set({
177+
...apiContext,
178+
column_meta_data_list: [{ attnum: columnId, ...displayOptions }],
179+
});
180+
}
181+
}
182+
await batchRun([...getApiRequests()]);
174183

175184
this.fetchedColumns.update((columns) =>
176-
columns.map((c) =>
177-
c.id === column.id
178-
? {
179-
...c,
180-
metadata: { ...c.metadata, ...displayOptions },
181-
}
182-
: c,
183-
),
185+
columns.map((column) => {
186+
const metadata = changes.get(column.id);
187+
return metadata === undefined ? column : { ...column, metadata };
188+
}),
184189
);
185190
}
186191

mathesar_ui/src/systems/data-explorer/result-pane/ExplorationResults.svelte

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,25 @@
7070
!rows.length;
7171
$: sheetItemCount = showDummyGhostRow ? 1 : rows.length;
7272
73-
function setColumnWidth(index: number, width: number | null) {
74-
if (queryHandler instanceof QueryManager) {
75-
void queryHandler.setColumnDisplayOptions(index, {
76-
display_width: width,
77-
});
73+
async function persistColumnWidths(widthsMap: [string, number | null][]) {
74+
for (const [columnId, width] of widthsMap) {
75+
if (queryHandler instanceof QueryManager) {
76+
const index = columnList.findIndex((c) => c.id === columnId);
77+
if (index === -1) return;
78+
// This might seem fishy having await inside a loop, but I think it's
79+
// okay in this case. It seems to me that the only reason this
80+
// `setColumnDisplayOptions` method is async is because of all the
81+
// conditional logic happening inside `QueryManager.update` which
82+
// _sometimes_ run network requests. Setting column display options
83+
// seems to happen synchronously. There don't appear to be any perf
84+
// issues with awaiting in a loop here. I didn't want to run these in
85+
// parallel because I didn't want to risk race conditions.
86+
//
87+
// eslint-disable-next-line no-await-in-loop
88+
await queryHandler.setColumnDisplayOptions(index, {
89+
display_width: width,
90+
});
91+
}
7892
}
7993
}
8094
</script>
@@ -101,15 +115,15 @@
101115
inspector.activate('column');
102116
}
103117
}}
118+
{persistColumnWidths}
104119
>
105120
<SheetHeader>
106121
<SheetOriginCell columnIdentifierKey={ID_ROW_CONTROL_COLUMN} />
107-
{#each columnList as processedQueryColumn, i (processedQueryColumn.id)}
122+
{#each columnList as processedQueryColumn (processedQueryColumn.id)}
108123
<ResultHeaderCell
109124
{processedQueryColumn}
110125
queryRunner={queryHandler}
111126
isSelected={columnIds.has(processedQueryColumn.id)}
112-
setColumnWidth={(width) => setColumnWidth(i, width)}
113127
/>
114128
{/each}
115129
</SheetHeader>

mathesar_ui/src/systems/data-explorer/result-pane/ResultHeaderCell.svelte

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
export let queryRunner: QueryRunner;
1313
export let processedQueryColumn: ProcessedQueryOutputColumn;
1414
export let isSelected: boolean;
15-
export let setColumnWidth: (width: number | null) => void;
1615
1716
$: ({ runState } = queryRunner);
1817
$: columnRunState = $runState?.state;
@@ -38,9 +37,5 @@
3837
}}
3938
/>
4039
</Button>
41-
<SheetCellResizer
42-
columnIdentifierKey={processedQueryColumn.id}
43-
afterResize={(width) => setColumnWidth(width)}
44-
onReset={() => setColumnWidth(null)}
45-
/>
40+
<SheetCellResizer columnId={processedQueryColumn.id} />
4641
</SheetColumnHeaderCell>

mathesar_ui/src/systems/table-view/TableView.svelte

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { get } from 'svelte/store';
55
import { _ } from 'svelte-i18n';
66
7+
import type { ColumnMetadata } from '@mathesar/api/rpc/_common/columnDisplayOptions';
78
import { ImmutableMap, Spinner } from '@mathesar/component-library';
89
import { Sheet } from '@mathesar/components/sheet';
910
import { SheetClipboardHandler } from '@mathesar/components/sheet/clipboard';
@@ -19,6 +20,7 @@
1920
ID_ADD_NEW_COLUMN,
2021
ID_ROW_CONTROL_COLUMN,
2122
getTabularDataStoreFromContext,
23+
isJoinedColumn,
2224
} from '@mathesar/stores/table-data';
2325
import { toast } from '@mathesar/stores/toast';
2426
import { modalRecordViewContext } from '@mathesar/systems/record-view-modal/modalRecordViewContext';
@@ -58,6 +60,8 @@
5860
isLoading,
5961
selection,
6062
recordsData,
63+
allColumns,
64+
columnsDataStore,
6165
} = $tabularData);
6266
$: $tabularData, (tableInspectorTab = 'table');
6367
$: clipboardHandler = new SheetClipboardHandler({
@@ -121,6 +125,19 @@
121125
...[...$joinedColumns.keys()].map((id): [string, number] => [id, 300]),
122126
]);
123127
$: showTableInspector = $tableInspectorVisible && supportsTableInspector;
128+
129+
function persistColumnWidths(widthsMap: [string, number | null][]): void {
130+
function* getChanges(): Generator<[number, ColumnMetadata | null]> {
131+
for (const [columnId, width] of widthsMap) {
132+
const column = $allColumns.get(columnId);
133+
if (!column) continue;
134+
// Joined columns do not persist width to the database
135+
if (isJoinedColumn(column)) continue;
136+
yield [parseInt(column.id, 10), { display_width: width }];
137+
}
138+
}
139+
void columnsDataStore.setDisplayOptions(new Map(getChanges()));
140+
}
124141
</script>
125142

126143
<div class="table-view">
@@ -137,6 +154,7 @@
137154
{columnWidths}
138155
{selection}
139156
{usesVirtualList}
157+
{persistColumnWidths}
140158
onCellSelectionStart={(cell) => {
141159
if (cell.type === 'column-header-cell') {
142160
tableInspectorTab = 'column';

mathesar_ui/src/systems/table-view/header/Header.svelte

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import {
1313
ID_ADD_NEW_COLUMN,
1414
ID_ROW_CONTROL_COLUMN,
15-
type JoinedColumn,
1615
type ProcessedColumn,
1716
getTabularDataStoreFromContext,
1817
isJoinedColumn,
@@ -30,8 +29,7 @@
3029
export let table: Table;
3130
3231
$: columnOrder = columnOrder ?? [];
33-
$: ({ selection, processedColumns, allColumns, columnsDataStore } =
34-
$tabularData);
32+
$: ({ selection, processedColumns, allColumns } = $tabularData);
3533
3634
let locationOfFirstDraggedColumn: number | undefined = undefined;
3735
let selectedColumnIdsOrdered: string[] = [];
@@ -99,19 +97,6 @@
9997
selectedColumnIdsOrdered = [];
10098
newColumnOrder = [];
10199
}
102-
103-
function saveColumnWidth(
104-
column: ProcessedColumn | JoinedColumn,
105-
width: number | null,
106-
) {
107-
// Joined columns do not persist width to the database
108-
if (isJoinedColumn(column)) {
109-
return;
110-
}
111-
void columnsDataStore.setDisplayOptions(column.column, {
112-
display_width: width,
113-
});
114-
}
115100
</script>
116101

117102
<SheetHeader>
@@ -150,11 +135,7 @@
150135
</Droppable>
151136
</Draggable>
152137
{/if}
153-
<SheetCellResizer
154-
columnIdentifierKey={columnId}
155-
afterResize={(width) => saveColumnWidth(columnFabric, width)}
156-
onReset={() => saveColumnWidth(columnFabric, null)}
157-
/>
138+
<SheetCellResizer {columnId} />
158139
</SheetColumnHeaderCell>
159140
{/each}
160141

mathesar_ui/src/systems/table-view/table-inspector/column/ColumnFormatting.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
async function save() {
3636
typeChangeState = { state: 'processing' };
3737
try {
38-
await columnsDataStore.setDisplayOptions(column.column, displayOptions);
38+
await columnsDataStore.setDisplayOptions(
39+
new Map([[column.column.id, displayOptions]]),
40+
);
3941
actionButtonsVisible = false;
4042
typeChangeState = { state: 'success' };
4143
} catch (err) {

0 commit comments

Comments
 (0)