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,19 +41,20 @@ 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-body">
{isProgress ? (
<div className="fm-spinner-center">
<div className="fm-spinner-message">
<div>{spinnerMessage || 'Working…'}</div>
<div className="fm-mini-spinner" />
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body">
{isProgress ? (
<div className="fm-spinner-center">
<div className="fm-spinner-message">
<div>{spinnerMessage || 'Working…'}</div>
<div className="fm-mini-spinner" />
</div>
{showMinimize && <Button label="Minimize" variant="secondary" onClick={onMinimize} />}
</div>
{showMinimize && <Button label="Minimize" variant="secondary" onClick={onMinimize} />}
</div>
) : (
<div className="fm-modal-white-section">{message}</div>
)}
) : (
<div className="fm-modal-white-section">{message}</div>
)}
</div>
</div>
{showFooter && (onCancel || onConfirm) && (
@@ -139,87 +139,89 @@ 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-body">
<div className="fm-modal-window-input-container">
<label htmlFor="drive-name" className="fm-input-label">
Drive name: <Tooltip label={TOOLTIPS.DRIVE_NAME} />
</label>
<input
type="text"
id="drive-name"
placeholder="My important files"
value={driveName}
onChange={e => setDriveName(e.target.value)}
onBlur={() => setDuplicate(true)}
maxLength={maxDriveNameLength}
/>
{validationError && <div className="fm-error-text">{validationError}</div>}
</div>
<div className="fm-modal-window-input-container">
<label htmlFor="drive-initial-capacity" className="fm-input-label">
Initial capacity: <Tooltip label={TOOLTIPS.DRIVE_INITIAL_CAPACITY} />
</label>
<CustomDropdown
id="drive-initial-capacity"
options={sizeMarks}
value={capacity}
onChange={handleCapacityChange}
placeholder="Select a value"
/>
</div>
<div className="fm-modal-info-warning">
Drive sizes are calculated automatically from your current stamp configuration.
</div>
<div className="fm-modal-window-input-container">
<label htmlFor="drive-desired-lifetime" className="fm-input-label">
Desired lifetime: <Tooltip label={TOOLTIPS.DRIVE_DESIRED_LIFETIME} />
</label>
<CustomDropdown
id="drive-desired-lifetime"
options={desiredLifetimeOptions}
value={lifetimeIndex}
onChange={setLifetimeIndex}
placeholder="Select a value"
/>
</div>
<div className="fm-modal-window-input-container">
<label htmlFor="drive-security-level" className="fm-input-label">
Security Level <Tooltip label={TOOLTIPS.DRIVE_SECURITY_LEVEL} />
</label>
<FMSlider
id="drive-security-level"
defaultValue={0}
marks={erasureCodeMarks}
onChange={value => setErasureCodeLevel(value)}
minValue={minMarkValue}
maxValue={maxMarkValue}
step={1}
/>
</div>
<div>
<div className="fm-modal-estimated-cost-container">
<div className="fm-emphasized-text">Estimated Cost:</div>
<div>
{cost} BZZ {isBalanceSufficient ? '' : '(Insufficient balance)'}
{isxDaiBalanceSufficient ? '' : ' (Insufficient xDAI balance)'}
</div>
<Tooltip label={TOOLTIPS.DRIVE_ESTIMATED_COST} bottomTooltip={true} />
<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">
Drive name: <Tooltip label={TOOLTIPS.DRIVE_NAME} />
</label>
<input
type="text"
id="drive-name"
placeholder="My important files"
value={driveName}
onChange={e => setDriveName(e.target.value)}
onBlur={() => setDuplicate(true)}
maxLength={maxDriveNameLength}
/>
{validationError && <div className="fm-error-text">{validationError}</div>}
</div>
<div>(Based on current network conditions)</div>
{isUltraLightNode && (
<div>
Creating a drive requires running a light node. Please{' '}
<a
href="https://docs.ethswarm.org/docs/desktop/configuration/#upgrading-from-an-ultra-light-to-a-light-node"
target="_blank"
rel="noreferrer"
>
upgrade
</a>{' '}
to continue.
<div className="fm-modal-window-input-container">
<label htmlFor="drive-initial-capacity" className="fm-input-label">
Initial capacity: <Tooltip label={TOOLTIPS.DRIVE_INITIAL_CAPACITY} />
</label>
<CustomDropdown
id="drive-initial-capacity"
options={sizeMarks}
value={capacity}
onChange={handleCapacityChange}
placeholder="Select a value"
/>
</div>
<div className="fm-modal-info-warning">
Drive sizes are calculated automatically from your current stamp configuration.
</div>
<div className="fm-modal-window-input-container">
<label htmlFor="drive-desired-lifetime" className="fm-input-label">
Desired lifetime: <Tooltip label={TOOLTIPS.DRIVE_DESIRED_LIFETIME} />
</label>
<CustomDropdown
id="drive-desired-lifetime"
options={desiredLifetimeOptions}
value={lifetimeIndex}
onChange={setLifetimeIndex}
placeholder="Select a value"
/>
</div>
<div className="fm-modal-window-input-container">
<label htmlFor="drive-security-level" className="fm-input-label">
Security Level <Tooltip label={TOOLTIPS.DRIVE_SECURITY_LEVEL} />
</label>
<FMSlider
id="drive-security-level"
defaultValue={0}
marks={erasureCodeMarks}
onChange={value => setErasureCodeLevel(value)}
minValue={minMarkValue}
maxValue={maxMarkValue}
step={1}
/>
</div>
<div>
<div className="fm-modal-estimated-cost-container">
<div className="fm-emphasized-text">Estimated Cost:</div>
<div>
{cost} BZZ {isBalanceSufficient ? '' : '(Insufficient balance)'}
{isxDaiBalanceSufficient ? '' : ' (Insufficient xDAI balance)'}
</div>
<Tooltip label={TOOLTIPS.DRIVE_ESTIMATED_COST} bottomTooltip={true} />
</div>
)}
<div>(Based on current network conditions)</div>
{isUltraLightNode && (
<div>
Creating a drive requires running a light node. Please{' '}
<a
href="https://docs.ethswarm.org/docs/desktop/configuration/#upgrading-from-an-ultra-light-to-a-light-node"
target="_blank"
rel="noreferrer"
>
upgrade
</a>{' '}
to continue.
</div>
)}
</div>
</div>
</div>
<div className="fm-modal-window-footer">
@@ -42,79 +42,82 @@ 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">
{names.map(n => (
<li key={n} className="fm-delete-file-modal-list-item" title={n}>
{n}
</li>
))}
</ul>
)}
<FormControl component="fieldset">
<div className="fm-radio-group">
<div className="fm-form-control-label">
<FormControlLabel
value={FileAction.Trash}
control={<Radio checked={value === FileAction.Trash} onChange={() => setValue(FileAction.Trash)} />}
label={
<div className="fm-radio-label">
<div className="fm-radio-label-header fm-main-font-color fm-line-height-fit">
Move to Trash
<Tooltip label={TOOLTIPS.FILE_OPERATION_TRASH} iconSize="14px" />
</div>
<div onClick={e => e.preventDefault()}>
Moves {subjectNoun} to the trash. It will still take up space on{' '}
{currentDriveName ?? 'this drive'} and expire along with it. You can restore it later.
</div>
</div>
}
/>
</div>
<div className="fm-modal-window-body">
{isBulk && (
<ul className="fm-delete-file-modal-list">
{names.map(n => (
<li key={n} className="fm-delete-file-modal-list-item" title={n}>
{n}
</li>
))}
</ul>
)}
<FormControl component="fieldset">
<div className="fm-radio-group">
<div className="fm-form-control-label">
<FormControlLabel
value={FileAction.Trash}
control={<Radio checked={value === FileAction.Trash} onChange={() => setValue(FileAction.Trash)} />}
label={
<div className="fm-radio-label">
<div className="fm-radio-label-header fm-main-font-color fm-line-height-fit">
Move to Trash
<Tooltip label={TOOLTIPS.FILE_OPERATION_TRASH} iconSize="14px" />
<div className="fm-form-control-label">
<FormControlLabel
value={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">
Forget
<Tooltip label={TOOLTIPS.FILE_OPERATION_FORGET} iconSize="14px" />
</div>
<div onClick={e => e.preventDefault()}>
Removes {subjectNoun} from your view. The data will remain on Swarm until{' '}
{currentDriveName ?? 'the drive'} expires. This action cannot be easily undone.
</div>
</div>
<div onClick={e => e.preventDefault()}>
Moves {subjectNoun} to the trash. It will still take up space on{' '}
{currentDriveName ?? 'this drive'} and expire along with it. You can restore it later.
</div>
</div>
}
/>
</div>
}
/>
</div>
<div className="fm-form-control-label">
<FormControlLabel
value={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">
Forget
<Tooltip label={TOOLTIPS.FILE_OPERATION_FORGET} iconSize="14px" />
<div className="fm-form-control-label">
<FormControlLabel
value={FileAction.Destroy}
control={
<Radio checked={value === FileAction.Destroy} onChange={() => setValue(FileAction.Destroy)} />
}
label={
<div className="fm-radio-label">
<div className="fm-radio-label-header fm-main-font-color fm-line-height-fit">
Destroy entire drive {currentDriveName ? `${currentDriveName}` : ''} to delete this{' '}
{subjectNoun}
</div>
<div className="fm-red-font" onClick={e => e.preventDefault()}>
<AlertIcon size="14px" className="fm-alert-icon-inline" />
Warning: This will make all files on this drive inaccessible. This action is irreversible.
</div>
</div>
<div onClick={e => e.preventDefault()}>
Removes {subjectNoun} from your view. The data will remain on Swarm until{' '}
{currentDriveName ?? 'the drive'} expires. This action cannot be easily undone.
</div>
</div>
}
/>
}
/>
</div>
</div>
<div className="fm-form-control-label">
<FormControlLabel
value={FileAction.Destroy}
control={
<Radio checked={value === FileAction.Destroy} onChange={() => setValue(FileAction.Destroy)} />
}
label={
<div className="fm-radio-label">
<div className="fm-radio-label-header fm-main-font-color fm-line-height-fit">
Destroy entire drive {currentDriveName ? `${currentDriveName}` : ''} to delete this{' '}
{subjectNoun}
</div>
<div className="fm-red-font" onClick={e => e.preventDefault()}>
<AlertIcon size="14px" className="fm-alert-icon-inline" />
Warning: This will make all files on this drive inaccessible. This action is irreversible.
</div>
</div>
}
/>
</div>
</div>
</FormControl>
</FormControl>
</div>
</div>
<div className="fm-modal-window-footer">
@@ -23,20 +23,22 @@ export function ProgressDestroyModal({ drive, onMinimize }: ProgressDestroyModal
return createPortal(
<div className="fm-modal-container">
<div className="fm-modal-window">
<div className="fm-modal-window-header fm-red-font">Destroying Drive</div>
<div className="fm-modal-window-body">
<div className="fm-modal-body-destroy">
<div className="fm-emphasized-text">Drive &quot;{drive.name}&quot; is being destroyed</div>
<div>Please wait while the operation completes...</div>
<div style={{ marginTop: '20px', textAlign: 'center' }}>
<div className="fm-mini-spinner" style={{ display: 'inline-block', marginRight: '10px' }} />
<span>Destroying drive...</span>
<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">
<div className="fm-modal-body-destroy">
<div className="fm-emphasized-text">Drive &quot;{drive.name}&quot; is being destroyed</div>
<div>Please wait while the operation completes...</div>
<div style={{ marginTop: '20px', textAlign: 'center' }}>
<div className="fm-mini-spinner" style={{ display: 'inline-block', marginRight: '10px' }} />
<span>Destroying drive...</span>
</div>
</div>
</div>
</div>
<div className="fm-modal-window-footer">
<Button label="Minimize" variant="secondary" onClick={onMinimize} />
<div className="fm-modal-window-footer">
<Button label="Minimize" variant="secondary" onClick={onMinimize} />
</div>
</div>
</div>
</div>,
@@ -57,28 +59,32 @@ 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-body">
<div className="fm-modal-body-destroy">
<div className="fm-emphasized-text">Destroy Drive? This Action Is Permanent</div>
<div>All files stored only on this drive will become inaccessible.</div>
<div>
While the data may still temporarily persist on Swarm, it will be permanently removed once the storage
expires and the data is garbage collected by the network. The File Manager will no longer recognise or
recover these files.
</div>
<div>Confirmation:</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>
<div className="fm-modal-window-input-container">
<input
type="text"
id="drive-name"
placeholder={destroyDriveText}
value={driveNameInput}
onChange={e => setDriveNameInput(e.target.value)}
/>
<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>
<div>All files stored only on this drive will become inaccessible.</div>
<div>
While the data may still temporarily persist on Swarm, it will be permanently removed once the storage
expires and the data is garbage collected by the network. The File Manager will no longer recognise or
recover these files.
</div>
<div>Confirmation:</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>
<div className="fm-modal-window-input-container">
<input
type="text"
id="drive-name"
placeholder={destroyDriveText}
value={driveNameInput}
onChange={e => setDriveNameInput(e.target.value)}
/>
</div>
</div>
</div>
</div>
@@ -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,62 +65,24 @@ 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-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
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body fm-expiring-notification-modal-body">
{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={() => {
setActualStamp(stamp)
setActualDrive(drive)
setShowUpgradeDriveModal(true)
}}
/>
</div>
</div>
</div>
)
})}
stamp={stamp}
drives={drives}
files={files}
currentPage={currentPage}
index={index}
onUpgradeClick={(stamp, drive) => {
setActualStamp(stamp)
setActualDrive(drive)
setShowUpgradeDriveModal(true)
}}
/>
))}
</div>
</div>
<div className="fm-modal-window-footer">
<div className="fm-expiring-notification-modal-footer-one-button">
@@ -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,65 +20,143 @@
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;
overscroll-behavior: contain;
}
}
@@ -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,71 +294,73 @@ 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>
{nonFullStamps.length > 0 && (
<div className="fm-modal-window-body">
<div className="fm-modal-window-input-container">
<CustomDropdown
id="batch-id-selector"
options={createBatchIdOptions(nonFullStamps)}
value={selectedBatchIndex}
label="Link an existing Admin Drive (optional)"
onChange={(index: number) => {
setSelectedBatchIndex(index)
<div className="fm-modal-window-scrollable">
{nonFullStamps.length > 0 && (
<div className="fm-modal-window-body">
<div className="fm-modal-window-input-container">
<CustomDropdown
id="batch-id-selector"
options={createBatchIdOptions(nonFullStamps)}
value={selectedBatchIndex}
label="Link an existing Admin Drive (optional)"
onChange={(index: number) => {
setSelectedBatchIndex(index)
if (index === -1) {
setSelectedBatch(null)
}
}}
placeholder={BATCH_ID_PLACEHOLDER}
/>
{selectedBatch && (
<div className="fm-drive-item-content">
<div className="fm-drive-item-capacity">
Capacity <ProgressBar value={capacityPct} width="64px" /> {usedSize} / {stampSize}
if (index === -1) {
setSelectedBatch(null)
}
}}
placeholder={BATCH_ID_PLACEHOLDER}
/>
{selectedBatch && (
<div className="fm-drive-item-content">
<div className="fm-drive-item-capacity">
Capacity <ProgressBar value={capacityPct} width="64px" /> {usedSize} / {stampSize}
</div>
<div className="fm-drive-item-capacity">
Expiry date: {selectedBatch.duration.toEndDate().toLocaleDateString()}
</div>
</div>
<div className="fm-drive-item-capacity">
Expiry date: {selectedBatch.duration.toEndDate().toLocaleDateString()}
</div>
</div>
)}
{selectedBatch && setSecurityLevel(setErasureCodeLevel)}
</div>
</div>
)}
{!selectedBatch && (
<div className="fm-modal-window-body">
<div className="fm-modal-window-input-container">
<label htmlFor="admin-desired-lifetime" className="fm-input-label">
Create a new Admin Drive with desired lifetime: <Tooltip label={TOOLTIPS.ADMIN_DESIRED_LIFETIME} />
</label>
<CustomDropdown
id="admin-desired-lifetime"
options={desiredLifetimeOptions}
value={lifetimeIndex}
onChange={setLifetimeIndex}
placeholder="Select a value"
/>
</div>
{setSecurityLevel(setErasureCodeLevel)}
<div className="fm-modal-window-input-container">
<div className="fm-modal-estimated-cost-container">
<div className="fm-emphasized-text">Estimated Cost:</div>
<div>
{cost} BZZ {isBalanceSufficient ? '' : '(Insufficient balance)'}
{isxDaiBalanceSufficient ? '' : ' (Insufficient xDAI balance)'}
</div>
<Tooltip label={TOOLTIPS.ADMIN_ESTIMATED_COST} />
)}
{selectedBatch && setSecurityLevel(setErasureCodeLevel)}
</div>
<div>(Based on current network conditions)</div>
{renderUltraLightNodeWarning()}
{isNodeSyncing && !selectedBatch && (
<div className="fm-modal-info-warning" style={{ marginBottom: '16px' }}>
Node is syncing. Please wait until sync completes before purchasing a stamp.
</div>
)}
</div>
</div>
)}
)}
{!selectedBatch && (
<div className="fm-modal-window-body">
<div className="fm-modal-window-input-container">
<label htmlFor="admin-desired-lifetime" className="fm-input-label">
Create a new Admin Drive with desired lifetime: <Tooltip label={TOOLTIPS.ADMIN_DESIRED_LIFETIME} />
</label>
<CustomDropdown
id="admin-desired-lifetime"
options={desiredLifetimeOptions}
value={lifetimeIndex}
onChange={setLifetimeIndex}
placeholder="Select a value"
/>
</div>
{setSecurityLevel(setErasureCodeLevel)}
<div className="fm-modal-window-input-container">
<div className="fm-modal-estimated-cost-container">
<div className="fm-emphasized-text">Estimated Cost:</div>
<div>
{cost} BZZ {isBalanceSufficient ? '' : '(Insufficient balance)'}
{isxDaiBalanceSufficient ? '' : ' (Insufficient xDAI balance)'}
</div>
<Tooltip label={TOOLTIPS.ADMIN_ESTIMATED_COST} />
</div>
<div>(Based on current network conditions)</div>
{renderUltraLightNodeWarning()}
{isNodeSyncing && !selectedBatch && (
<div className="fm-modal-info-warning" style={{ marginBottom: '16px' }}>
Node is syncing. Please wait until sync completes before purchasing a stamp.
</div>
)}
</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;
@@ -74,4 +64,4 @@
display: flex;
flex-direction: column;
gap: 8px;
}
}
@@ -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>Choose extension period and additional storage for your drive.</div>
<div className="fm-modal-window-scrollable">
<div>Choose extension period and additional storage for your drive.</div>
<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,63 +39,64 @@ export function UploadConflictModal({
<WarningIcon size="18px" />
<span className="fm-main-font-color">File already exists</span>
</div>
<div className="fm-modal-window-body">
<div className="fm-modal-white-section">
<div className="fm-conflict-row">
<div className="fm-emphasized-text">A file named {filename} already exists in this drive.</div>
<div className="fm-soft-text">What would you like to do?</div>
</div>
<div className="fm-conflict-option">
<div className="fm-conflict-option-title">Keep both</div>
<div className="fm-conflict-option-sub">
Upload the new file as a separate item with a different name.
<div className="fm-modal-window-scrollable">
<div className="fm-modal-window-body">
<div className="fm-modal-white-section">
<div className="fm-conflict-row">
<div className="fm-emphasized-text">A file named {filename} already exists in this drive.</div>
<div className="fm-soft-text">What would you like to do?</div>
</div>
<div className="fm-conflict-rename-row">
<label htmlFor="conflict-newname">New name</label>
<input
id="conflict-newname"
type="text"
value={customName}
onChange={e => setCustomName(e.target.value)}
className="fm-input"
placeholder={suggestedName}
<div className="fm-conflict-option">
<div className="fm-conflict-option-title">Keep both</div>
<div className="fm-conflict-option-sub">
Upload the new file as a separate item with a different name.
</div>
<div className="fm-conflict-rename-row">
<label htmlFor="conflict-newname">New name</label>
<input
id="conflict-newname"
type="text"
value={customName}
onChange={e => setCustomName(e.target.value)}
className="fm-input"
placeholder={suggestedName}
/>
{!isNameValid && customName.trim().length > 0 && existingNames.has(customName.trim()) && (
<div className="fm-soft-text" style={{ marginTop: 6 }}>
That name already exists.
</div>
)}
</div>
<Button
label="Keep both"
variant="secondary"
onClick={() => isNameValid && onKeepBoth(customName.trim())}
disabled={!isNameValid}
/>
{!isNameValid && customName.trim().length > 0 && existingNames.has(customName.trim()) && (
<div className="fm-soft-text" style={{ marginTop: 6 }}>
That name already exists.
</div>
)}
</div>
<Button
label="Keep both"
variant="secondary"
onClick={() => isNameValid && onKeepBoth(customName.trim())}
disabled={!isNameValid}
/>
</div>
<div className="fm-conflict-sep" />
<div className="fm-conflict-sep" />
<div className="fm-conflict-option">
<div className="fm-conflict-option-title">Replace</div>
<div className="fm-conflict-option-sub">
Replace the existing file by uploading this as a new version of {filename}.
<div className="fm-conflict-option">
<div className="fm-conflict-option-title">Replace</div>
<div className="fm-conflict-option-sub">
Replace the existing file by uploading this as a new version of {filename}.
</div>
<Button label="Replace" variant="primary" onClick={onReplace} />
</div>
<Button label="Replace" variant="primary" onClick={onReplace} />
</div>
{isTrashedExisting && (
<div className="fm-callout fm-callout--warning" role="note" aria-live="polite" style={{ marginTop: 12 }}>
<span className="fm-callout__icon" aria-hidden>
<WarningIcon size="16px" />
</span>
<span className="fm-callout__text">
<b>Heads up:</b> The existing &apos;{filename}&apos; is currently in <b>Trash</b>.
</span>
</div>
)}
</div>
{isTrashedExisting && (
<div className="fm-callout fm-callout--warning" role="note" aria-live="polite" style={{ marginTop: 12 }}>
<span className="fm-callout__icon" aria-hidden>
<WarningIcon size="16px" />
</span>
<span className="fm-callout__text">
<b>Heads up:</b> The existing &apos;{filename}&apos; is currently in <b>Trash</b>.
</span>
</div>
)}
</div>
<div className="fm-modal-window-footer">
@@ -293,60 +293,61 @@ 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>}
<div className="fm-modal-window-body fm-expiring-notification-modal-body">
{error && <div className="fm-modal-white-section fm-soft-text">{error}</div>}
{loading && <div className="fm-loading">Loading</div>}
{!error && !loading && pageVersions.length === 0 && (
<div className="fm-empty">No versions found for this file.</div>
)}
{conflictWarning && (
<div
className="fm-modal-white-section fm-soft-text"
style={{ borderLeft: '3px solid var(--fm-accent, #6aa7ff)' }}
>
{conflictWarning}
</div>
)}
{loading && <div className="fm-loading">Loading</div>}
{!error && !loading && pageVersions.length === 0 && (
<div className="fm-empty">No versions found for this file.</div>
)}
{conflictWarning && (
<div
className="fm-modal-white-section fm-soft-text"
style={{ borderLeft: '3px solid var(--fm-accent, #6aa7ff)' }}
>
{conflictWarning}
</div>
)}
{renameConfirm && (
<ConfirmModal
title={
<>
Restore this version?
<Tooltip label={TOOLTIPS.FILE_OPERATION_RESTORE_VERSION} />
</>
}
message={
<>
Restoring will rename:&nbsp;
<b className="vh-name" title={renameConfirm.headName}>
{truncateNameMiddle(renameConfirm.headName)}
</b>{' '}
{' '}
<b className="vh-name" title={renameConfirm.targetName}>
{truncateNameMiddle(renameConfirm.targetName)}
</b>
.
</>
}
confirmLabel="Restore"
cancelLabel="Cancel"
onConfirm={async () => {
await doRestore(renameConfirm.version)
setRenameConfirm(null)
}}
onCancel={() => setRenameConfirm(null)}
/>
)}
{renameConfirm && (
<ConfirmModal
title={
<>
Restore this version?
<Tooltip label={TOOLTIPS.FILE_OPERATION_RESTORE_VERSION} />
</>
}
message={
<>
Restoring will rename:&nbsp;
<b className="vh-name" title={renameConfirm.headName}>
{truncateNameMiddle(renameConfirm.headName)}
</b>{' '}
{' '}
<b className="vh-name" title={renameConfirm.targetName}>
{truncateNameMiddle(renameConfirm.targetName)}
</b>
.
</>
}
confirmLabel="Restore"
cancelLabel="Cancel"
onConfirm={async () => {
await doRestore(renameConfirm.version)
setRenameConfirm(null)
}}
onCancel={() => setRenameConfirm(null)}
<VersionsList
versions={!error && !loading ? pageVersions : []}
headFi={fileInfo}
restoreVersion={restoreVersion}
onDownload={trackDownload}
/>
)}
<VersionsList
versions={!error && !loading ? pageVersions : []}
headFi={fileInfo}
restoreVersion={restoreVersion}
onDownload={trackDownload}
/>
</div>
</div>
<div className="fm-modal-window-footer vh-footer">
@@ -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;