From 0bad527cd5f318ff1bf8052497bcf0e622e80c6e Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Wed, 29 May 2024 17:48:56 +0200 Subject: [PATCH] Use a table for the grid and list overview of files Our card listingtable could not easily support showing columns for size, modified and name. But instead we can use a Table with CSS to accommodate both views. The added benefits of dropping the card, showing the icons via CSS outweigh using a Card or a per view specific component. The ListingTable is not used as it has a for every row which is simply unneeded and we already require our own CSS customization for styling the Table to support both views. Co-Authored-By: Garrett LeSage --- src/app.scss | 76 --------------- src/files-card-body.jsx | 176 ++++++++++++++++++---------------- src/files-card-body.scss | 195 ++++++++++++++++++++++++++++++++++++++ src/files-folder-view.tsx | 1 + src/header.tsx | 57 ++++++++++- test/check-application | 121 ++++++++++++----------- 6 files changed, 410 insertions(+), 216 deletions(-) create mode 100644 src/files-card-body.scss diff --git a/src/app.scss b/src/app.scss index eb9c30f9e..abfb80154 100644 --- a/src/app.scss +++ b/src/app.scss @@ -63,78 +63,6 @@ button.breadcrumb-edit-cancel-button { grid-template-columns: repeat(auto-fill, minmax(var(--pf-v5-l-gallery--GridTemplateColumns--max), 1fr)); } -.directory-item, .file-item { - // Align icon and text in icon mode - .pf-v5-l-gallery & .pf-v5-c-card__header-main { - flex-direction: column; - align-items: center; - text-align: center; - gap: 0; - } - - // Align icon and text in list mode - .ct-table & .pf-v5-c-card__header-main { - justify-content: start !important; - gap: var(--pf-v5-global--spacer--sm); - } - - // Limit to 3 vertical lines and add ellipsis at the end when needed. - // (Sadly, there's no way to truncate in the middle, except via a PF - // component... but that doesn't allow wrapping and clamping line height.) - .pf-v5-c-card__title-text { - // Yes, all browsers support line-clamp with the webkit prefix. No - // browser supports it unprefixed. All browsers require a display of - // -webkit-box and -webkit-box-orient as well. It's one of the few - // prefixed attributes that's so well supported and still needed. - display: -webkit-box; /* stylelint-disable-line value-no-vendor-prefix */ - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - overflow-wrap: anywhere; - // Wrapping should be more balanced (new; not supported everywhere yet) - text-wrap: balance; - } -} - -.directory-item { - --icon-color: var(--pf-v5-global--link--Color--light); - - &:is(:hover,.pf-m-selected) { - --icon-color: var(--pf-v5-global--link--Color); - } - - .pf-v5-theme-dark & { - --icon-color: var(--pf-v5-global--link--Color--dark); - } - - .pf-v5-theme-dark &:is(:hover,.pf-m-selected) { - --icon-color: var(--pf-v5-global--link--Color--dark--hover); - } -} - -.file-item { - --icon-color: var(--pf-v5-global--palette--black-400); - - &:is(:hover,.pf-m-selected) { - --icon-color: var(--pf-v5-global--palette--black-500); - } - - .pf-v5-theme-dark & { - --icon-color: var(--pf-v5-global--palette--black-400); - } - - .pf-v5-theme-dark &:is(:hover,.pf-m-selected) { - --icon-color: var(--pf-v5-global--palette--black-300); - } -} - -.directory-item, .file-item { - svg > path { - fill: var(--icon-color); - } -} - .sidebar-card { background: inherit; box-shadow: none; @@ -149,10 +77,6 @@ button.breadcrumb-edit-cancel-button { margin-block-end: 1rem; } -.item-button { - color: var(--pf-v5-global--Color--200); -} - // Improve header layout and wrap header text .sidebar-panel { .pf-v5-c-card__header-main { diff --git a/src/files-card-body.jsx b/src/files-card-body.jsx index 41d93b417..bc86f004d 100644 --- a/src/files-card-body.jsx +++ b/src/files-card-body.jsx @@ -19,25 +19,26 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { - Card, CardBody, Flex, - Gallery, - Icon, - CardTitle, Spinner, CardHeader, - MenuItem, MenuList, + Spinner, + MenuItem, + MenuList, Divider, } from "@patternfly/react-core"; -import { FileIcon, FolderIcon } from "@patternfly/react-icons"; +import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; import cockpit from "cockpit"; import { useDialogs } from "dialogs.jsx"; -import { ListingTable } from "cockpit-components-table.jsx"; import { EmptyStatePanel } from "cockpit-components-empty-state.jsx"; +import * as timeformat from "timeformat"; import { ContextMenu } from "cockpit-components-context-menu.jsx"; import { fileActions, ConfirmDeletionDialog } from "./fileActions.jsx"; +import { filterColumnMapping, filterColumns } from "./header"; import { useFilesContext } from "./app"; +import "./files-card-body.scss"; + const _ = cockpit.gettext; const compare = (sortBy) => { @@ -73,6 +74,10 @@ const compare = (sortBy) => { ? -1 : 1) : compareFileType(a, b); + case "largest_size": + return (a, b) => b.size - a.size; + case "smallest_size": + return (a, b) => a.size - b.size; default: break; } @@ -109,6 +114,7 @@ export const FilesCardBody = ({ selected, setSelected, sortBy, + setSortBy, loadingFiles, clipboard, setClipboard, @@ -127,7 +133,7 @@ export const FilesCardBody = ({ const folderViewRef = React.useRef(); function calculateBoxPerRow () { - const boxes = document.querySelectorAll(".item-button"); + const boxes = document.querySelectorAll(".fileview tbody > tr"); if (boxes.length > 1) { let i = 0; const total = boxes.length; @@ -156,7 +162,8 @@ export const FilesCardBody = ({ let folderViewElem = null; const resetSelected = e => { - if (e.target.id === "folder-view" || e.target.id === "files-card-body") { + if (e.target.id === "folder-view" || e.target.id === "files-card-parent" || + (e.target.parentElement && e.target.parentElement.id === "folder-view")) { if (selected.length !== 0) { setSelected([]); } @@ -164,6 +171,7 @@ export const FilesCardBody = ({ }; const handleDoubleClick = (ev) => { + ev.preventDefault(); const name = getFilenameForEvent(ev); const file = sortedFiles?.find(file => file.name === name); if (!file) @@ -177,6 +185,7 @@ export const FilesCardBody = ({ }; const handleClick = (ev) => { + ev.preventDefault(); const name = getFilenameForEvent(ev); const file = sortedFiles?.find(file => file.name === name); if (!file) { @@ -342,94 +351,95 @@ export const FilesCardBody = ({ ); + const sortColumn = (columnIndex) => ({ + sortBy: { + index: filterColumnMapping[sortBy][0], + direction: filterColumnMapping[sortBy][1], + }, + onSort: (_event, index, direction) => { + setSortBy(filterColumns[index][direction].itemId); + }, + columnIndex, + }); + return ( -
+
{contextMenu} -
- {sortedFiles.length === 0 && + {sortedFiles.length === 0 && } - {isGrid && - - - {sortedFiles.map(file => - s.name === file.name)} - isGrid={isGrid} - />)} - - } - {!isGrid && - ({ - columns: [ - { - title: ( - s.name === file.name)} - isGrid={isGrid} - />) - } - ] - }))} - />} -
+ {sortedFiles.length !== 0 && + + + + + + + + + + {sortedFiles.map((file, rowIndex) => + s.name === file.name)} + />)} + +
{_("Name")}{_("Size")}{_("Modified")} +
}
); }; -// Memoize the Item component as rendering thousands of them on each render of parent component is costly. -const Item = React.memo(function Item({ file, isSelected, isGrid }) { - function getFileType(file) { - if (file.type === "dir") { - return "directory-item"; - } else if (file.type === "lnk" && file?.to === "dir") { - return "directory-item"; - } else { - return "file-item"; - } +const getFileType = (file) => { + if (file.type === "dir" || file.to === "dir") { + return "folder"; + } else { + return "file"; } +}; + +// Memoize the Item component as rendering thousands of them on each render of parent component is costly. +const Row = React.memo(function Item({ file, isSelected }) { + const fileType = getFileType(file); + let className = fileType; + if (isSelected) + className += " row-selected"; + if (file.type === "lnk") + className += " symlink"; return ( - - - - {file.type === "dir" || file.to === "dir" - ? - : } - - - {file.name} - - - + {file.name} + + + {cockpit.format_bytes(file.size)} + + + {timeformat.dateTime(file.mtime * 1000)} + + ); }); diff --git a/src/files-card-body.scss b/src/files-card-body.scss new file mode 100644 index 000000000..8f8356204 --- /dev/null +++ b/src/files-card-body.scss @@ -0,0 +1,195 @@ +.fileview { + .row-selected { + background-color: var(--pf-v5-c-card--m-selectable--m-selected--BackgroundColor); + } + + --icon-size: 32px; + border-collapse: collapse; + inline-size: 100%; + margin: var(--pf-v5-global--spacer--sm); + line-height: var(--pf-v5-global--LineHeight--md); + font-family: var(--pf-v5-global--FontFamily--text); + font-size: var(--pf-v5-global--FontSize--md); + block-size: 100%; + overflow: auto; + + th { + text-align: start; + } + + tr { + --color-folder: var(--pf-v5-global--primary-color--100); + --color-icon: var(--pf-v5-global--Color--400); + + &:focus, + &:focus-within { + --color-folder: var(--pf-v5-global--primary-color--200); + --color-icon: var(--pf-v5-global--Color--300); + } + } + + a:focus:not(:focus-visible) { + outline: none; + } + + a:focus-within { + outline-offset: var(--pf-v5-global--spacer--sm); + border-radius: var(--pf-v5-global--BorderWidth--md); + outline-style: dashed; + } + + .item-name { + // Bump up the font size to standard for the filenames + a { + font-size: var(--pf-v5-global--FontSize--md); + } + + a::before { + /* content-visibility: auto; */ + aspect-ratio: 1; + background-color: var(--color-icon); + block-size: var(--icon-size); + content: ''; + mask-repeat: no-repeat; + mask-position: center; + mask-image: url('data:image/svg+xml,'); + } + } + + tr.folder { + .item-name a::before { + background-color: var(--color-folder); + mask-image: url('data:image/svg+xml,'); + } + } + + a:not(:hover, :focus) { + color: inherit; + } + + // Propogate the hovering via variables to the link and cursor + tr:hover { + --pf-v5-global--link--Color: var(--pf-v5-global--link--Color--hover); + --pf-v5-global--link--TextDecoration: var(--pf-v5-global--link--TextDecoration--hover); + cursor: pointer; + } + + &.view-details .item-name a::before { + display: inline-block; + margin-inline-end: var(--pf-v5-global--spacer--sm); + // FIXME: Figure out why there's an offset + transform: translateY(3px); + } + + &.view-grid .item-name a { + display: block; + + &::before { + display: block; + margin-inline: auto; + margin-block-end: var(--pf-v5-global--spacer--sm); + } + } + + &.view-details { + --icon-size: 16px; + inline-size: calc(100% - var(--pf-v5-global--spacer--md)); + margin-block: var(--pf-v5-global--spacer--sm); + margin-inline: auto; + + tbody tr { + border-block: 1px solid var(--pf-v5-global--BorderColor--100); + } + + tbody > tr:hover { + background-color: var(--pf-v5-global--BackgroundColor--200); + } + + th, td { + padding: var(--pf-v5-global--spacer--sm); + border-block-start: none; + } + + .col-size, .item-size, + .col-date, .item-date { + inline-size: 12ch; + text-align: end; + } + + // Remove the extra padding on the end of the column and button, as the icon has padding already + .col-size, .col-date { + &, .pf-v5-c-table__button { + padding-inline-end: 0; + } + + .pf-v5-c-table__button { + inline-size: 100%; + justify-content: end; + } + } + } + + &.view-grid { + --icon-size: 64px; + display: contents; + + thead { + display: none; + } + + tbody { + align-items: start; + display: grid; + // As we're using justify-content: space-between, this is the minimum gap horizontally; + gap: var(--pf-v5-global--spacer--sm); + grid-template-columns: repeat(auto-fill, minmax(8rem,1fr)); + justify-content: space-between; + margin: var(--pf-v5-global--spacer--sm); + } + + tr { + display: block; + // Override default PF padding + padding: 0; + + td { + display: block; + text-align: center; + word-break: break-all; + // Override default PF padding + padding: 0; + + a { + padding: var(--pf-v5-global--spacer--sm); + } + + &:not(:last-child) a { + padding-block-end: 0; + } + + + td { + padding-block-end: var(--pf-v5-global--spacer--sm); + } + } + + .item-num { + display: none; + } + + .item-size, + .item-date { + font-size: 0.8em; + color: #888; + } + + .item-date { + display: none; + } + + &:hover { + background-color: var(--pf-v5-global--BackgroundColor--200); + outline: 1px solid var(--pf-v5-global--BackgroundColor--300); + } + } + } +} diff --git a/src/files-folder-view.tsx b/src/files-folder-view.tsx index e7135ef18..507dff57b 100644 --- a/src/files-folder-view.tsx +++ b/src/files-folder-view.tsx @@ -65,6 +65,7 @@ export const FilesFolderView = ({ path={path} isGrid={isGrid} sortBy={sortBy} + setSortBy={setSortBy} selected={selected} setSelected={setSelected} loadingFiles={loadingFiles} diff --git a/src/header.tsx b/src/header.tsx index 1494dd084..af4f0691b 100644 --- a/src/header.tsx +++ b/src/header.tsx @@ -35,12 +35,56 @@ import { TextContent, TextVariants } from "@patternfly/react-core"; +import { SortByDirection } from '@patternfly/react-table'; import { GripVerticalIcon, ListIcon } from "@patternfly/react-icons"; import { UploadButton } from "./upload-button"; const _ = cockpit.gettext; +export const filterColumns = [ + { + title: _("Name"), + [SortByDirection.asc]: { + itemId: "az", + label: _("A-Z"), + }, + [SortByDirection.desc]: { + itemId: "za", + label: _("Z-A"), + } + }, + { + title: _("Size"), + [SortByDirection.asc]: { + itemId: "largest_size", + label: _("Largest size"), + }, + [SortByDirection.desc]: { + itemId: "smallest_size", + label: _("Smallest size"), + } + }, + { + title: _("Modified"), + [SortByDirection.asc]: { + itemId: "first_modified", + label: _("First modified"), + }, + [SortByDirection.desc]: { + itemId: "last_modified", + label: _("Last modified"), + }, + }, +]; + +// { itemId: [index, sortdirection] } +export const filterColumnMapping = filterColumns.reduce((a, v, i) => ({ + ...a, + [v[SortByDirection.asc].itemId]: [i, SortByDirection.asc], + [v[SortByDirection.desc].itemId]: [i, SortByDirection.desc] +}), {}); + export const FilesCardHeader = ({ currentFilter, onFilterChange, @@ -135,10 +179,15 @@ const ViewSelector = ({ isGrid, setIsGrid, sortBy, setSortBy }: )} > - {_("A-Z")} - {_("Z-A")} - {_("Last modified")} - {_("First modified")} + {filterColumns.map((column, rowIndex) => + + + {column[SortByDirection.asc].label} + + + {column[SortByDirection.desc].label} + + )} ); diff --git a/test/check-application b/test/check-application index 9619fbae6..79f8529ab 100755 --- a/test/check-application +++ b/test/check-application @@ -122,7 +122,7 @@ class TestFiles(testlib.MachineCase): b.wait_in_text("#description-list-last-modified ", "Jan 1, 2022, 12:00 PM") # clicking empty space resets sidebar - b.click("#folder-view") + b.click("#folder-view tbody") b.wait_in_text("#sidebar-card-header", "admin") # folder information doesn't contain size @@ -134,13 +134,13 @@ class TestFiles(testlib.MachineCase): b.wait_text("#description-list-last-modified dd", "Jan 1, 2022, 12:00 PM") # filtering works - self.browser.wait_js_cond("ph_count('#folder-view > .pf-v5-c-card') > 1") + self.browser.wait_js_cond("ph_count('#folder-view tbody tr') > 1") b.set_input_text("input[placeholder='Filter directory']", "newfile") - self.browser.wait_js_cond("ph_count('#folder-view > .pf-v5-c-card') == 1") + self.browser.wait_js_cond("ph_count('#folder-view tbody tr') == 1") # no results when filtering b.set_input_text("input[placeholder='Filter directory']", "absolutelynothing") - self.browser.wait_js_cond("ph_count('#folder-view > .pf-v5-c-card') == 0") + self.browser.wait_js_cond("ph_count('#folder-view tbody tr') == 0") b.wait_text(".pf-v5-c-empty-state__body", "No matching results") # clear using input button @@ -224,7 +224,7 @@ class TestFiles(testlib.MachineCase): m.execute("ln -s /tmp /home/admin/tmplink") b.click("[data-item='tmplink']") b.wait_in_text("#sidebar-card-header", "symbolic link to /tmp") - b.mouse(".directory-item[data-item='tmplink']", "dblclick") + b.mouse("[data-item='tmplink']", "dblclick") b.wait_not_present("[data-item='tmplink']") b.wait_text(".pf-v5-c-page__main-breadcrumb > div > button:last-of-type", "tmplink") b.go("/files#/?path=/home/admin") @@ -262,8 +262,8 @@ class TestFiles(testlib.MachineCase): b.wait_text(".pf-v5-c-page__main-breadcrumb > div > button:last-of-type", "newdir2") b.eval_js("window.history.forward()") # Switching navigation resets selected state - b.wait_visible("#card-item-admindir") - b.wait_not_present("#card-item-admindir.pf-m-selected") + b.wait_visible("[data-item='admin']") + b.wait_not_present("[data-item='admin'].row-selected") b.wait_in_text("#sidebar-card-header", "home") b.wait_not_present("[data-item='newdir']") b.wait_text(".pf-v5-c-page__main-breadcrumb > div > button:last-of-type", "home") @@ -351,46 +351,61 @@ class TestFiles(testlib.MachineCase): # Default sort is A-Z # Alphabet sorts should be case insensetive - b.wait_text(".item-button:nth-of-type(1)", "aaa") - b.wait_text(".item-button:nth-of-type(2)", "BBB") - b.wait_text(".item-button:nth-of-type(3)", "ccc") + b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "aaa") + b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "BBB") + b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ccc") # Sort by reverse alphabet b.select_PF5("#sort-menu-toggle", "#sort-menu", "Z-A") # Alphabet sorts should be case insensetive - b.wait_text(".item-button:nth-of-type(1)", "ccc") - b.wait_text(".item-button:nth-of-type(2)", "BBB") - b.wait_text(".item-button:nth-of-type(3)", "aaa") + b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ccc") + b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "BBB") + b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "aaa") # Sort by last modified b.select_PF5("#sort-menu-toggle", "#sort-menu", "Last modified") - b.wait_text(".item-button:nth-of-type(1)", "ccc") - b.wait_text(".item-button:nth-of-type(2)", "aaa") - b.wait_text(".item-button:nth-of-type(3)", "BBB") + b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ccc") + b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "aaa") + b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "BBB") # Update content of files m.execute('echo "update" > /home/admin/aaa') - b.wait_text(".item-button:nth-of-type(1)", "aaa") - b.wait_text(".item-button:nth-of-type(2)", "ccc") - b.wait_text(".item-button:nth-of-type(3)", "BBB") + b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "aaa") + b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "ccc") + b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "BBB") # Sort by first modified b.select_PF5("#sort-menu-toggle", "#sort-menu", "First modified") - b.wait_text(".item-button:nth-of-type(1)", "BBB") - b.wait_text(".item-button:nth-of-type(2)", "ccc") - b.wait_text(".item-button:nth-of-type(3)", "aaa") + b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "BBB") + b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "ccc") + b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "aaa") # Sort option should be saved in localStorage b.select_PF5("#sort-menu-toggle", "#sort-menu", "Z-A") - b.wait_text(".item-button:nth-of-type(1)", "ccc") - b.wait_text(".item-button:nth-of-type(2)", "BBB") - b.wait_text(".item-button:nth-of-type(3)", "aaa") + b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ccc") + b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "BBB") + b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "aaa") b.reload() b.enter_page("/files") - b.wait_text(".item-button:nth-of-type(1)", "ccc") - b.wait_text(".item-button:nth-of-type(2)", "BBB") - b.wait_text(".item-button:nth-of-type(3)", "aaa") + b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ccc") + b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "BBB") + b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "aaa") + + # Sort on size + m.execute(""" + echo 'lol' > /home/admin/aaa + truncate -s 10M /home/admin/BBB + """) + b.select_PF5("#sort-menu-toggle", "#sort-menu", "Largest size") + b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "BBB") + b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "aaa") + b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ccc") + + b.select_PF5("#sort-menu-toggle", "#sort-menu", "Smallest size") + b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ccc") + b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "aaa") + b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "BBB") m.execute(""" ln -s /tmp /home/admin/ddd @@ -403,12 +418,12 @@ class TestFiles(testlib.MachineCase): # Directories are sorted first b.select_PF5("#sort-menu-toggle", "#sort-menu", "A-Z") - b.wait_text(".item-button:nth-of-type(1)", "ddd") - b.wait_text(".item-button:nth-of-type(2)", "eee") - b.wait_text(".item-button:nth-of-type(3)", "Eee") - b.wait_text(".item-button:nth-of-type(4)", "aaa") - b.wait_text(".item-button:nth-of-type(5)", "BBB") - b.wait_text(".item-button:nth-of-type(6)", "ccc") + b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ddd") + b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee") + b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "Eee") + b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "aaa") + b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "BBB") + b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "ccc") def testDelete(self): b = self.browser @@ -467,8 +482,8 @@ class TestFiles(testlib.MachineCase): m.execute("touch /home/admin/delete1 /home/admin/delete2") b.click("[data-item='delete1']") b.mouse("[data-item='delete2']", "click", ctrlKey=True) - b.wait_visible("[data-item='delete1'].pf-m-selected") - b.wait_visible("[data-item='delete2'].pf-m-selected") + b.wait_visible("[data-item='delete1'].row-selected") + b.wait_visible("[data-item='delete2'].row-selected") self.press_special_key("Delete") b.wait_in_text("h1.pf-v5-c-modal-box__title", "Delete 2 items?") b.click("button.pf-m-danger") @@ -538,7 +553,7 @@ class TestFiles(testlib.MachineCase): b.allow_download() # Create folder from context menu - b.mouse("#folder-view", "contextmenu") + b.mouse("#files-card-parent", "contextmenu") b.click(".contextMenu button:contains('Create directory')") b.set_input_text("#create-directory-input", "newdir") b.click("button.pf-m-primary") @@ -546,7 +561,7 @@ class TestFiles(testlib.MachineCase): # Opening context menu from empty space deselects item b.click("[data-item='newdir']") - b.mouse("#folder-view", "contextmenu") + b.mouse("#files-card-parent tbody", "contextmenu") b.click(".contextMenu button:contains('Create directory')") b.set_input_text("#create-directory-input", "newdir2") b.click("button.pf-m-primary") @@ -763,7 +778,7 @@ class TestFiles(testlib.MachineCase): b.wait_not_present(".pf-v5-c-empty-state") # Via contextmenu - b.mouse("#folder-view", "contextmenu") + b.mouse("#files-card-parent", "contextmenu") b.click(".contextMenu button:contains('Edit permissions')") b.wait_in_text(".pf-v5-c-modal-box__title-text", "testdir") b.select_from_dropdown("#edit-permissions-owner-access", "6") @@ -848,27 +863,27 @@ class TestFiles(testlib.MachineCase): m.execute("touch /home/admin/file1 && touch /home/admin/file2") b.click("[data-item='file1']") b.mouse("[data-item='file2']", "click", ctrlKey=True) - b.wait_visible("[data-item='file1'].pf-m-selected") - b.wait_visible("[data-item='file2'].pf-m-selected") + b.wait_visible("[data-item='file1'].row-selected") + b.wait_visible("[data-item='file2'].row-selected") b.wait_text("#sidebar-card-header", "admin2 items selected") b.mouse("[data-item='file2']", "click", ctrlKey=True) - b.wait_visible("[data-item='file1'].pf-m-selected") - b.wait_not_present("[data-item='file2'].pf-m-selected") + b.wait_visible("[data-item='file1'].row-selected") + b.wait_not_present("[data-item='file2'].row-selected") b.wait_text("#sidebar-card-header", "file1empty") b.mouse("[data-item='file1']", "click", ctrlKey=True) - b.wait_not_present("[data-item='file1'].pf-m-selected") + b.wait_not_present("[data-item='file1'].row-selected") b.wait_in_text("#sidebar-card-header", "admin") # Control-clicking when nothing is selected should select item normally b.mouse("[data-item='file1']", "click", ctrlKey=True) - b.wait_visible("[data-item='file1'].pf-m-selected") + b.wait_visible("[data-item='file1'].row-selected") b.wait_text("#sidebar-card-header", "file1empty") # Check context menu b.mouse("[data-item='file2']", "click", ctrlKey=True) - b.wait_visible("[data-item='file2'].pf-m-selected") + b.wait_visible("[data-item='file2'].row-selected") b.wait_text("#sidebar-card-header", "admin2 items selected") b.mouse("[data-item='file1']", "contextmenu") b.wait_in_text(".contextMenu li:nth-child(2) button", "Delete") @@ -904,28 +919,28 @@ class TestFiles(testlib.MachineCase): b.eval_js("window.focus()") b.click("[data-item='file0']") - b.wait_visible("[data-item='file0'].pf-m-selected") + b.wait_visible("[data-item='file0'].row-selected") self.press_special_key("ArrowRight") - b.wait_visible("[data-item='file1'].pf-m-selected") + b.wait_visible("[data-item='file1'].row-selected") self.press_special_key("ArrowLeft") - b.wait_visible("[data-item='file0'].pf-m-selected") + b.wait_visible("[data-item='file0'].row-selected") # Up / Down depends on the layout, this is tested on mobile where the # width is two cards. b.set_layout("mobile") b.click("[data-item='file0']") - b.wait_visible("[data-item='file0'].pf-m-selected") + b.wait_visible("[data-item='file0'].row-selected") self.press_special_key("ArrowDown") - b.wait_visible("[data-item='file2'].pf-m-selected") + b.wait_visible("[data-item='file2'].row-selected") self.press_special_key("ArrowUp") - b.wait_visible("[data-item='file0'].pf-m-selected") + b.wait_visible("[data-item='file0'].row-selected") m.execute("mkdir /home/admin/foo") b.click("[data-item='foo']") - b.wait_visible("[data-item='foo'].pf-m-selected") + b.wait_visible("[data-item='foo'].row-selected") self.press_special_key("Enter") b.wait_text(".breadcrumb-button:nth-of-type(5)", "foo")