diff --git a/frontend-web/webclient/app/Files/FileBrowse.tsx b/frontend-web/webclient/app/Files/FileBrowse.tsx index 3c09b8f4f6..464c784233 100644 --- a/frontend-web/webclient/app/Files/FileBrowse.tsx +++ b/frontend-web/webclient/app/Files/FileBrowse.tsx @@ -20,6 +20,7 @@ import { favoriteRowIcon, ResourceBrowseHeaderControls, createProjectSwitcherPortal, + OperationGroup, } from "@/ui-components/ResourceBrowser"; import FilesApi, { addFileSensitivityDialog, @@ -653,15 +654,46 @@ function FileBrowse({ browser.on("fetchOperations", () => { function groupOperations(ops: Operation[]): OperationOrGroup[] { const result: OperationOrGroup[] = []; + const restOperations: Operation[] = []; + let splitButtonGroups = new Map[]>(); + + // Finding split buttons and grouping them together + ops.forEach(op => { + if (op.splitButtonGroupId) { + if (!splitButtonGroups.has(op.splitButtonGroupId)){ + splitButtonGroups.set(op.splitButtonGroupId, [op]); + } + else { + splitButtonGroups.get(op.splitButtonGroupId)?.push(op); + } + } + else { + restOperations.push(op); + } + }); + + // Creating split buttons + splitButtonGroups.forEach((operations, k) => { + result.push({ + color: "secondaryMain", + icon: "ellipsis", + text: "", + iconRotation: 90, + operations: operations, + buttonStyle: "split", + }) + }); + let i = 0; - for (; i < ops.length && result.length < 4; i++) { - const op = ops[i]; + // A max of 4 buttons for the view else we collapse them + for (; i < restOperations.length && result.length < 4; i++) { + const op = restOperations[i]; result.push(op); } const overflow: Operation[] = []; - for (; i < ops.length; i++) { - overflow.push(ops[i]); + for (; i < restOperations.length; i++) { + overflow.push(restOperations[i]); } if (overflow.length > 0) { diff --git a/frontend-web/webclient/app/UCloud/FilesApi.tsx b/frontend-web/webclient/app/UCloud/FilesApi.tsx index 0a5a5f8887..636504470c 100644 --- a/frontend-web/webclient/app/UCloud/FilesApi.tsx +++ b/frontend-web/webclient/app/UCloud/FilesApi.tsx @@ -312,7 +312,9 @@ class FilesApi extends ResourceApi cb.startFolderCreation!(), - shortcut: ShortcutKey.F + shortcut: ShortcutKey.F, + splitButtonGroupId: 'createOperations', + color: "secondaryMain" }, { @@ -651,7 +653,9 @@ class FilesApi extends ResourceApi { cb.startFileCreation!(); }, - shortcut: ShortcutKey.L + shortcut: ShortcutKey.L, + splitButtonGroupId: "createOperations", + color: "secondaryDark" }, { icon: "trash", diff --git a/frontend-web/webclient/app/ui-components/Operation.tsx b/frontend-web/webclient/app/ui-components/Operation.tsx index a5bbca1762..6ca1f4123a 100644 --- a/frontend-web/webclient/app/ui-components/Operation.tsx +++ b/frontend-web/webclient/app/ui-components/Operation.tsx @@ -73,6 +73,7 @@ export interface Operation { primary?: boolean; confirm?: boolean; tag?: string; + splitButtonGroupId?: string } export function defaultOperationType( diff --git a/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx b/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx index 2a4b7b586c..f66a143722 100644 --- a/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx +++ b/frontend-web/webclient/app/ui-components/ResourceBrowser.tsx @@ -1,4 +1,4 @@ -import {Operation, ShortcutKey} from "@/ui-components/Operation"; +import {Operation, OperationEnabled, ShortcutKey} from "@/ui-components/Operation"; import {IconName} from "@/ui-components/Icon"; import { ThemeColor, @@ -50,6 +50,9 @@ import {callAPI, noopCall} from "@/Authentication/DataHook"; import {injectResourceBrowserStyle, ShortcutClass} from "./ResourceBrowserStyle"; import {ASC, DESC, Filter, FilterCheckbox, FilterInput, FilterOption, FilterWithOptions, MultiOption, MultiOptionFilter, SORT_BY, SORT_DIRECTION} from "./ResourceBrowserFilters"; import {sendInformationNotification} from "@/Notifications"; +import { UFile } from "@/UCloud/UFile"; +import ReactClient from "react-dom/client"; +import {VmActionItem, VmActionSplitButton} from "@/Applications/Jobs/VmActionSplitButton"; const CLEAR_FILTER_VALUE = "\n\nCLEAR_FILTER\n\n"; const UTILITY_COLOR: ThemeColor = "textPrimary"; @@ -138,6 +141,8 @@ export interface OperationGroup { backgroundColor?: ThemeColor, operations: Operation[]; iconRotation?: number; + operationGroupId?: string; + buttonStyle?: string } export enum SelectionMode { @@ -1569,6 +1574,67 @@ export class ResourceBrowser { } } + private renderVmActionSplitButton( + opGroup: OperationGroup, + selected: T[], + callbacks: unknown, + page: T[] + ): HTMLElement { + const container = document.createElement("div"); + container.className = "operation"; + container.style.display = "flex"; + + const root = ReactClient.createRoot(container); + + const mainOp = opGroup.operations[0]; + const rest = opGroup.operations.slice(1); + + const enableResult: OperationEnabled = mainOp.enabled(selected, callbacks, page); + const isEnabled = enableResult === true; + + + const getText = (op): string => { + return typeof op.text === "string" ? op.text : op.text(selected, callbacks); + } + + // Rest are menu items + const menuItems: VmActionItem[] = rest.map((childOp, idx) => ({ + key: idx.toString(), + value: getText(childOp), + icon: childOp.icon, + color: childOp.color ?? "primaryMain" + + })); + + root.render( +
+ { + const foundOp = rest[parseInt(item.key)]; + if (foundOp && foundOp.enabled(selected, callbacks, page) === true) { + foundOp.onClick(selected, callbacks, page); + } + }} + onButtonClick={() => { + if (isEnabled) { + console.log("Enabled", mainOp); + mainOp.onClick(selected, callbacks, page); + } + }} + /> +
+ ); + + return container; + } + + private renderOperationsIn(useContextMenu: boolean, contextOpts?: { x: number, y: number, @@ -1879,7 +1945,13 @@ export class ResourceBrowser { const target = this.operations; target.innerHTML = ""; for (const op of operations) { - target.append(renderOperation(op)); + if (op.buttonStyle === "split") { + // Rendering SplitButton + target.append(this.renderVmActionSplitButton(op, selected, callbacks, page)); + } + else { + target.append(renderOperation(op)); + } } } else { const posX = contextOpts?.x ?? 0; @@ -3868,3 +3940,4 @@ export function favoriteRowIcon(row: ResourceBrowserRow) { } return favoriteIcon; } +