From e3213f5e4ccbe97b29a718218b3be22edaac2c09 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:08:13 -0500 Subject: [PATCH 1/5] mapping file and edit csv --- .../editor/FileWorkspaceHeader.svelte | 22 +- .../entity-management/AddAssetButton.svelte | 12 +- .../workspaces/MappingWorkspace.svelte | 348 ++++++++++++++++++ .../mapping/edit/[...file]/+page.svelte | 98 +++++ .../mapping/edit/[...file]/+page.ts | 37 ++ .../(workspace)/mapping/new/+page.svelte | 15 + 6 files changed, 530 insertions(+), 2 deletions(-) create mode 100644 web-common/src/features/workspaces/MappingWorkspace.svelte create mode 100644 web-local/src/routes/(application)/(workspace)/mapping/edit/[...file]/+page.svelte create mode 100644 web-local/src/routes/(application)/(workspace)/mapping/edit/[...file]/+page.ts create mode 100644 web-local/src/routes/(application)/(workspace)/mapping/new/+page.svelte diff --git a/web-common/src/features/editor/FileWorkspaceHeader.svelte b/web-common/src/features/editor/FileWorkspaceHeader.svelte index 74544c99511..e93d827df13 100644 --- a/web-common/src/features/editor/FileWorkspaceHeader.svelte +++ b/web-common/src/features/editor/FileWorkspaceHeader.svelte @@ -1,5 +1,6 @@ +> + + {#if isEditableCSV} + @@ -265,7 +269,9 @@ sql: "select * from read_csv('${csvPath}', auto_detect=true, ignore_errors=1, he - {#each columns as col, colIndex (colIndex)} @@ -274,7 +280,8 @@ sql: "select * from read_csv('${csvPath}', auto_detect=true, ignore_errors=1, he updateColumnName(colIndex, e.currentTarget.value)} + on:input={(e) => + updateColumnName(colIndex, e.currentTarget.value)} on:keydown={handleKeydown} class="flex-1 p-2 font-semibold text-sm bg-transparent border-none outline-none focus:bg-blue-50" /> @@ -295,7 +302,9 @@ sql: "select * from read_csv('${csvPath}', auto_detect=true, ignore_errors=1, he {#each rows as row, rowIndex (rowIndex)} - {#each row as cell, colIndex (colIndex)} @@ -305,7 +314,8 @@ sql: "select * from read_csv('${csvPath}', auto_detect=true, ignore_errors=1, he value={cell} data-row={rowIndex} data-col={colIndex} - on:input={(e) => updateCell(rowIndex, colIndex, e.currentTarget.value)} + on:input={(e) => + updateCell(rowIndex, colIndex, e.currentTarget.value)} on:keydown={(e) => handleKeydown(e, rowIndex, colIndex)} class="w-full p-2 text-sm bg-white border-none outline-none focus:bg-blue-50" /> @@ -337,7 +347,6 @@ sql: "select * from read_csv('${csvPath}', auto_detect=true, ignore_errors=1, he @apply items-center; } - table { border-spacing: 0; } diff --git a/web-local/src/routes/(application)/(workspace)/mapping/edit/[...file]/+page.ts b/web-local/src/routes/(application)/(workspace)/mapping/edit/[...file]/+page.ts index 78c78ffecc8..d27a3f1a5b5 100644 --- a/web-local/src/routes/(application)/(workspace)/mapping/edit/[...file]/+page.ts +++ b/web-local/src/routes/(application)/(workspace)/mapping/edit/[...file]/+page.ts @@ -31,7 +31,10 @@ export const load = async ({ params: { file }, parent }) => { if (statusCode === 404 || statusCode === 400) { throw error(404, "File not found: " + path); } else { - throw error(e.response?.status ?? 500, e.response?.data?.message ?? "Unknown error"); + throw error( + e.response?.status ?? 500, + e.response?.data?.message ?? "Unknown error", + ); } } }; From 6677252dcce3037b2271a8eb3f5c1442de9a4f34 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:15:03 -0500 Subject: [PATCH 3/5] e2e --- web-local/tests/mapping.spec.ts | 260 ++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 web-local/tests/mapping.spec.ts diff --git a/web-local/tests/mapping.spec.ts b/web-local/tests/mapping.spec.ts new file mode 100644 index 00000000000..bcc0d988a63 --- /dev/null +++ b/web-local/tests/mapping.spec.ts @@ -0,0 +1,260 @@ +import { expect } from "@playwright/test"; +import { test } from "./setup/base"; +import { waitForFileNavEntry, fileNotPresent } from "./utils/waitHelpers"; +import { deleteFile } from "./utils/commonHelpers"; + +test.describe("mapping workspace", () => { + test.use({ project: "Blank" }); + + test("Create new mapping file from Add menu", async ({ page }) => { + // Click Add button + await page.getByRole("button", { name: "Add Asset" }).click(); + + // Navigate to More submenu + await page.getByRole("menuitem", { name: "More" }).click(); + + // Click Mapping file + await page.getByRole("menuitem", { name: "Mapping file" }).click(); + + // Should navigate to mapping workspace + await page.waitForURL(/\/mapping\/new/); + + // Should see the default filename + await expect(page.locator("#mapping-title-input")).toHaveValue("mapping.csv"); + + // Should see Add Column and Add Row buttons + await expect(page.getByRole("button", { name: "Add Column" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Add Row" })).toBeVisible(); + + // Should see Save button + await expect(page.getByRole("button", { name: "Save" })).toBeVisible(); + + // Should see default columns + const columnHeaders = page.locator("thead input"); + await expect(columnHeaders).toHaveCount(2); + }); + + test("Edit cells and columns", async ({ page }) => { + // Navigate to mapping workspace + await page.goto("/mapping/new"); + await page.waitForURL(/\/mapping\/new/); + + // Edit first column name + const firstColumnInput = page.locator("thead input").first(); + await firstColumnInput.click(); + await firstColumnInput.fill("name"); + + // Edit second column name + const secondColumnInput = page.locator("thead input").nth(1); + await secondColumnInput.click(); + await secondColumnInput.fill("value"); + + // Edit first cell + const firstCell = page.locator("tbody input").first(); + await firstCell.click(); + await firstCell.fill("test_name"); + + // Edit second cell + const secondCell = page.locator("tbody input").nth(1); + await secondCell.click(); + await secondCell.fill("test_value"); + + // Verify values are set + await expect(firstColumnInput).toHaveValue("name"); + await expect(secondColumnInput).toHaveValue("value"); + await expect(firstCell).toHaveValue("test_name"); + await expect(secondCell).toHaveValue("test_value"); + }); + + test("Add and remove columns", async ({ page }) => { + await page.goto("/mapping/new"); + await page.waitForURL(/\/mapping\/new/); + + // Initially 2 columns + await expect(page.locator("thead input")).toHaveCount(2); + + // Add a column + await page.getByRole("button", { name: "Add Column" }).click(); + await expect(page.locator("thead input")).toHaveCount(3); + + // Remove a column (click the trash icon in header) + await page.locator("thead button").first().click(); + await expect(page.locator("thead input")).toHaveCount(2); + }); + + test("Add and remove rows", async ({ page }) => { + await page.goto("/mapping/new"); + await page.waitForURL(/\/mapping\/new/); + + // Initially 1 row + await expect(page.locator("tbody tr")).toHaveCount(1); + + // Add a row + await page.getByRole("button", { name: "Add Row" }).click(); + await expect(page.locator("tbody tr")).toHaveCount(2); + + // Add another row + await page.getByRole("button", { name: "Add Row" }).click(); + await expect(page.locator("tbody tr")).toHaveCount(3); + + // Remove a row (click trash icon) + await page.locator("tbody tr").first().locator("button").click(); + await expect(page.locator("tbody tr")).toHaveCount(2); + }); + + test("Tab creates new row at last cell", async ({ page }) => { + await page.goto("/mapping/new"); + await page.waitForURL(/\/mapping\/new/); + + // Initially 1 row + await expect(page.locator("tbody tr")).toHaveCount(1); + + // Focus the last cell (second column of first row) + const lastCell = page.locator("tbody input").last(); + await lastCell.click(); + + // Press Tab + await page.keyboard.press("Tab"); + + // Should create a new row + await expect(page.locator("tbody tr")).toHaveCount(2); + + // First cell of new row should be focused + const newRowFirstCell = page.locator('tbody input[data-row="1"][data-col="0"]'); + await expect(newRowFirstCell).toBeFocused(); + }); + + test("Ctrl+A selects all text in cell", async ({ page }) => { + await page.goto("/mapping/new"); + await page.waitForURL(/\/mapping\/new/); + + // Fill a cell with text + const firstCell = page.locator("tbody input").first(); + await firstCell.click(); + await firstCell.fill("test content"); + + // Use Ctrl+A (or Cmd+A on Mac) + const modifier = process.platform === "darwin" ? "Meta" : "Control"; + await page.keyboard.press(`${modifier}+a`); + + // Text should be selected - we can verify by typing and seeing it replaces + await page.keyboard.type("replaced"); + await expect(firstCell).toHaveValue("replaced"); + }); + + test("Save creates CSV and YAML files", async ({ page }) => { + await page.goto("/mapping/new"); + await page.waitForURL(/\/mapping\/new/); + + // Edit column names + const firstColumnInput = page.locator("thead input").first(); + await firstColumnInput.click(); + await firstColumnInput.fill("city"); + + const secondColumnInput = page.locator("thead input").nth(1); + await secondColumnInput.click(); + await secondColumnInput.fill("country"); + + // Edit cells + const firstCell = page.locator("tbody input").first(); + await firstCell.click(); + await firstCell.fill("Paris"); + + const secondCell = page.locator("tbody input").nth(1); + await secondCell.click(); + await secondCell.fill("France"); + + // Click Save + await page.getByRole("button", { name: "Save" }).click(); + + // Should navigate to the model file + await page.waitForURL(/\/files\/models\/mapping\.yaml/); + + // CSV file should exist in nav + await waitForFileNavEntry(page, "/data/mapping.csv", false); + + // Model file should exist in nav + await waitForFileNavEntry(page, "/models/mapping.yaml", false); + + // Clean up + await deleteFile(page, "/data/mapping.csv"); + await deleteFile(page, "/models/mapping.yaml"); + }); + + test("Auto-increment filename when files exist", async ({ page }) => { + // Create first mapping + await page.goto("/mapping/new"); + await page.waitForURL(/\/mapping\/new/); + + await page.getByRole("button", { name: "Save" }).click(); + await page.waitForURL(/\/files\/models\/mapping\.yaml/); + + // Create second mapping - should auto-increment + await page.goto("/mapping/new"); + await page.waitForURL(/\/mapping\/new/); + + // Title should show mapping_1.csv + await expect(page.locator("#mapping-title-input")).toHaveValue("mapping_1.csv"); + + await page.getByRole("button", { name: "Save" }).click(); + await page.waitForURL(/\/files\/models\/mapping_1\.yaml/); + + // Verify both files exist + await waitForFileNavEntry(page, "/data/mapping.csv", false); + await waitForFileNavEntry(page, "/data/mapping_1.csv", false); + await waitForFileNavEntry(page, "/models/mapping.yaml", false); + await waitForFileNavEntry(page, "/models/mapping_1.yaml", false); + + // Clean up + await deleteFile(page, "/data/mapping.csv"); + await deleteFile(page, "/data/mapping_1.csv"); + await deleteFile(page, "/models/mapping.yaml"); + await deleteFile(page, "/models/mapping_1.yaml"); + }); + + test("Edit existing CSV file", async ({ page }) => { + // First create a mapping file + await page.goto("/mapping/new"); + await page.waitForURL(/\/mapping\/new/); + + const firstCell = page.locator("tbody input").first(); + await firstCell.click(); + await firstCell.fill("original"); + + await page.getByRole("button", { name: "Save" }).click(); + await page.waitForURL(/\/files\/models\/mapping\.yaml/); + + // Navigate to CSV file + await page.getByLabel("/data/mapping.csv Nav Entry").click(); + await page.waitForURL(/\/files\/data\/mapping\.csv/); + + // Click Edit in Table button + await page.getByRole("button", { name: "Edit in Table" }).click(); + await page.waitForURL(/\/mapping\/edit\/data\/mapping\.csv/); + + // Verify the data is loaded + const loadedCell = page.locator("tbody input").first(); + await expect(loadedCell).toHaveValue("original"); + + // Edit the cell + await loadedCell.click(); + await loadedCell.fill("modified"); + + // Save + await page.getByRole("button", { name: "Save" }).click(); + await page.waitForURL(/\/files\/models\/mapping\.yaml/); + + // Go back to CSV and verify via Edit in Table + await page.getByLabel("/data/mapping.csv Nav Entry").click(); + await page.waitForURL(/\/files\/data\/mapping\.csv/); + await page.getByRole("button", { name: "Edit in Table" }).click(); + await page.waitForURL(/\/mapping\/edit\/data\/mapping\.csv/); + + await expect(page.locator("tbody input").first()).toHaveValue("modified"); + + // Clean up + await page.goto("/"); + await deleteFile(page, "/data/mapping.csv"); + await deleteFile(page, "/models/mapping.yaml"); + }); +}); From bf402e3aae5ea53aaaf7b48379bd9379637db220 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:19:54 -0500 Subject: [PATCH 4/5] prettier --- web-local/tests/mapping.spec.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/web-local/tests/mapping.spec.ts b/web-local/tests/mapping.spec.ts index bcc0d988a63..ce1411060e4 100644 --- a/web-local/tests/mapping.spec.ts +++ b/web-local/tests/mapping.spec.ts @@ -20,10 +20,14 @@ test.describe("mapping workspace", () => { await page.waitForURL(/\/mapping\/new/); // Should see the default filename - await expect(page.locator("#mapping-title-input")).toHaveValue("mapping.csv"); + await expect(page.locator("#mapping-title-input")).toHaveValue( + "mapping.csv", + ); // Should see Add Column and Add Row buttons - await expect(page.getByRole("button", { name: "Add Column" })).toBeVisible(); + await expect( + page.getByRole("button", { name: "Add Column" }), + ).toBeVisible(); await expect(page.getByRole("button", { name: "Add Row" })).toBeVisible(); // Should see Save button @@ -120,7 +124,9 @@ test.describe("mapping workspace", () => { await expect(page.locator("tbody tr")).toHaveCount(2); // First cell of new row should be focused - const newRowFirstCell = page.locator('tbody input[data-row="1"][data-col="0"]'); + const newRowFirstCell = page.locator( + 'tbody input[data-row="1"][data-col="0"]', + ); await expect(newRowFirstCell).toBeFocused(); }); @@ -194,7 +200,9 @@ test.describe("mapping workspace", () => { await page.waitForURL(/\/mapping\/new/); // Title should show mapping_1.csv - await expect(page.locator("#mapping-title-input")).toHaveValue("mapping_1.csv"); + await expect(page.locator("#mapping-title-input")).toHaveValue( + "mapping_1.csv", + ); await page.getByRole("button", { name: "Save" }).click(); await page.waitForURL(/\/files\/models\/mapping_1\.yaml/); From b445dbecb887e73721d013aa505cc50f15010465 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:00:07 -0500 Subject: [PATCH 5/5] qual --- web-local/tests/mapping.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-local/tests/mapping.spec.ts b/web-local/tests/mapping.spec.ts index ce1411060e4..fb747b27ddc 100644 --- a/web-local/tests/mapping.spec.ts +++ b/web-local/tests/mapping.spec.ts @@ -1,6 +1,6 @@ import { expect } from "@playwright/test"; import { test } from "./setup/base"; -import { waitForFileNavEntry, fileNotPresent } from "./utils/waitHelpers"; +import { waitForFileNavEntry } from "./utils/waitHelpers"; import { deleteFile } from "./utils/commonHelpers"; test.describe("mapping workspace", () => {
+ #
+ {rowIndex + 1}