Skip to content

Commit ca387e5

Browse files
committed
drag re-order schema
1 parent b4fd401 commit ca387e5

14 files changed

Lines changed: 403 additions & 3 deletions

File tree

apps/web/src/tests/browser/app/functional/test-data/text-schema-grid-sync.spec.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,29 @@ test.describe('7. Test Data Generation', () => {
244244
expectNoPageErrors(pageErrors);
245245
});
246246

247+
test('schema rows can be dragged to the top and bottom in the app editor', async ({ page }) => {
248+
const { appPage, pageErrors } = await openApp(page);
249+
250+
await appPage.testDataPanel.expand();
251+
await appPage.testDataPanel.expectExpanded();
252+
253+
await appPage.testDataPanel.setSchemaText('A\nliteral(a)\n\nB\nliteral(b)\n\nC\nliteral(c)\n\nD\nliteral(d)');
254+
await expect.poll(async () => appPage.testDataPanel.getSchemaRowCount()).toBe(4);
255+
256+
await appPage.testDataPanel.schemaEditor.dragRowToIndex(3, 0, { placement: 'before' });
257+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'columnName')).toBe('D');
258+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(1, 'columnName')).toBe('A');
259+
260+
await appPage.testDataPanel.schemaEditor.dragRowToIndex(0, 3, { placement: 'after' });
261+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(0, 'columnName')).toBe('A');
262+
await expect.poll(async () => appPage.testDataPanel.getSchemaCell(3, 'columnName')).toBe('D');
263+
264+
await expect
265+
.poll(async () => appPage.testDataPanel.getSchemaText())
266+
.toContain('A\nliteral(a)\nB\nliteral(b)\nC\nliteral(c)\nD\nliteral(d)');
267+
expectNoPageErrors(pageErrors);
268+
});
269+
247270
test('switching enum rows to domain datatype.enum preserves the enum params in the app editor', async ({ page }) => {
248271
const { appPage, pageErrors } = await openApp(page);
249272

apps/web/src/tests/browser/generator/functional/schema-edit.spec.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,24 @@ test.describe('Generator Schema Editing', () => {
247247
expectNoPageErrors(pageErrors);
248248
});
249249

250+
test('can drag schema rows directly to the top and bottom in the generator editor', async ({ page }) => {
251+
const { generatorPage, pageErrors } = await openGenerator(page);
252+
253+
await generatorPage.schema.setSchemaText('A\nliteral(a)\n\nB\nliteral(b)\n\nC\nliteral(c)\n\nD\nliteral(d)');
254+
await generatorPage.schema.setTextMode(false);
255+
256+
await generatorPage.schema.editor.dragRowToIndex(3, 0, { placement: 'before' });
257+
await expect.poll(async () => getSchemaRowNames(generatorPage)).toEqual(['D', 'A', 'B', 'C']);
258+
259+
await generatorPage.schema.editor.dragRowToIndex(0, 3, { placement: 'after' });
260+
await expect.poll(async () => getSchemaRowNames(generatorPage)).toEqual(['A', 'B', 'C', 'D']);
261+
262+
const schemaText = await generatorPage.schema.getSchemaText();
263+
expectOrderedSubstrings(schemaText, ['A\nliteral(a)', 'B\nliteral(b)', 'C\nliteral(c)', 'D\nliteral(d)']);
264+
265+
expectNoPageErrors(pageErrors);
266+
});
267+
250268
test('can remove a schema row in schema mode and text no longer contains it', async ({ page }) => {
251269
const { generatorPage, pageErrors } = await openGenerator(page);
252270

apps/web/src/tests/browser/shared/abstractions/components/schema-editor.component.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,21 @@ class SchemaEditorComponent {
151151
await this.ensureSchemaMode();
152152
await this.row(index).locator(`button[data-action="${action}"]`).click();
153153
}
154+
155+
async dragRowToIndex(fromIndex, toIndex, { placement = 'before' } = {}) {
156+
await this.ensureSchemaMode();
157+
const source = this.row(fromIndex).locator('[data-action="drag"]');
158+
const target = this.row(toIndex);
159+
const targetBox = await target.boundingBox();
160+
if (!targetBox) {
161+
throw new Error(`Unable to drag schema row ${fromIndex} to ${toIndex}: target row is not visible`);
162+
}
163+
const targetPosition = {
164+
x: Math.max(8, Math.min(targetBox.width - 8, 20)),
165+
y: placement === 'after' ? Math.max(8, targetBox.height - 8) : 8,
166+
};
167+
await source.dragTo(target, { targetPosition });
168+
}
154169
}
155170

156171
module.exports = { SchemaEditorComponent };

apps/web/styles.css

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,7 @@ body.theme-dark .generator-status-text {
718718
display: grid;
719719
gap: 0.4rem;
720720
align-items: center;
721+
position: relative;
721722
}
722723

723724
.generator-schema-row-invalid {
@@ -761,6 +762,37 @@ body.theme-dark .generator-status-text {
761762
gap: 0.2rem;
762763
}
763764

765+
.generator-schema-drag-handle {
766+
cursor: grab;
767+
}
768+
769+
.generator-schema-drag-handle:active {
770+
cursor: grabbing;
771+
}
772+
773+
.generator-schema-row-dragging {
774+
opacity: 0.65;
775+
}
776+
777+
.generator-schema-row-drop-before::before,
778+
.generator-schema-row-drop-after::after {
779+
content: '';
780+
position: absolute;
781+
left: 0.25rem;
782+
right: 0.25rem;
783+
height: 3px;
784+
border-radius: 999px;
785+
background: #0b6aa2;
786+
}
787+
788+
.generator-schema-row-drop-before::before {
789+
top: -0.35rem;
790+
}
791+
792+
.generator-schema-row-drop-after::after {
793+
bottom: -0.35rem;
794+
}
795+
764796
.icon-button {
765797
min-width: 2rem;
766798
height: 2rem;

packages/core-ui/js/gui_components/app/test-data-grid/schema/test-data-grid-schema-grid-controller.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ function createSchemaGridController({
8989
container?.addEventListener('input', schemaEditor.handleInput);
9090
container?.addEventListener('change', schemaEditor.handleInput);
9191
container?.addEventListener('focusout', schemaEditor.handleFocusOut);
92+
container?.addEventListener('dragstart', schemaEditor.handleDragStart);
93+
container?.addEventListener('dragover', schemaEditor.handleDragOver);
94+
container?.addEventListener('drop', schemaEditor.handleDrop);
95+
container?.addEventListener('dragend', schemaEditor.handleDragEnd);
9296
container?.addEventListener('click', (event) => {
9397
void schemaEditor.handleClick(event);
9498
});

packages/core-ui/js/gui_components/generator/controller/data-generator-page-controller.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ import {
5656
updateSchemaEditModeView,
5757
insertExampleSchema,
5858
renderGeneratorSchemaRows,
59+
clearSchemaRowDragClasses,
60+
getSchemaRowDropInstruction,
61+
applySchemaRowDropInstructionIndicator,
5962
handleGeneratorRowInputChange,
6063
handleGeneratorRowButtonClick,
6164
} from '../schema/index.js';
@@ -183,6 +186,7 @@ class DataGeneratorPage {
183186
this.schemaErrorDisplay = undefined;
184187
this.lastPreviewDataTable = undefined;
185188
this.semanticValidationTimers = new Map();
189+
this.dragState = null;
186190
}
187191

188192
get schemaRows() {
@@ -441,6 +445,12 @@ class DataGeneratorPage {
441445

442446
destroy() {
443447
this.clearAllSemanticValidationTimers();
448+
this.clearDragState();
449+
}
450+
451+
clearDragState() {
452+
this.dragState = null;
453+
clearSchemaRowDragClasses(this.documentObj);
444454
}
445455

446456
applySemanticValidationForRow(rowId) {
@@ -733,6 +743,78 @@ class DataGeneratorPage {
733743
this.renderSchemaRows();
734744
}
735745

746+
moveRowToIndex(fromIndex, toIndex) {
747+
this.schemaSession.moveRowToIndex(fromIndex, toIndex);
748+
this.revalidateSchemaRows();
749+
this.renderSchemaRows();
750+
}
751+
752+
handleRowDragStart(event) {
753+
const dragHandle = event?.target?.closest?.('[data-action="drag"]');
754+
if (!dragHandle) {
755+
return;
756+
}
757+
const rowId = dragHandle.getAttribute('data-row-id');
758+
const rowIndex = this.schemaRows.findIndex((row) => row.id === rowId);
759+
if (rowIndex < 0) {
760+
return;
761+
}
762+
this.dragState = { rowId, rowIndex };
763+
event.dataTransfer?.setData?.('text/plain', rowId);
764+
if (event.dataTransfer) {
765+
event.dataTransfer.effectAllowed = 'move';
766+
}
767+
applySchemaRowDropInstructionIndicator({
768+
documentObj: this.documentObj,
769+
draggedRowId: rowId,
770+
dropInstruction: null,
771+
});
772+
}
773+
774+
handleRowDragOver(event) {
775+
if (!this.dragState?.rowId) {
776+
return;
777+
}
778+
const dropInstruction = getSchemaRowDropInstruction({
779+
event,
780+
schemaRows: this.schemaRows,
781+
draggedRowId: this.dragState.rowId,
782+
});
783+
if (!dropInstruction) {
784+
clearSchemaRowDragClasses(this.documentObj);
785+
return;
786+
}
787+
event.preventDefault();
788+
if (event.dataTransfer) {
789+
event.dataTransfer.dropEffect = 'move';
790+
}
791+
applySchemaRowDropInstructionIndicator({
792+
documentObj: this.documentObj,
793+
draggedRowId: this.dragState.rowId,
794+
dropInstruction,
795+
});
796+
}
797+
798+
handleRowDrop(event) {
799+
if (!this.dragState?.rowId) {
800+
return;
801+
}
802+
const dropInstruction = getSchemaRowDropInstruction({
803+
event,
804+
schemaRows: this.schemaRows,
805+
draggedRowId: this.dragState.rowId,
806+
});
807+
event.preventDefault();
808+
if (dropInstruction && dropInstruction.finalIndex !== dropInstruction.draggedIndex) {
809+
this.moveRowToIndex(dropInstruction.draggedIndex, dropInstruction.finalIndex);
810+
}
811+
this.clearDragState();
812+
}
813+
814+
handleRowDragEnd() {
815+
this.clearDragState();
816+
}
817+
736818
parseRowCount(inputId) {
737819
return parseGeneratorRowCount({ documentObj: this.documentObj, inputId });
738820
}

packages/core-ui/js/gui_components/generator/host/data-generator-page-coordinator.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ function bindDataGeneratorPageEvents({ page }) {
4141
schemaRowsContainer?.addEventListener('input', (event) => page.handleRowInputChange(event));
4242
schemaRowsContainer?.addEventListener('change', (event) => page.handleRowInputChange(event));
4343
schemaRowsContainer?.addEventListener('focusout', (event) => page.handleRowFocusOut(event));
44+
schemaRowsContainer?.addEventListener('dragstart', (event) => page.handleRowDragStart(event));
45+
schemaRowsContainer?.addEventListener('dragover', (event) => page.handleRowDragOver(event));
46+
schemaRowsContainer?.addEventListener('drop', (event) => page.handleRowDrop(event));
47+
schemaRowsContainer?.addEventListener('dragend', () => page.handleRowDragEnd());
4448
schemaRowsContainer?.addEventListener('click', (event) => page.handleRowButtonClick(event));
4549
}
4650

packages/core-ui/js/gui_components/generator/schema/data-generator-schema-ui.js

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import {
1717
} from '../../shared/schema-row-rule-mapper.js';
1818
import { applySchemaSourceTypeChange } from '../../shared/test-data/schema/schema-row-mapper.js';
1919

20+
const SCHEMA_ROW_DRAGGING_CLASS = 'generator-schema-row-dragging';
21+
const SCHEMA_ROW_DROP_BEFORE_CLASS = 'generator-schema-row-drop-before';
22+
const SCHEMA_ROW_DROP_AFTER_CLASS = 'generator-schema-row-drop-after';
23+
2024
function refreshHelpHints(documentObj) {
2125
const windowObj = documentObj?.defaultView || globalThis.window;
2226
if (typeof windowObj?.updateHelpHints === 'function') {
@@ -134,8 +138,10 @@ function renderGeneratorSchemaRows({
134138
row?.validation?.valid === false ? 'generator-schema-row-invalid' : ''
135139
}`;
136140
rowElem.setAttribute('data-row-id', row.id);
141+
rowElem.setAttribute('data-row-index', String(index));
137142
rowElem.innerHTML = `
138143
<div class="generator-row-actions">
144+
<button class="icon-button generator-schema-drag-handle" type="button" data-action="drag" data-row-id="${row.id}" title="Drag to reorder" draggable="true" aria-label="Drag field to reorder">&#x2630;</button>
139145
<button class="icon-button" data-action="add" data-row-id="${row.id}" title="Add field">+</button>
140146
<button class="icon-button" data-action="remove" data-row-id="${row.id}" title="Remove field">-</button>
141147
<button class="icon-button" data-action="up" data-row-id="${row.id}" title="Move up" ${index === 0 ? 'disabled' : ''}>&uarr;</button>
@@ -200,6 +206,82 @@ function renderGeneratorSchemaRows({
200206
updateAllPairsButtonVisibility();
201207
}
202208

209+
function clearSchemaRowDragClasses(documentObj) {
210+
const rows = documentObj?.querySelectorAll?.('.generator-schema-row');
211+
rows?.forEach?.((rowElem) => {
212+
rowElem.classList.remove(SCHEMA_ROW_DRAGGING_CLASS, SCHEMA_ROW_DROP_BEFORE_CLASS, SCHEMA_ROW_DROP_AFTER_CLASS);
213+
});
214+
}
215+
216+
function getSchemaRowDropInstruction({ event, schemaRows, draggedRowId }) {
217+
if (!draggedRowId || !Array.isArray(schemaRows) || schemaRows.length === 0) {
218+
return null;
219+
}
220+
const draggedIndex = schemaRows.findIndex((row) => row.id === draggedRowId);
221+
if (draggedIndex < 0) {
222+
return null;
223+
}
224+
225+
const targetRowElem = event?.target?.closest?.('.generator-schema-row');
226+
if (!targetRowElem) {
227+
const container = event?.currentTarget;
228+
if (!container?.classList?.contains?.('generator-schema-rows')) {
229+
return null;
230+
}
231+
const lastIndex = schemaRows.length - 1;
232+
if (lastIndex < 0) {
233+
return null;
234+
}
235+
const finalIndex = draggedIndex < lastIndex ? lastIndex : draggedIndex;
236+
return {
237+
draggedRowId,
238+
draggedIndex,
239+
targetRowId: schemaRows[lastIndex].id,
240+
targetIndex: lastIndex,
241+
placement: 'after',
242+
finalIndex,
243+
targetRowElem: container.querySelector(`.generator-schema-row[data-row-id="${schemaRows[lastIndex].id}"]`),
244+
};
245+
}
246+
247+
const targetRowId = targetRowElem.getAttribute('data-row-id');
248+
const targetIndex = schemaRows.findIndex((row) => row.id === targetRowId);
249+
if (targetIndex < 0) {
250+
return null;
251+
}
252+
253+
const rect = typeof targetRowElem.getBoundingClientRect === 'function' ? targetRowElem.getBoundingClientRect() : null;
254+
const placement =
255+
rect && typeof event?.clientY === 'number' && event.clientY > rect.top + rect.height / 2 ? 'after' : 'before';
256+
let finalIndex = placement === 'after' ? targetIndex + 1 : targetIndex;
257+
if (draggedIndex < finalIndex) {
258+
finalIndex -= 1;
259+
}
260+
finalIndex = Math.max(0, Math.min(schemaRows.length - 1, finalIndex));
261+
262+
return {
263+
draggedRowId,
264+
draggedIndex,
265+
targetRowId,
266+
targetIndex,
267+
placement,
268+
finalIndex,
269+
targetRowElem,
270+
};
271+
}
272+
273+
function applySchemaRowDropInstructionIndicator({ documentObj, draggedRowId, dropInstruction }) {
274+
clearSchemaRowDragClasses(documentObj);
275+
const draggedRowElem = documentObj?.querySelector?.(`.generator-schema-row[data-row-id="${draggedRowId}"]`);
276+
draggedRowElem?.classList?.add?.(SCHEMA_ROW_DRAGGING_CLASS);
277+
if (!dropInstruction?.targetRowElem || dropInstruction.finalIndex === dropInstruction.draggedIndex) {
278+
return;
279+
}
280+
dropInstruction.targetRowElem.classList.add(
281+
dropInstruction.placement === 'after' ? SCHEMA_ROW_DROP_AFTER_CLASS : SCHEMA_ROW_DROP_BEFORE_CLASS
282+
);
283+
}
284+
203285
function handleGeneratorRowInputChange({
204286
event,
205287
schemaRows,
@@ -254,7 +336,7 @@ function handleGeneratorRowInputChange({
254336

255337
function handleGeneratorRowButtonClick({ event, schemaRows, addRowAfter, removeRow, moveRow }) {
256338
const action = event.target.getAttribute('data-action');
257-
if (!action) {
339+
if (!action || action === 'drag') {
258340
return;
259341
}
260342

@@ -282,11 +364,17 @@ function handleGeneratorRowButtonClick({ event, schemaRows, addRowAfter, removeR
282364
}
283365

284366
export {
367+
SCHEMA_ROW_DRAGGING_CLASS,
368+
SCHEMA_ROW_DROP_BEFORE_CLASS,
369+
SCHEMA_ROW_DROP_AFTER_CLASS,
285370
buildSchemaModeHelpHtml,
286371
hideVisibleHelpTooltips,
287372
updateSchemaEditModeView,
288373
insertExampleSchema,
289374
renderGeneratorSchemaRows,
375+
clearSchemaRowDragClasses,
376+
getSchemaRowDropInstruction,
377+
applySchemaRowDropInstructionIndicator,
290378
handleGeneratorRowInputChange,
291379
handleGeneratorRowButtonClick,
292380
};

0 commit comments

Comments
 (0)