diff --git a/web/src/core/usecases/fileExplorer/selectors.ts b/web/src/core/usecases/fileExplorer/selectors.ts index bc8c40375..641d85800 100644 --- a/web/src/core/usecases/fileExplorer/selectors.ts +++ b/web/src/core/usecases/fileExplorer/selectors.ts @@ -72,28 +72,41 @@ const currentWorkingDirectoryView = createSelector( if (directoryPath === undefined) { return undefined; } - const items = objects.map(object => { - const isBeingUploaded = ongoingOperations.some( - op => op.operation === "create" && op.object.basename === object.basename - ); - - const isBeingDeleted = ongoingOperations.some( - op => op.operation === "delete" && op.object.basename === object.basename - ); - - const isPolicyChanging = ongoingOperations.some( - op => - op.operation === "modifyPolicy" && - op.object.basename === object.basename - ); - - return { - ...object, - isBeingUploaded, - isBeingDeleted, - isPolicyChanging - }; - }); + const items = objects + .map(object => { + const isBeingUploaded = ongoingOperations.some( + op => + op.operation === "create" && + op.object.basename === object.basename + ); + + const isBeingDeleted = ongoingOperations.some( + op => + op.operation === "delete" && + op.object.basename === object.basename + ); + + const isPolicyChanging = ongoingOperations.some( + op => + op.operation === "modifyPolicy" && + op.object.basename === object.basename + ); + + return { + ...object, + isBeingUploaded, + isBeingDeleted, + isPolicyChanging + }; + }) + .sort((a, b) => { + // Sort directories first + if (a.kind === "directory" && b.kind !== "directory") return -1; + if (a.kind !== "directory" && b.kind === "directory") return 1; + + // Sort alphabetically by basename + return a.basename.localeCompare(b.basename); + }); return { directoryPath, diff --git a/web/src/core/usecases/fileExplorer/thunks.ts b/web/src/core/usecases/fileExplorer/thunks.ts index ecf4c012a..60c3d3d37 100644 --- a/web/src/core/usecases/fileExplorer/thunks.ts +++ b/web/src/core/usecases/fileExplorer/thunks.ts @@ -29,15 +29,28 @@ export declare namespace ExplorersCreateParams { const privateThunks = { "createOperation": - (params: { operation: "create" | "delete" | "modifyPolicy"; object: S3Object }) => + (params: { + operation: "create" | "delete" | "modifyPolicy"; + object: S3Object; + directoryPath: string; + }) => async (...args) => { const [dispatch, ,] = args; - const { operation, object } = params; + const { operation, object, directoryPath } = params; const operationId = `${operation}-${Date.now()}`; dispatch(actions.operationStarted({ operationId, object, operation })); + + await dispatch( + privateThunks.waitForNoOngoingOperation({ + "kind": object.kind, + directoryPath, + "basename": object.basename, + "ignoreOperationId": operationId + }) + ); return operationId; }, "waitForNoOngoingOperation": @@ -45,11 +58,12 @@ const privateThunks = { kind: "file" | "directory"; basename: string; directoryPath: string; + ignoreOperationId?: string; }) => async (...args) => { const [, getState, { evtAction }] = args; - const { kind, basename, directoryPath } = params; + const { kind, basename, directoryPath, ignoreOperationId } = params; const { ongoingOperations } = getState()[name]; @@ -57,7 +71,8 @@ const privateThunks = { o => o.object.kind === kind && o.object.basename === basename && - o.directoryPath === directoryPath + o.directoryPath === directoryPath && + o.operationId !== ignoreOperationId ); if (ongoingOperation === undefined) { @@ -257,16 +272,20 @@ export const thunks = { const [dispatch, getState] = args; - const object = getState()[name].objects.find( - o => o.basename === basename && o.kind === kind - ); + const state = getState()[name]; + + const { directoryPath, objects } = state; + + const object = objects.find(o => o.basename === basename && o.kind === kind); assert(object !== undefined); + assert(directoryPath !== undefined); const operationId = await dispatch( privateThunks.createOperation({ operation: "modifyPolicy", - object: { ...object, policy } + object: { ...object, policy }, + directoryPath }) ); const s3Client = await dispatch( @@ -276,10 +295,6 @@ export const thunks = { return r.s3Client; }); - const { directoryPath } = getState()[name]; - - assert(directoryPath !== undefined); - const filePath = pathJoin(directoryPath, basename); const s3Prefix = pathJoin("s3", filePath); @@ -358,14 +373,6 @@ export const thunks = { assert(directoryPath !== undefined); - await dispatch( - privateThunks.waitForNoOngoingOperation({ - "kind": params.createWhat, - directoryPath, - "basename": params.basename - }) - ); - const operationId = await dispatch( privateThunks.createOperation({ object: { @@ -375,6 +382,7 @@ export const thunks = { size: undefined, lastModified: undefined }, + directoryPath, operation: "create" }) ); @@ -498,16 +506,12 @@ export const thunks = { assert(directoryPath !== undefined); - await dispatch( - privateThunks.waitForNoOngoingOperation({ - "kind": s3Object.kind, - directoryPath, - basename: s3Object.basename - }) - ); - const operationId = await dispatch( - privateThunks.createOperation({ operation: "delete", object: s3Object }) + privateThunks.createOperation({ + operation: "delete", + object: s3Object, + directoryPath + }) ); const s3Client = await dispatch( @@ -542,13 +546,17 @@ export const thunks = { { const { crawl } = crawlFactory({ "list": async ({ directoryPath }) => { - const { directories, files } = await s3Client.list({ + const { objects } = await s3Client.listObjects({ "path": directoryPath }); return { - "fileBasenames": files, - "directoryBasenames": directories + "fileBasenames": objects + .filter(object => object.kind === "file") + .map(object => object.basename), + "directoryBasenames": objects + .filter(object => object.kind === "directory") + .map(object => object.basename) }; } }); diff --git a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx index cb0002ab0..9688d239d 100644 --- a/web/src/ui/pages/myFiles/Explorer/Explorer.tsx +++ b/web/src/ui/pages/myFiles/Explorer/Explorer.tsx @@ -249,7 +249,10 @@ export const Explorer = memo((props: ExplorerProps) => { ); const itemsOnDeleteItem = useConstCallback( - async ({ item }: Parameters[0]) => { + async ( + { item }: Parameters[0], + onDeleteConfirmed?: () => void // Callback optionnelle pour après la suppression + ) => { if (doShowDeletionDialogNextTime) { const dDoProceedToDeletion = new Deferred(); @@ -269,6 +272,7 @@ export const Explorer = memo((props: ExplorerProps) => { } onDeleteItem({ item }); + onDeleteConfirmed?.(); } ); diff --git a/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItem.stories.tsx b/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItem.stories.tsx index f4b352051..b20e1797e 100644 --- a/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItem.stories.tsx +++ b/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItem.stories.tsx @@ -20,6 +20,9 @@ export const FileSelected: Story = { policy: "private", className: css({ "width": "160px", "height": "160px" }), isSelected: true, + isCircularProgressShown: false, // Valeur par défaut pour l'animation + isPolicyChanging: false, // Pas de changement de politique en cours + onPolicyChange: action("onPolicyChange"), // Action pour les changements de politique onClick: action("onClick"), onDoubleClick: action("onDoubleClick") } @@ -30,9 +33,12 @@ export const DirectoryUnselected: Story = { kind: "directory", basename: "example-directory", size: undefined, - "policy": "public", + policy: "public", className: css({ "width": "160px", "height": "160px" }), isSelected: false, + isCircularProgressShown: false, // Valeur par défaut pour l'animation + isPolicyChanging: false, // Pas de changement de politique en cours + onPolicyChange: action("onPolicyChange"), // Action pour les changements de politique onClick: action("onClick"), onDoubleClick: action("onDoubleClick") } diff --git a/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItem.tsx b/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItem.tsx index 273c509e6..43e34da73 100644 --- a/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItem.tsx +++ b/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItem.tsx @@ -1,6 +1,6 @@ import { tss } from "tss"; import { Text } from "onyxia-ui/Text"; -import { memo, useState } from "react"; +import { memo } from "react"; import { Icon } from "onyxia-ui/Icon"; import { MuiIconComponentName } from "onyxia-ui/MuiIconComponentName"; import { id } from "tsafe"; @@ -24,6 +24,7 @@ export type ExplorerItemProps = { isSelected: boolean; isCircularProgressShown: boolean; + isPolicyChanging: boolean; /** File size in bytes */ size: number | undefined; @@ -44,7 +45,8 @@ export const ExplorerItem = memo((props: ExplorerItemProps) => { size, onDoubleClick, onPolicyChange, - onClick + onClick, + isPolicyChanging } = props; const prettySize = size ? fileSizePrettyPrint({ bytes: size }) : null; @@ -99,6 +101,7 @@ export const ExplorerItem = memo((props: ExplorerItemProps) => { policy={policy} className={classes.policyIcon} changePolicy={onPolicyChange} + isPolicyChanging={isPolicyChanging} /> diff --git a/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItems.stories.tsx b/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItems.stories.tsx index 152a360ad..ddd5359ff 100644 --- a/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItems.stories.tsx +++ b/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItems.stories.tsx @@ -13,39 +13,54 @@ export default meta; type Story = StoryObj; -const itemsSample = [ +const itemsSample: Item[] = [ { kind: "file", basename: "document.pdf", - size: 1024000, // in bytes + size: 1024000, // en bytes lastModified: new Date("2023-10-01"), - policy: "private" + policy: "private", + isBeingUploaded: false, + isBeingDeleted: false, + isPolicyChanging: false }, { kind: "file", basename: "photo.png", - size: 2048000, // in bytes + size: 2048000, // en bytes lastModified: new Date("2023-09-15"), - policy: "public" + policy: "public", + isBeingUploaded: false, + isBeingDeleted: false, + isPolicyChanging: false }, { kind: "directory", basename: "Projects", - policy: "private" + policy: "private", + isBeingUploaded: false, + isBeingDeleted: false, + isPolicyChanging: false }, { kind: "file", basename: "presentation.pptx", - size: 5120000, // in bytes + size: 5120000, // en bytes lastModified: new Date("2023-09-20"), - policy: "private" + policy: "private", + isBeingUploaded: false, + isBeingDeleted: false, + isPolicyChanging: false }, { kind: "directory", basename: "Photos", - policy: "public" + policy: "public", + isBeingUploaded: false, + isBeingDeleted: false, + isPolicyChanging: false } -] satisfies Item[]; +]; export const Default: Story = { args: { @@ -56,6 +71,7 @@ export const Default: Story = { onDeleteItem: action("Delete item"), onCopyPath: action("Copy path"), onSelectedItemKindValueChange: action("Selected item kind changed"), + onPolicyChange: action("Policy change"), evtAction: Evt.create<"DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH">() } }; @@ -68,6 +84,7 @@ export const EmptyDirectory: Story = { onOpenFile: action("Open file"), onDeleteItem: action("Delete item"), onCopyPath: action("Copy path"), + onPolicyChange: action("Policy change"), onSelectedItemKindValueChange: action("Selected item kind changed"), evtAction: Evt.create<"DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH">() } diff --git a/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItems.tsx b/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItems.tsx index b45561bb9..3a244d502 100644 --- a/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItems.tsx +++ b/web/src/ui/pages/myFiles/Explorer/ExplorerItems/ExplorerItems.tsx @@ -151,8 +151,9 @@ export const ExplorerItems = memo((props: ExplorerItemsProps) => { onClick={handleItemClick(item)} onDoubleClick={handleItemDoubleClick(item)} isCircularProgressShown={ - isBeingDeleted || isBeingUploaded || isPolicyChanging + isBeingDeleted || isBeingUploaded } + isPolicyChanging={isPolicyChanging} /> ); })} diff --git a/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.stories.tsx b/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.stories.tsx index 5d8ed285f..7ca203ea3 100644 --- a/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.stories.tsx +++ b/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ListExplorerItems } from "./ListExplorerItems"; import { action } from "@storybook/addon-actions"; import { Evt } from "evt"; +import { Item } from "../../shared/types"; const meta = { title: "Pages/MyFiles/Explorer/ListExplorerItems", @@ -12,84 +13,55 @@ export default meta; type Story = StoryObj; -const itemsSample: ListExplorerItems["items"] = [ +const itemsSample: Item[] = [ { kind: "file", - size: 2048, basename: "document.pdf", - lastModified: new Date("2023-09-14T10:34:00Z"), - policy: "public" - }, - { - kind: "directory", - basename: "photos", - policy: "private" + size: 1024000, // en bytes + lastModified: new Date("2023-10-01"), + policy: "private", + isBeingUploaded: false, + isBeingDeleted: false, + isPolicyChanging: false }, { kind: "file", - size: 1048576, - basename: "video.mp4", - lastModified: new Date("2023-12-01T08:12:45Z"), - policy: "private" + basename: "photo.png", + size: 2048000, // en bytes + lastModified: new Date("2023-09-15"), + policy: "public", + isBeingUploaded: false, + isBeingDeleted: false, + isPolicyChanging: false }, { kind: "directory", - basename: "projects", - policy: "private" - }, - { - kind: "file", - size: 512, - basename: "notes.txt", - lastModified: new Date("2024-09-10T11:42:25Z"), - policy: "public" + basename: "Projects", + policy: "private", + isBeingUploaded: false, + isBeingDeleted: false, + isPolicyChanging: false }, { kind: "file", - size: 12345, basename: "presentation.pptx", - lastModified: new Date("2023-06-20T09:15:00Z"), - policy: "public" - }, - { - kind: "file", - size: 789456, - basename: "music.mp3", - lastModified: new Date("2023-11-11T12:45:00Z"), - policy: "private" - }, - { - kind: "directory", - basename: "archive", - policy: "private" - }, - { - kind: "file", - size: 500, - basename: "readme.md", - lastModified: new Date("2024-02-15T10:00:00Z"), - policy: "public" + size: 5120000, // en bytes + lastModified: new Date("2023-09-20"), + policy: "private", + isBeingUploaded: false, + isBeingDeleted: false, + isPolicyChanging: false }, { kind: "directory", - basename: "backup", - policy: "private" - }, - { - kind: "file", - size: 4096, - basename: "spreadsheet.xlsx", - lastModified: new Date("2023-08-10T08:30:00Z"), - policy: "private" - }, - { - kind: "file", - size: 20480, - basename: "photo.jpg", - lastModified: new Date("2024-03-12T15:40:00Z"), - policy: "public" + basename: "Photos", + policy: "public", + isBeingUploaded: false, + isBeingDeleted: false, + isPolicyChanging: false } ]; + export const Default: Story = { args: { isNavigating: false, @@ -97,6 +69,7 @@ export const Default: Story = { onNavigate: action("Navigate to directory"), onOpenFile: action("Open file"), onDeleteItem: action("Delete item"), + onPolicyChange: action("Policy change"), onCopyPath: action("Copy path"), onSelectedItemKindValueChange: action("Selected item kind changed"), evtAction: Evt.create<"DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH">() diff --git a/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.tsx b/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.tsx index 035211985..f645dd8be 100644 --- a/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.tsx +++ b/web/src/ui/pages/myFiles/Explorer/ListExplorer/ListExplorerItems.tsx @@ -3,26 +3,23 @@ import { type GridRowSelectionModel, type GridColDef, type GridCallbackDetails, - GridRenderCellParams, - GridRowParams, - GRID_CHECKBOX_SELECTION_COL_DEF + type GridRowParams, + GRID_CHECKBOX_SELECTION_COL_DEF, + useGridApiRef } from "@mui/x-data-grid"; import { memo, useMemo, useState } from "react"; import { ExplorerIcon } from "../ExplorerIcon"; import { tss } from "tss"; import { Text } from "onyxia-ui/Text"; -import { useEffectOnValueChange } from "powerhooks/useEffectOnValueChange"; import { fileSizePrettyPrint } from "ui/tools/fileSizePrettyPrint"; -import { id } from "tsafe"; import { CustomDataGrid } from "ui/shared/Datagrid/CustomDataGrid"; import type { Item } from "../../shared/types"; import { useConstCallback } from "powerhooks/useConstCallback"; import { assert } from "tsafe/assert"; import { useEvt } from "evt/hooks"; import type { NonPostableEvt } from "evt"; -import { useConst } from "powerhooks/useConst"; import { PolicySwitch } from "../PolicySwitch"; -import { useCallbackFactory } from "powerhooks/useCallbackFactory"; +import { CircularProgress } from "onyxia-ui/CircularProgress"; export type ListExplorerItems = { className?: string; @@ -43,13 +40,15 @@ export type ListExplorerItems = { kind: Item["kind"]; }) => void; - onDeleteItem: (params: { item: Item }) => void; + onDeleteItem: (params: { item: Item }, onDeleteConfirmed?: () => void) => void; onCopyPath: (params: { basename: string }) => void; evtAction: NonPostableEvt< "DELETE SELECTED ITEM" | "COPY SELECTED ITEM PATH" //TODO: Delete, legacy from secret explorer >; }; +type Row = Item & { id: number }; + export const ListExplorerItems = memo((props: ListExplorerItems) => { const { className, @@ -64,29 +63,31 @@ export const ListExplorerItems = memo((props: ListExplorerItems) => { onSelectedItemKindValueChange } = props; + const apiRef = useGridApiRef(); + const { classes, cx } = useStyles(); const [rowSelectionModel, setRowSelectionModel] = useState([]); - const selectedItemRef = useConst(() => ({ - current: id({ - basename: undefined, - kind: "none" - }) - })); - const columns: GridColDef<(typeof rows)[number]>[] = useMemo( () => [ { ...GRID_CHECKBOX_SELECTION_COL_DEF, - maxWidth: 100 + maxWidth: 50, + renderCell: params => { + if (params.row.isBeingDeleted || params.row.isBeingUploaded) + return ; + + assert(GRID_CHECKBOX_SELECTION_COL_DEF.renderCell !== undefined); + + return GRID_CHECKBOX_SELECTION_COL_DEF.renderCell(params); + } }, { field: "basename", headerName: "Name", type: "string", - display: "flex" as const, renderCell: params => ( <> { /> {params.value} - ) + ), + cellClassName: classes.basenameCell }, { field: "size", @@ -152,6 +154,7 @@ export const ListExplorerItems = memo((props: ListExplorerItems) => { } e.stopPropagation(); }} + isPolicyChanging={params.row.isPolicyChanging} /> ); } @@ -167,7 +170,7 @@ export const ListExplorerItems = memo((props: ListExplorerItems) => { ({ ...item, id: index // Maybe a better id is necessary due to pagination - }) satisfies Item & { id: number } + }) satisfies Row ), [items] ); @@ -175,15 +178,22 @@ export const ListExplorerItems = memo((props: ListExplorerItems) => { useEvt( ctx => evtAction.attach(ctx, action => { + const selectedItem = apiRef.current.getSelectedRows().values().next() + .value as Row; switch (action) { - case "DELETE SELECTED ITEM": - assert(selectedItemRef.current.kind !== "none"); - onDeleteItem({ "item": selectedItemRef.current }); + case "DELETE SELECTED ITEM": { + assert(selectedItem !== undefined); + onDeleteItem({ "item": selectedItem }, () => + apiRef.current.updateRows([ + { id: selectedItem.id, _action: "delete" } + ]) + ); break; + } case "COPY SELECTED ITEM PATH": - assert(selectedItemRef.current.kind !== "none"); + assert(selectedItem !== undefined); onCopyPath({ - "basename": selectedItemRef.current.basename + "basename": selectedItem.basename }); break; } @@ -191,14 +201,10 @@ export const ListExplorerItems = memo((props: ListExplorerItems) => { [evtAction, onDeleteItem, onCopyPath] ); - useEffectOnValueChange(() => { - selectedItemRef.current = { basename: undefined, kind: "none" }; - }, [isNavigating]); - const handleRowSelection = useConstCallback( (params: GridRowSelectionModel, details: GridCallbackDetails) => { - const selectedRows = details.api.getSelectedRows(); - const firstSelectedRow = selectedRows.values().next().value; + const previousSelectedRows = details.api.getSelectedRows(); + const firstPreviouslySelectedRow = previousSelectedRows.values().next().value; const rowIndex = params[0]; @@ -207,7 +213,8 @@ export const ListExplorerItems = memo((props: ListExplorerItems) => { const selectedItemKind = params.length === 0 ? "none" - : firstSelectedRow && firstSelectedRow.kind === rows[rowIndex].kind + : firstPreviouslySelectedRow && + firstPreviouslySelectedRow.kind === rows[rowIndex].kind ? undefined // No need to update the kind if it hasn't changed : rows[rowIndex].kind; @@ -216,7 +223,6 @@ export const ListExplorerItems = memo((props: ListExplorerItems) => { } setRowSelectionModel(params); - selectedItemRef.current = rows[rowIndex]; } ); @@ -234,7 +240,8 @@ export const ListExplorerItems = memo((props: ListExplorerItems) => { return (
- + apiRef={apiRef} rows={rows} columns={columns} initialState={{ @@ -278,5 +285,10 @@ const useStyles = tss.withName({ ListExplorerItems }).create(({ theme }) => ({ "height": "30px", "marginRight": theme.spacing(2), "flexShrink": 0 + }, + "basenameCell": { + "cursor": "pointer", + "display": "flex", + "alignItems": "center" } })); diff --git a/web/src/ui/pages/myFiles/Explorer/PolicySwitch.stories.tsx b/web/src/ui/pages/myFiles/Explorer/PolicySwitch.stories.tsx index 6fa7835c0..af9195cac 100644 --- a/web/src/ui/pages/myFiles/Explorer/PolicySwitch.stories.tsx +++ b/web/src/ui/pages/myFiles/Explorer/PolicySwitch.stories.tsx @@ -27,7 +27,8 @@ type Story = StoryObj; export const Default: Story = { args: { changePolicy: action("Change policy"), - policy: "private" + policy: "private", + isPolicyChanging: false }, render: props => { const { changePolicy, ...rest } = props; @@ -45,3 +46,32 @@ export const Default: Story = { ); } }; + +export const Loading: Story = { + args: { + changePolicy: action("Change policy"), + policy: "private", + isPolicyChanging: false + }, + render: props => { + const { changePolicy, ...rest } = props; + const [policy, setPolicy] = useState(props.policy); + const [isPolicyChanging, setIsPolicyChanging] = useState(props.isPolicyChanging); + + return ( + { + changePolicy(e); + setIsPolicyChanging(true); + setTimeout(() => { + setPolicy(prev => (prev === "private" ? "public" : "private")); + setIsPolicyChanging(false); + }, 3000); // 1-second loading simulation + }} + /> + ); + } +}; diff --git a/web/src/ui/pages/myFiles/Explorer/PolicySwitch.tsx b/web/src/ui/pages/myFiles/Explorer/PolicySwitch.tsx index a29ab7902..10b815b12 100644 --- a/web/src/ui/pages/myFiles/Explorer/PolicySwitch.tsx +++ b/web/src/ui/pages/myFiles/Explorer/PolicySwitch.tsx @@ -13,19 +13,21 @@ type Props = { ariaLabel?: string; policy: Item["policy"]; changePolicy: (e: React.MouseEvent) => void; + isPolicyChanging: boolean; // New loading prop }; export const PolicySwitch = memo((props: Props) => { - const { className, size, policy, changePolicy, ariaLabel } = props; + const { className, size, policy, changePolicy, ariaLabel, isPolicyChanging } = props; const isPublic = policy === "public"; - const { classes, cx } = useStyles({ isPublic }); + const { classes, cx } = useStyles({ isPublic, isPolicyChanging }); return ( () - .create(({ isPublic }) => ({ + .create(({ isPublic, isPolicyChanging }) => ({ "root": { - "transition": "transform 500ms", - "transform": `rotate(${isPublic ? 180 : 0}deg)`, - "transitionTimingFunction": "cubic-bezier(.34,1.27,1,1)" + "animation": isPolicyChanging + ? `${isPublic ? "spinClockwise" : "spinCounterClockwise"} 1s linear infinite` + : "none", // Apply the corresponding animation + "@keyframes spinClockwise": { + "0%": { "transform": "rotate(0deg)" }, + "100%": { "transform": "rotate(360deg)" } + }, + "@keyframes spinCounterClockwise": { + "0%": { "transform": "rotate(0deg)" }, + "100%": { "transform": "rotate(-360deg)" } + } } })); diff --git a/web/src/ui/shared/Datagrid/CustomDataGrid.tsx b/web/src/ui/shared/Datagrid/CustomDataGrid.tsx index 44a68e905..8410d2496 100644 --- a/web/src/ui/shared/Datagrid/CustomDataGrid.tsx +++ b/web/src/ui/shared/Datagrid/CustomDataGrid.tsx @@ -5,7 +5,7 @@ import { type GridClasses, type GridColDef } from "@mui/x-data-grid"; -import { type ComponentProps, memo, useMemo } from "react"; +import { type ComponentProps, useMemo } from "react"; import { tss } from "tss"; import { CopyToClipboardIconButton } from "ui/shared/CopyToClipboardIconButton"; import { CustomNoRowsOverlay } from "./CustomNoRowsOverlay"; @@ -26,7 +26,9 @@ export const autosizeOptions = { includeOutliers: false }; -export const CustomDataGrid = memo((props: CustomDataGridProps) => { +export const CustomDataGrid = ( + props: CustomDataGridProps +) => { const { classes, css } = useStyles(); const { columns, @@ -72,7 +74,7 @@ export const CustomDataGrid = memo((props: CustomDataGridProps) => { ); return ( - {...propsRest} slots={{ "noRowsOverlay": CustomNoRowsOverlay, ...slots }} columns={modifiedColumns} @@ -81,7 +83,7 @@ export const CustomDataGrid = memo((props: CustomDataGridProps) => { autosizeOptions={autosizeOptions} /> ); -}); +}; const useStyles = tss.withName({ CustomDataGrid }).create(({ theme }) => ({ "columnSeparator": { "&&&&&": { opacity: "1" } }, //Ensures the column separator remains visible (opacity 1) when a column header is selected. By default, MUI reduces the opacity to 0 because an outline is applied to the selected column header