feat: add bulk file actions (#7827)
* fixed styling * Enabled header checkbox selection * Changed styling of selection * Implemented bulk downloading and deleting * Added delete bulk hook * Added download bulk hook * Fix backend to send extension in download single file * Fix hook to download single file directly * Added header and selection handling * Added delete confirmation * [autofix.ci] apply automated fixes * Fixed selection with shift * Show disabled files * Show disabled files as not clickable * Changed color of icon when disabled * Implemented pressed shift handling * Fixed shift selection and disabled text selection when holding shift * Created test for bulk selection on files modal * add test of disabled components in file component * Fixed files page test to include bulk editing test * removed ring on focus visible * Changed delete files having the right select --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
596a219de3
commit
a72995c408
11 changed files with 1244 additions and 235 deletions
|
|
@ -391,3 +391,390 @@ test(
|
|||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should be able to select multiple files with shift-click",
|
||||
{
|
||||
tag: ["@release", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
// Generate unique filenames for this test run
|
||||
const file1 = generateRandomFilename();
|
||||
const file2 = generateRandomFilename();
|
||||
const file3 = generateRandomFilename();
|
||||
const file4 = generateRandomFilename();
|
||||
const file5 = generateRandomFilename();
|
||||
|
||||
// Read the test file content
|
||||
const testFilePath = path.join(__dirname, "../../assets/test_file.txt");
|
||||
const fileContent = fs.readFileSync(testFilePath);
|
||||
|
||||
await awaitBootstrapTest(page);
|
||||
|
||||
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
await page.getByTestId("blank-flow").click();
|
||||
|
||||
await addLegacyComponents(page);
|
||||
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("file");
|
||||
|
||||
await page.waitForSelector('[data-testid="dataFile"]', {
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByTestId("dataFile")
|
||||
.first()
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
await adjustScreenView(page);
|
||||
|
||||
// Check if file management button is visible
|
||||
const fileManagement = await page
|
||||
.getByTestId("button_open_file_management")
|
||||
?.isVisible();
|
||||
|
||||
if (fileManagement) {
|
||||
// Open file management modal
|
||||
await page.getByTestId("button_open_file_management").click();
|
||||
|
||||
// Upload 5 files for testing shift-click selection
|
||||
// Upload file 1
|
||||
const createFileTransfer = async (
|
||||
filename: string,
|
||||
content: string,
|
||||
type: string,
|
||||
) => {
|
||||
return page.evaluateHandle(
|
||||
(params) => {
|
||||
const data = new DataTransfer();
|
||||
const file = new File(
|
||||
[params.content],
|
||||
`${params.filename}.${params.type}`,
|
||||
{ type: params.mimeType },
|
||||
);
|
||||
data.items.add(file);
|
||||
return data;
|
||||
},
|
||||
{
|
||||
filename,
|
||||
content,
|
||||
type,
|
||||
mimeType: type === "txt" ? "text/plain" : "application/json",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// Upload five files
|
||||
const files = [
|
||||
{ name: file1, content: "file content 1", type: "txt" },
|
||||
{ name: file2, content: "file content 2", type: "txt" },
|
||||
{ name: file3, content: "file content 3", type: "txt" },
|
||||
{ name: file4, content: "file content 4", type: "txt" },
|
||||
{ name: file5, content: "file content 5", type: "txt" },
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
const dataTransfer = await createFileTransfer(
|
||||
file.name,
|
||||
file.content,
|
||||
file.type,
|
||||
);
|
||||
|
||||
// Trigger drag events
|
||||
await page.dispatchEvent(
|
||||
'[data-testid="drag-files-component"]',
|
||||
"dragover",
|
||||
{ dataTransfer },
|
||||
);
|
||||
await page.dispatchEvent(
|
||||
'[data-testid="drag-files-component"]',
|
||||
"drop",
|
||||
{ dataTransfer },
|
||||
);
|
||||
|
||||
// Verify file was uploaded
|
||||
await expect(
|
||||
page.getByText(`${file.name}.${file.type}`).last(),
|
||||
).toBeVisible({
|
||||
timeout: 1000,
|
||||
});
|
||||
}
|
||||
|
||||
// Unselect all files first
|
||||
for (const file of files) {
|
||||
if (
|
||||
(await page
|
||||
.getByTestId(`checkbox-${file.name}`)
|
||||
.last()
|
||||
.getAttribute("data-state")) === "checked"
|
||||
) {
|
||||
await page.getByTestId(`checkbox-${file.name}`).last().click();
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: Select first file, then shift-click the third file
|
||||
// First file
|
||||
await page.getByTestId(`checkbox-${file1}`).last().click();
|
||||
|
||||
// Hold shift and click third file
|
||||
await page.keyboard.down("Shift");
|
||||
await page.getByTestId(`checkbox-${file3}`).last().click();
|
||||
await page.keyboard.up("Shift");
|
||||
|
||||
// Verify files 1, 2, and 3 are selected
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file1}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file2}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file3}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file4}`).last(),
|
||||
).toHaveAttribute("data-state", "unchecked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file5}`).last(),
|
||||
).toHaveAttribute("data-state", "unchecked");
|
||||
|
||||
// Test 2: Shift-click to extend selection to file 5
|
||||
await page.keyboard.down("Shift");
|
||||
await page.getByTestId(`checkbox-${file5}`).last().click();
|
||||
await page.keyboard.up("Shift");
|
||||
|
||||
// Verify all files are selected
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file1}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file2}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file3}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file4}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file5}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
|
||||
// Test 3: Unselect a range with shift-click
|
||||
// First select only file 2
|
||||
for (const file of files) {
|
||||
if (
|
||||
(await page
|
||||
.getByTestId(`checkbox-${file.name}`)
|
||||
.last()
|
||||
.getAttribute("data-state")) === "checked"
|
||||
) {
|
||||
await page.getByTestId(`checkbox-${file.name}`).last().click();
|
||||
}
|
||||
}
|
||||
await page.getByTestId(`checkbox-${file2}`).last().click();
|
||||
|
||||
// Select file 2 through 4
|
||||
await page.keyboard.down("Shift");
|
||||
await page.getByTestId(`checkbox-${file4}`).last().click();
|
||||
await page.keyboard.up("Shift");
|
||||
|
||||
// Verify files 2, 3, and 4 are selected
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file1}`).last(),
|
||||
).toHaveAttribute("data-state", "unchecked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file2}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file3}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file4}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file5}`).last(),
|
||||
).toHaveAttribute("data-state", "unchecked");
|
||||
|
||||
// Now use shift-click on an already selected range to deselect
|
||||
await page.keyboard.down("Shift");
|
||||
await page.getByTestId(`checkbox-${file2}`).last().click();
|
||||
await page.keyboard.up("Shift");
|
||||
|
||||
// Verify the range is now deselected
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file1}`).last(),
|
||||
).toHaveAttribute("data-state", "unchecked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file2}`).last(),
|
||||
).toHaveAttribute("data-state", "checked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file3}`).last(),
|
||||
).toHaveAttribute("data-state", "unchecked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file4}`).last(),
|
||||
).toHaveAttribute("data-state", "unchecked");
|
||||
await expect(
|
||||
page.getByTestId(`checkbox-${file5}`).last(),
|
||||
).toHaveAttribute("data-state", "unchecked");
|
||||
|
||||
// Close the modal
|
||||
await page.getByTestId("select-files-modal-button").click();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should show PNG file as disabled in file component",
|
||||
{
|
||||
tag: ["@release", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
// Generate unique filenames for this test run
|
||||
const pngFileName = generateRandomFilename();
|
||||
const txtFileName = generateRandomFilename();
|
||||
|
||||
// Create PNG content (a simple 1x1 transparent PNG)
|
||||
const pngFileContent = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
"base64",
|
||||
);
|
||||
|
||||
// Read the test file content for text file
|
||||
const testFilePath = path.join(__dirname, "../../assets/test_file.txt");
|
||||
const txtFileContent = fs.readFileSync(testFilePath);
|
||||
|
||||
// Step 1: First navigate to files page and upload both files
|
||||
await awaitBootstrapTest(page, { skipModal: true });
|
||||
|
||||
// Navigate to My Files page
|
||||
await page.getByText("My Files").first().click();
|
||||
|
||||
// Check if we're on the files page
|
||||
await page.waitForSelector('[data-testid="mainpage_title"]');
|
||||
const title = await page.getByTestId("mainpage_title");
|
||||
expect(await title.textContent()).toContain("My Files");
|
||||
|
||||
// Upload the PNG file
|
||||
const fileChooserPromisePng = page.waitForEvent("filechooser");
|
||||
await page.getByTestId("upload-file-btn").click();
|
||||
|
||||
const fileChooserPng = await fileChooserPromisePng;
|
||||
await fileChooserPng.setFiles([
|
||||
{
|
||||
name: `${pngFileName}.png`,
|
||||
mimeType: "image/png",
|
||||
buffer: pngFileContent,
|
||||
},
|
||||
]);
|
||||
|
||||
// Wait for upload success message
|
||||
await expect(page.getByText("File uploaded successfully")).toBeVisible();
|
||||
|
||||
// Verify PNG file appears in the list
|
||||
await expect(page.getByText(`${pngFileName}.png`)).toBeVisible();
|
||||
|
||||
// Upload the TXT file
|
||||
const fileChooserPromiseTxt = page.waitForEvent("filechooser");
|
||||
await page.getByTestId("upload-file-btn").click();
|
||||
|
||||
const fileChooserTxt = await fileChooserPromiseTxt;
|
||||
await fileChooserTxt.setFiles([
|
||||
{
|
||||
name: `${txtFileName}.txt`,
|
||||
mimeType: "text/plain",
|
||||
buffer: txtFileContent,
|
||||
},
|
||||
]);
|
||||
|
||||
// Wait for upload success message
|
||||
await expect(page.getByText("File uploaded successfully")).toBeVisible();
|
||||
|
||||
// Verify TXT file appears in the list
|
||||
await expect(page.getByText(`${txtFileName}.txt`)).toBeVisible();
|
||||
|
||||
// Step 2: Create a flow with File component and check if PNG file is disabled
|
||||
// Navigate to workspace page
|
||||
await page.getByText("Starter Project").first().click();
|
||||
|
||||
await awaitBootstrapTest(page, { skipGoto: true });
|
||||
|
||||
// Create a new flow
|
||||
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
await page.getByTestId("blank-flow").click();
|
||||
|
||||
await addLegacyComponents(page);
|
||||
|
||||
// Add a file component to the flow
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("file");
|
||||
|
||||
await page.waitForSelector('[data-testid="dataFile"]', {
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByTestId("dataFile")
|
||||
.first()
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
await adjustScreenView(page);
|
||||
|
||||
// Open the file management modal
|
||||
await page.getByTestId("button_open_file_management").click();
|
||||
console.log(pngFileName);
|
||||
|
||||
// Check if the PNG file has the disabled class (greyed out)
|
||||
await expect(page.getByTestId(`file-item-${pngFileName}`)).toHaveClass(
|
||||
/pointer-events-none cursor-not-allowed opacity-50/,
|
||||
);
|
||||
|
||||
// Check that the TXT file is not disabled
|
||||
await expect(page.getByTestId(`file-item-${txtFileName}`)).not.toHaveClass(
|
||||
/pointer-events-none cursor-not-allowed opacity-50/,
|
||||
);
|
||||
|
||||
// Verify the tooltip for PNG file states it's not supported
|
||||
await page
|
||||
.locator(`[data-testid="file-item-${pngFileName}"]`)
|
||||
.locator("..")
|
||||
.hover();
|
||||
|
||||
await expect(
|
||||
page.getByText("Type not supported by component"),
|
||||
).toBeVisible();
|
||||
|
||||
// Try to select the PNG file (should not change its state)
|
||||
await expect(page.getByTestId(`checkbox-${pngFileName}`)).toBeDisabled();
|
||||
|
||||
// Verify the PNG file checkbox remains unchecked
|
||||
await expect(page.getByTestId(`checkbox-${pngFileName}`)).toHaveAttribute(
|
||||
"data-state",
|
||||
"unchecked",
|
||||
);
|
||||
|
||||
// Select the TXT file (should work normally)
|
||||
await page.getByTestId(`checkbox-${txtFileName}`).click();
|
||||
|
||||
// Verify the TXT file checkbox becomes checked
|
||||
await expect(page.getByTestId(`checkbox-${txtFileName}`)).toHaveAttribute(
|
||||
"data-state",
|
||||
"checked",
|
||||
);
|
||||
|
||||
// Submit the file selection
|
||||
await page.getByTestId("select-files-modal-button").click();
|
||||
|
||||
// Verify that only the TXT file was selected in the component
|
||||
await expect(page.getByText(`${txtFileName}.txt`)).toBeVisible();
|
||||
await expect(page.getByText(`${pngFileName}.png`)).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -318,3 +318,148 @@ test(
|
|||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should handle bulk actions for multiple files",
|
||||
{ tag: ["@release", "@files"] },
|
||||
async ({ page }) => {
|
||||
const fileNames = {
|
||||
txt: generateRandomFilename(),
|
||||
json: generateRandomFilename(),
|
||||
py: generateRandomFilename(),
|
||||
};
|
||||
|
||||
const testFiles = [
|
||||
path.join(__dirname, "../../assets/test-file.txt"),
|
||||
path.join(__dirname, "../../assets/test-file.json"),
|
||||
path.join(__dirname, "../../assets/test-file.py"),
|
||||
];
|
||||
|
||||
const fileContents = testFiles.map((file) => fs.readFileSync(file));
|
||||
|
||||
await awaitBootstrapTest(page, { skipModal: true });
|
||||
|
||||
const firstRunLangflow = await page
|
||||
.getByTestId("empty-project-description")
|
||||
.count();
|
||||
|
||||
if (firstRunLangflow > 0) {
|
||||
await addFlowToTestOnEmptyLangflow(page);
|
||||
}
|
||||
|
||||
await page.waitForSelector('[data-testid="mainpage_title"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
await page.getByText("My Files").first().click();
|
||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||
await page.getByTestId("upload-file-btn").click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles([
|
||||
{
|
||||
name: `${fileNames.txt}.txt`,
|
||||
mimeType: "text/plain",
|
||||
buffer: fileContents[0],
|
||||
},
|
||||
{
|
||||
name: `${fileNames.json}.json`,
|
||||
mimeType: "application/json",
|
||||
buffer: fileContents[1],
|
||||
},
|
||||
{
|
||||
name: `${fileNames.py}.py`,
|
||||
mimeType: "text/x-python",
|
||||
buffer: fileContents[2],
|
||||
},
|
||||
]);
|
||||
|
||||
// Wait for upload success message
|
||||
const successMessage = await page.getByText("Files uploaded successfully");
|
||||
expect(successMessage).toBeTruthy();
|
||||
|
||||
// Verify all files appear in the list
|
||||
for (const name of Object.values(fileNames)) {
|
||||
const file = await page.getByText(name).last();
|
||||
await expect(file).toBeVisible({
|
||||
timeout: 1000,
|
||||
});
|
||||
}
|
||||
|
||||
// Select files with shift (checkbox on the grid)
|
||||
|
||||
await page.keyboard.down("Shift");
|
||||
await page.locator('input[data-ref="eInput"]').nth(5).click();
|
||||
await page.locator('input[data-ref="eInput"]').nth(7).click();
|
||||
await page.keyboard.up("Shift");
|
||||
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(5).isChecked(),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(6).isChecked(),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await page.locator('input[data-ref="eInput"]').nth(7).isChecked(),
|
||||
).toBe(true);
|
||||
|
||||
// Check if the bulk actions toolbar appears
|
||||
const selectedCountText = await page.getByText("3 selected");
|
||||
await expect(selectedCountText).toBeVisible();
|
||||
|
||||
// Check if download button is visible
|
||||
const downloadButton = await page.getByTestId("bulk-download-btn");
|
||||
await expect(downloadButton).toBeVisible();
|
||||
|
||||
// Set up download listener
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
|
||||
// Click download button
|
||||
await downloadButton.click();
|
||||
|
||||
// Wait for download to start
|
||||
const download = await downloadPromise;
|
||||
|
||||
// Verify the download was initiated
|
||||
expect(download).toBeTruthy();
|
||||
|
||||
// Check for success message
|
||||
const downloadSuccessMessage = await page.getByText(
|
||||
/Files? downloaded successfully/,
|
||||
);
|
||||
expect(downloadSuccessMessage).toBeTruthy();
|
||||
|
||||
// Select both files (checkbox on the grid)
|
||||
|
||||
await page.locator('input[data-ref="eInput"]').nth(7).click();
|
||||
|
||||
// Check if the bulk actions toolbar appears
|
||||
const selectedCountTextDelete = await page.getByText("2 selected");
|
||||
await expect(selectedCountTextDelete).toBeVisible();
|
||||
|
||||
const deleteButton = await page.getByTestId("bulk-delete-btn");
|
||||
await expect(deleteButton).toBeVisible();
|
||||
|
||||
// Test delete functionality
|
||||
await deleteButton.click();
|
||||
|
||||
// Confirm the delete in the modal
|
||||
const confirmDeleteButton = await page.getByRole("button", {
|
||||
name: "Delete",
|
||||
});
|
||||
await confirmDeleteButton.click();
|
||||
|
||||
// Check for success message
|
||||
const deleteSuccessMessage = await page.getByText(
|
||||
"Files deleted successfully",
|
||||
);
|
||||
expect(deleteSuccessMessage).toBeTruthy();
|
||||
|
||||
// Verify the deleted files are no longer visible
|
||||
const remainingFileCount =
|
||||
(await page.getByText(fileNames.py + ".py").count()) +
|
||||
(await page.getByText(fileNames.txt + ".txt").count()) +
|
||||
(await page.getByText(fileNames.json + ".json").count());
|
||||
expect(remainingFileCount).toBe(1);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue