fix: spdv-914 - Modals are partially cut off in File Manager on Windows (Chrome) (#219)

* fix: spdv-914

* refactor: spdv-914-fix

* refactor: spdv-914-fix
This commit is contained in:
rolandlor
2026-03-10 16:13:59 +01:00
committed by Bálint Ujvári
parent 8992c189fd
commit 220618f19b
18 changed files with 598 additions and 489 deletions
+3 -1
View File
@@ -20,7 +20,9 @@ const useStyles = makeStyles()(theme => ({
},
fileManagerOn: {
padding: '0px',
padding: '0px !important',
margin: '0px !important',
maxWidth: '100% !important',
},
}))
@@ -7,8 +7,6 @@
}
.fm-modal-container .fm-modal-window-body .fm-modal-white-section {
max-height: 50vh;
overflow: auto;
word-break: break-word;
padding: 12px 14px;
border-radius: 8px;
@@ -41,7 +41,7 @@ export function ConfirmModal({
<div className={`fm-modal-container fm-confirm-modal ${background ? '' : 'fm-modal-no-background'}`}>
<div className="fm-modal-window">
<div className="fm-modal-window-header">{title}</div>
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body">
{isProgress ? (
<div className="fm-spinner-center">
@@ -55,6 +55,7 @@ export function ConfirmModal({
<div className="fm-modal-white-section">{message}</div>
)}
</div>
</div>
{showFooter && (onCancel || onConfirm) && (
<div className="fm-modal-window-footer">
@@ -139,6 +139,7 @@ export function CreateDriveModal({
<div className="fm-modal-container">
<div className="fm-modal-window">
<div className="fm-modal-window-header">Create new drive</div>
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body">
<div className="fm-modal-window-input-container">
<label htmlFor="drive-name" className="fm-input-label">
@@ -222,6 +223,7 @@ export function CreateDriveModal({
)}
</div>
</div>
</div>
<div className="fm-modal-window-footer">
<Button
label="Create drive"
@@ -42,7 +42,7 @@ export function DeleteFileModal({
<div className="fm-modal-window-header">
<TrashIcon /> <span className="fm-main-font-color">{headerText}</span>
</div>
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body">
{isBulk && (
<ul className="fm-delete-file-modal-list">
@@ -77,7 +77,9 @@ export function DeleteFileModal({
<div className="fm-form-control-label">
<FormControlLabel
value={FileAction.Forget}
control={<Radio checked={value === FileAction.Forget} onChange={() => setValue(FileAction.Forget)} />}
control={
<Radio checked={value === FileAction.Forget} onChange={() => setValue(FileAction.Forget)} />
}
label={
<div className="fm-radio-label">
<div className="fm-radio-label-header fm-main-font-color fm-line-height-fit">
@@ -116,6 +118,7 @@ export function DeleteFileModal({
</div>
</FormControl>
</div>
</div>
<div className="fm-modal-window-footer">
<Button label="Proceed" variant="primary" onClick={() => onProceed(value)} />
@@ -23,6 +23,7 @@ export function ProgressDestroyModal({ drive, onMinimize }: ProgressDestroyModal
return createPortal(
<div className="fm-modal-container">
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window">
<div className="fm-modal-window-header fm-red-font">Destroying Drive</div>
<div className="fm-modal-window-body">
@@ -39,6 +40,7 @@ export function ProgressDestroyModal({ drive, onMinimize }: ProgressDestroyModal
<Button label="Minimize" variant="secondary" onClick={onMinimize} />
</div>
</div>
</div>
</div>,
modalRoot,
)
@@ -57,6 +59,7 @@ export function DestroyDriveModal({ drive, onCancelClick, doDestroy }: DestroyDr
<div className="fm-modal-container">
<div className="fm-modal-window">
<div className="fm-modal-window-header fm-red-font">Destroy entire drive</div>
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body">
<div className="fm-modal-body-destroy">
<div className="fm-emphasized-text">Destroy Drive? This Action Is Permanent</div>
@@ -67,7 +70,9 @@ export function DestroyDriveModal({ drive, onCancelClick, doDestroy }: DestroyDr
recover these files.
</div>
<div>Confirmation:</div>
<div>Requires typing a fixed expression to prevent accidental deletion. This action cannot be undone.</div>
<div>
Requires typing a fixed expression to prevent accidental deletion. This action cannot be undone.
</div>
<div>
Type: <span className="fm-emphasized-text">{destroyDriveText}</span>
</div>
@@ -82,6 +87,7 @@ export function DestroyDriveModal({ drive, onCancelClick, doDestroy }: DestroyDr
</div>
</div>
</div>
</div>
<div className="fm-modal-window-footer">
<Button
label="Destroy entire drive"
@@ -1,17 +1,15 @@
import { PostageBatch } from '@ethersphere/bee-js'
import { Warning } from '@mui/icons-material'
import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement, useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import AlertIcon from 'remixicon-react/AlertLineIcon'
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
import DriveIcon from 'remixicon-react/HardDrive2LineIcon'
import { calculateStampCapacityMetrics } from '../../utils/bee'
import { getDaysLeft } from '../../utils/common'
import { Button } from '../Button/Button'
import { UpgradeDriveModal } from '../UpgradeDriveModal/UpgradeDriveModal'
import { ExpiringNotificationModalItem } from './ExpiringNotificationModalItem/ExpiringNotificationModalItem'
import './ExpiringNotificationModal.scss'
import '../../styles/global.scss'
@@ -67,63 +65,25 @@ export function ExpiringNotificationModal({
<AlertIcon size="21px" /> Drives Expiring soon
</div>
<div>The following drives will expire soon. Extend them to keep your data accessible.</div>
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body fm-expiring-notification-modal-body">
{paginatedStamps.map((stamp, index) => {
const daysLeft = getDaysLeft(stamp.duration.toEndDate())
let daysClass = ''
const drive = drives.find(d => d.batchId.toString() === stamp.batchID.toString())
if (!drive) return null
const filesPerDrive = files.filter(fi => fi.driveId === drive.id.toString())
const { usedSize, stampSize } = calculateStampCapacityMetrics(stamp, filesPerDrive, drive.redundancyLevel)
if (daysLeft < 10) {
daysClass = 'fm-red-font'
} else if (daysLeft < 30) {
daysClass = 'fm-swarm-orange-font'
}
return (
<div
{paginatedStamps.map((stamp, index) => (
<ExpiringNotificationModalItem
key={`${stamp.batchID.toString()}-${currentPage}-${index}`}
className="fm-modal-white-section fm-space-between"
>
<div className="fm-expiring-notification-modal-section-left fm-space-between">
<DriveIcon size="20" color="rgb(237, 129, 49)" />
<div>
<div className="fm-expiring-notification-modal-section-left-header fm-emphasized-text">
{stamp.label} {drive.isAdmin && <Warning style={{ fontSize: '16px' }} />}
</div>
<div className="fm-expiring-notification-modal-section-left-value">
{usedSize} / {stampSize}
</div>
</div>
</div>
<div className="fm-expiring-notification-modal-section-right">
<div className="fm-expiring-notification-modal-section-right-header">
<CalendarIcon size="14" /> Expiry date: {stamp.duration.toEndDate().toLocaleDateString()}
</div>
<div className={daysClass}>{daysLeft} days left</div>
<div className="fm-expiring-notification-modal-section-right-button">
<Button
label="Upgrade"
variant="primary"
onClick={() => {
stamp={stamp}
drives={drives}
files={files}
currentPage={currentPage}
index={index}
onUpgradeClick={(stamp, drive) => {
setActualStamp(stamp)
setActualDrive(drive)
setShowUpgradeDriveModal(true)
}}
/>
))}
</div>
</div>
</div>
)
})}
</div>
<div className="fm-modal-window-footer">
<div className="fm-expiring-notification-modal-footer-one-button">
<Button label="Cancel" variant="secondary" onClick={onCancelClick} />
@@ -0,0 +1,75 @@
import { PostageBatch } from '@ethersphere/bee-js'
import { Warning } from '@mui/icons-material'
import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
import { ReactElement } from 'react'
import CalendarIcon from 'remixicon-react/CalendarLineIcon'
import DriveIcon from 'remixicon-react/HardDrive2LineIcon'
import { calculateStampCapacityMetrics } from '../../../utils/bee'
import { getDaysLeft } from '../../../utils/common'
import { Button } from '../../Button/Button'
import '../../../styles/global.scss'
interface ExpiringNotificationModalItemProps {
stamp: PostageBatch
drives: DriveInfo[]
files: FileInfo[]
currentPage: number
index: number
onUpgradeClick: (stamp: PostageBatch, drive: DriveInfo) => void
}
export function ExpiringNotificationModalItem({
stamp,
drives,
files,
currentPage,
index,
onUpgradeClick,
}: ExpiringNotificationModalItemProps): ReactElement | null {
const daysLeft = getDaysLeft(stamp.duration.toEndDate())
let daysClass = ''
const drive = drives.find(d => d.batchId.toString() === stamp.batchID.toString())
if (!drive) return null
const filesPerDrive = files.filter(fi => fi.driveId === drive.id.toString())
const { usedSize, stampSize } = calculateStampCapacityMetrics(stamp, filesPerDrive, drive.redundancyLevel)
if (daysLeft < 10) {
daysClass = 'fm-red-font'
} else if (daysLeft < 30) {
daysClass = 'fm-swarm-orange-font'
}
return (
<div
key={`${stamp.batchID.toString()}-${currentPage}-${index}`}
className="fm-modal-white-section fm-space-between"
>
<div className="fm-expiring-notification-modal-section-left fm-space-between">
<DriveIcon size="20" color="rgb(237, 129, 49)" />
<div>
<div className="fm-expiring-notification-modal-section-left-header fm-emphasized-text">
{stamp.label} {drive.isAdmin && <Warning style={{ fontSize: '16px' }} />}
</div>
<div className="fm-expiring-notification-modal-section-left-value">
{usedSize} / {stampSize}
</div>
</div>
</div>
<div className="fm-expiring-notification-modal-section-right">
<div className="fm-expiring-notification-modal-section-right-header">
<CalendarIcon size="14" /> Expiry date: {stamp.duration.toEndDate().toLocaleDateString()}
</div>
<div className={daysClass}>{daysLeft} days left</div>
<div className="fm-expiring-notification-modal-section-right-button">
<Button label="Upgrade" variant="primary" onClick={() => onUpgradeClick(stamp, drive)} />
</div>
</div>
</div>
)
}
@@ -5,6 +5,11 @@
bottom: 45px;
background-color: white;
z-index: 1200;
max-height: calc(100vh - 275px);
min-height: 170px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.fm-file-progress-window-header {
@@ -15,63 +20,141 @@
border-bottom: 1px solid rgb(209, 213, 219);
}
.fm-file-progress-window-header-actions { display: inline-flex; gap: 6px; }
.fm-file-progress-window-header-actions {
display: inline-flex;
gap: 6px;
}
.fm-file-progress-window-header-btn {
width: 22px; height: 22px; display: inline-grid; place-items: center;
padding: 0; margin: 0; background: #f0f0f0; color: #4b5563;
border: none; border-radius: 4px; cursor: pointer;
&:hover { background: #e5e7eb; } &:active { background: #d1d5db; }
&:disabled { cursor: default; opacity: .6; filter: grayscale(.3); }
width: 22px;
height: 22px;
display: inline-grid;
place-items: center;
padding: 0;
margin: 0;
background: #f0f0f0;
color: #4b5563;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background: #e5e7eb;
}
&:active {
background: #d1d5db;
}
&:disabled {
cursor: default;
opacity: 0.6;
filter: grayscale(0.3);
}
}
.fm-file-progress-window-file-item {
display: flex; align-items: flex-start; gap: 8px;
padding: 12px; border-bottom: 1px solid rgb(243, 244, 246);
display: flex;
align-items: flex-start;
gap: 8px;
padding: 12px;
border-bottom: 1px solid rgb(243, 244, 246);
}
.fm-file-progress-window-file-type-icon { margin-top: 4px; }
.fm-file-progress-window-file-type-icon {
margin-top: 4px;
}
.fm-file-progress-window-file-datas {
display: flex; flex-direction: column; gap: 8px; width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.fm-file-progress-window-file-item-header {
display: grid; grid-template-columns: 1fr auto auto;
align-items: center; gap: 8px; min-width: 0;
display: grid;
grid-template-columns: 1fr auto auto;
align-items: center;
gap: 8px;
min-width: 0;
}
.fm-file-progress-window-name { min-width: 0; }
.fm-file-progress-window-name {
min-width: 0;
}
.fm-file-progress-window-name-text {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fm-drive-line {
margin-top: 2px;
}
.fm-drive-line { margin-top: 2px; }
.fm-file-progress-window-percent { white-space: nowrap; }
.fm-file-progress-window-percent {
white-space: nowrap;
}
.fm-file-progress-window-file-item-footer {
display: grid; grid-template-columns: auto 1fr auto;
align-items: center; column-gap: 8px; font-size: 11px;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
column-gap: 8px;
font-size: 11px;
}
.fm-file-progress-window-size {
white-space: nowrap;
}
.fm-file-progress-window-center {
justify-self: center;
white-space: nowrap;
}
.fm-file-progress-window-status {
justify-self: end;
white-space: nowrap;
}
.fm-file-progress-window-size { white-space: nowrap; }
.fm-file-progress-window-center { justify-self: center; white-space: nowrap; }
.fm-file-progress-window-status { justify-self: end; white-space: nowrap; }
.fm-file-progress-window-row-close {
width: 20px; height: 20px; display: inline-grid; place-items: center;
padding: 0; margin: 0; background: #f0f0f0; color: #4b5563;
border: none; border-radius: 4px; cursor: pointer;
&:hover { background: #e5e7eb; } &:active { background: #d1d5db; }
&:disabled { cursor: default; opacity: .6; filter: grayscale(.3); }
width: 20px;
height: 20px;
display: inline-grid;
place-items: center;
padding: 0;
margin: 0;
background: #f0f0f0;
color: #4b5563;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background: #e5e7eb;
}
&:active {
background: #d1d5db;
}
&:disabled {
cursor: default;
opacity: 0.6;
filter: grayscale(0.3);
}
}
.fm-drive-chip {
display: inline-block; margin-left: 0; padding: 2px 6px;
border-radius: 999px; font-size: 11px; line-height: 1;
background: rgba(0,0,0,.06); color: #333; vertical-align: middle;
display: inline-block;
margin-left: 0;
padding: 2px 6px;
border-radius: 999px;
font-size: 11px;
line-height: 1;
background: rgba(0, 0, 0, 0.06);
color: #333;
vertical-align: middle;
}
.fm-eta {
font-size: 12px;
opacity: 0.8;
}
.fm-file-subtext {
line-height: 1.2;
}
.fm-eta { font-size: 12px; opacity: .8; }
.fm-file-subtext { line-height: 1.2; }
.fm-file-progress-window-list {
overflow-y: auto;
@@ -1,4 +1,4 @@
import { ReactElement, useLayoutEffect, useRef } from 'react'
import { ReactElement } from 'react'
import ArrowDownIcon from 'remixicon-react/ArrowDownSLineIcon'
import CloseIcon from 'remixicon-react/CloseLineIcon'
@@ -44,8 +44,6 @@ export function FileProgressWindow({
onRowClose,
onCloseAll,
}: FileProgressWindowProps): ReactElement | null {
const listRef = useRef<HTMLDivElement | null>(null)
const firstRowRef = useRef<HTMLDivElement | null>(null)
const count = items?.length ?? 0
const rows: ProgressItem[] = items ?? []
@@ -73,17 +71,6 @@ export function FileProgressWindow({
)
})
useLayoutEffect(() => {
const rowEl = firstRowRef.current
const listEl = listRef.current
if (!rowEl || !listEl) return
const rowH = rowEl.getBoundingClientRect().height
const safeRowH = rowH > 0 ? rowH : 72
listEl.style.maxHeight = `${safeRowH * 5}px`
}, [rows.length])
return (
<div className="fm-file-progress-window">
<div className="fm-file-progress-window-header">
@@ -113,7 +100,7 @@ export function FileProgressWindow({
</button>
</div>
</div>
<div className="fm-file-progress-window-list" ref={listRef}>
<div className="fm-file-progress-window-list">
{rows.map((item, idx) => {
const pctNum = Number.isFinite(item.percent)
? Math.max(0, Math.min(100, Math.round(item.percent as number)))
@@ -140,11 +127,7 @@ export function FileProgressWindow({
const centerDisplay = getCenterText() || '\u00A0'
return (
<div
className="fm-file-progress-window-file-item"
key={item.uuid || `${item.name}-${idx}`}
ref={idx === 0 ? firstRowRef : undefined}
>
<div className="fm-file-progress-window-file-item" key={item.uuid || `${item.name}-${idx}`}>
<div className="fm-file-progress-window-file-type-icon">
<GetIconElement size="14" name={item.name} color="black" />
</div>
@@ -294,6 +294,7 @@ export function InitialModal({
<div className="fm-modal-window">
<div className="fm-modal-window-header">Welcome to your Swarm File Manager</div>
<div>{initText} the File Manager</div>
<div className="fm-modal-window-scrollable">
{nonFullStamps.length > 0 && (
<div className="fm-modal-window-body">
<div className="fm-modal-window-input-container">
@@ -359,6 +360,7 @@ export function InitialModal({
</div>
</div>
)}
</div>
<div className="fm-modal-window-footer">
<Button
label={selectedBatch ? `${createText} Drive` : `Purchase Stamp & ${createText} Drive`}
@@ -116,16 +116,6 @@
flex-shrink: 0;
}
.fm-initialization-modal-container .fm-modal-window-scrollable {
overflow-y: auto;
overflow-x: hidden;
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
gap: 24px;
}
.fm-main:has(.fm-initialization-modal-container) {
border-left: none;
}
@@ -9,16 +9,6 @@
flex-direction: column;
}
.fm-modal-window-scrollable {
overflow-y: auto;
overflow-x: hidden;
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
padding-right: 4px;
}
.fm-upgrade-drive-modal-wallet {
display: flex;
flex-direction: column;
@@ -228,8 +228,8 @@ export function UpgradeDriveModal({
<div className="fm-modal-window-header">
<DriveIcon size="18px" /> Upgrade {truncateNameMiddle(drive.name || stamp.label || shortBatchId, 35)}
</div>
<div className="fm-modal-window-scrollable">
<div>Choose extension period and additional storage for your drive.</div>
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body">
<div className="fm-upgrade-drive-modal-wallet">
<div className="fm-upgrade-drive-modal-wallet-header fm-emphasized-text">
@@ -39,7 +39,7 @@ export function UploadConflictModal({
<WarningIcon size="18px" />
<span className="fm-main-font-color">File already exists</span>
</div>
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body">
<div className="fm-modal-white-section">
<div className="fm-conflict-row">
@@ -97,6 +97,7 @@ export function UploadConflictModal({
</div>
)}
</div>
</div>
<div className="fm-modal-window-footer">
<div className="fm-expiring-notification-modal-footer-one-button">
@@ -293,7 +293,7 @@ export function VersionHistoryModal({ fileInfo, onCancelClick, onDownload }: Ver
</>
</span>
</div>
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body fm-expiring-notification-modal-body">
{error && <div className="fm-modal-white-section fm-soft-text">{error}</div>}
@@ -348,6 +348,7 @@ export function VersionHistoryModal({ fileInfo, onCancelClick, onDownload }: Ver
onDownload={trackDownload}
/>
</div>
</div>
<div className="fm-modal-window-footer vh-footer">
<div className="vh-footer-left">
@@ -1,3 +1,5 @@
$fm-modal-vertical-offset: 48px;
.fm-modal-container {
position: absolute;
top: 0;
@@ -25,6 +27,7 @@
flex-direction: column;
gap: 24px;
justify-content: center;
max-height: calc(100vh - #{$fm-modal-vertical-offset});
}
.fm-modal-window-header {
@@ -38,6 +41,15 @@
font-weight: 700;
}
.fm-modal-window-scrollable {
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 6px;
}
}
.fm-modal-window-body {
display: flex;
flex-direction: column;