patch: Select drive on list interaction rather than modal closing

Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
Lorenzo Alberto Maria Ambrosi 2021-04-19 12:58:25 +02:00
parent ccb08a48f1
commit 061afca5d3
7 changed files with 113 additions and 103 deletions

View File

@ -43,6 +43,7 @@ import {
} from '../../styled-components';
import { SourceMetadata } from '../source-selector/source-selector';
import { middleEllipsis } from '../../utils/middle-ellipsis';
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
progress: number;
@ -136,17 +137,18 @@ const InitProgress = styled(
`;
export interface DriveSelectorProps
extends Omit<ModalProps, 'done' | 'cancel'> {
extends Omit<ModalProps, 'done' | 'cancel' | 'onSelect'> {
write: boolean;
multipleSelection: boolean;
showWarnings?: boolean;
cancel: () => void;
cancel: (drives: DrivelistDrive[]) => void;
done: (drives: DrivelistDrive[]) => void;
titleLabel: string;
emptyListLabel: string;
emptyListIcon: JSX.Element;
selectedList?: DrivelistDrive[];
updateSelectedList?: () => DrivelistDrive[];
onSelect?: (drive: DrivelistDrive) => void;
}
interface DriveSelectorState {
@ -167,12 +169,14 @@ export class DriveSelector extends React.Component<
> {
private unsubscribe: (() => void) | undefined;
tableColumns: Array<TableColumn<Drive>>;
originalList: DrivelistDrive[];
constructor(props: DriveSelectorProps) {
super(props);
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const selectedList = this.props.selectedList || [];
this.originalList = [...(this.props.selectedList || [])];
this.state = {
drives: getDrives(),
@ -199,7 +203,9 @@ export class DriveSelector extends React.Component<
fill={drive.isSystem ? '#fca321' : '#8f9297'}
/>
)}
<Txt ml={(hasWarnings && 8) || 0}>{description}</Txt>
<Txt ml={(hasWarnings && 8) || 0}>
{middleEllipsis(description, 32)}
</Txt>
</Flex>
);
}
@ -259,7 +265,7 @@ export class DriveSelector extends React.Component<
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
!isDriveValid(drive, image) ||
!isDriveValid(drive, image, this.props.write) ||
(this.props.write && drive.isReadOnly)
);
}
@ -348,16 +354,6 @@ export class DriveSelector extends React.Component<
}
}
private deselectingAll(rows: DrivelistDrive[]) {
return (
rows.length > 0 &&
rows.length === this.state.selectedList.length &&
this.state.selectedList.every(
(d) => rows.findIndex((r) => d.device === r.device) > -1,
)
);
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
const drives = getDrives();
@ -408,7 +404,7 @@ export class DriveSelector extends React.Component<
</Flex>
}
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={cancel}
cancel={() => cancel(this.originalList)}
done={() => done(selectedList)}
action={`Select (${selectedList.length})`}
primaryButtonProps={{
@ -448,14 +444,34 @@ export class DriveSelector extends React.Component<
onCheck={(rows: Drive[]) => {
let newSelection = rows.filter(isDrivelistDrive);
if (this.props.multipleSelection) {
if (this.deselectingAll(newSelection)) {
if (rows.length === 0) {
newSelection = [];
}
const deselecting = selectedList.filter(
(selected) =>
newSelection.filter(
(row) => row.device === selected.device,
).length === 0,
);
const selecting = newSelection.filter(
(row) =>
selectedList.filter(
(selected) => row.device === selected.device,
).length === 0,
);
deselecting.concat(selecting).forEach((row) => {
if (this.props.onSelect) {
this.props.onSelect(row);
}
});
this.setState({
selectedList: newSelection,
});
return;
}
if (this.props.onSelect) {
this.props.onSelect(newSelection[newSelection.length - 1]);
}
this.setState({
selectedList: newSelection.slice(newSelection.length - 1),
});
@ -467,6 +483,9 @@ export class DriveSelector extends React.Component<
) {
return;
}
if (this.props.onSelect) {
this.props.onSelect(row);
}
const index = selectedList.findIndex(
(d) => d.device === row.device,
);

View File

@ -558,6 +558,12 @@ export class SourceSelector extends React.Component<
this.setState({ defaultFlowActive });
}
private closeModal() {
this.setState({
showDriveSelector: false,
});
}
// TODO add a visual change when dragging a file over the selector
public render() {
const { flashing } = this.props;
@ -661,6 +667,9 @@ export class SourceSelector extends React.Component<
{this.state.warning != null && (
<SmallModal
style={{
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
}}
titleElement={
<span>
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
@ -736,21 +745,30 @@ export class SourceSelector extends React.Component<
titleLabel="Select source"
emptyListLabel="Plug a source drive"
emptyListIcon={<SrcSvg width="40px" />}
cancel={() => {
this.setState({
showDriveSelector: false,
});
}}
done={async (drives: DrivelistDrive[]) => {
if (drives.length) {
await this.selectSource(
drives[0],
sourceDestination.BlockDevice,
);
cancel={(originalList) => {
if (originalList.length) {
const originalSource = originalList[0];
if (selectionImage?.drive?.device !== originalSource.device) {
this.selectSource(
originalSource,
sourceDestination.BlockDevice,
);
}
} else {
selectionState.deselectImage();
}
this.closeModal();
}}
done={() => this.closeModal()}
onSelect={(drive) => {
if (drive) {
if (
selectionState.getImage()?.drive?.device === drive?.device
) {
return selectionState.deselectImage();
}
this.selectSource(drive, sourceDestination.BlockDevice);
}
this.setState({
showDriveSelector: false,
});
}}
/>
)}

View File

@ -28,6 +28,7 @@ import {
getSelectedDrives,
deselectDrive,
selectDrive,
deselectAllDrives,
} from '../../models/selection-state';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
@ -164,11 +165,30 @@ export const TargetSelector = ({
{showTargetSelectorModal && (
<TargetSelectorModal
write={true}
cancel={() => setShowTargetSelectorModal(false)}
done={(modalTargets) => {
selectAllTargets(modalTargets);
cancel={(originalList) => {
if (originalList.length) {
selectAllTargets(originalList);
} else {
deselectAllDrives();
}
setShowTargetSelectorModal(false);
}}
done={(modalTargets) => {
if (modalTargets.length === 0) {
deselectAllDrives();
}
setShowTargetSelectorModal(false);
}}
onSelect={(drive) => {
if (
getSelectedDrives().find(
(selectedDrive) => selectedDrive.device === drive.device,
)
) {
return deselectDrive(drive.device);
}
selectDrive(drive.device);
}}
/>
)}
</Flex>

View File

@ -17,12 +17,11 @@
import * as _ from 'lodash';
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
import {
isSourceDrive,
DrivelistDrive,
} from '../../../shared/drive-constraints';
import { DrivelistDrive } from '../../../shared/drive-constraints';
import { getDrives } from './available-drives';
import { getImage, getSelectedDrives } from './selection-state';
import * as settings from './settings';
import { DEFAULT_STATE, observe } from './store';
import { observe, store } from './store';
const leds: Map<string, RGBLed> = new Map();
const animator = new Animator([], 10);
@ -40,7 +39,7 @@ function createAnimationFunction(
): AnimationFunction {
return (t: number): Color => {
const intensity = intensityFunction(t);
return color.map((v) => v * intensity) as Color;
return color.map((v: number) => v * intensity) as Color;
};
}
@ -160,35 +159,28 @@ export function updateLeds({
animator.mapping = mapping;
}
interface DeviceFromState {
devicePath?: string;
device: string;
}
let ledsState: LedsState | undefined;
function stateObserver(state: typeof DEFAULT_STATE) {
const s = state.toJS();
function stateObserver() {
const s = store.getState().toJS();
let step: 'main' | 'flashing' | 'verifying' | 'finish';
if (s.isFlashing) {
step = s.flashState.type;
} else {
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
}
const availableDrives = s.availableDrives.filter(
(d: DeviceFromState) => d.devicePath,
const availableDrives = getDrives().filter(
(d: DrivelistDrive) => d.devicePath,
);
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
isSourceDrive(d, s.selection.image),
)[0]?.devicePath;
const sourceDrivePath = getImage()?.drive?.devicePath;
const availableDrivesPaths = availableDrives.map(
(d: DeviceFromState) => d.devicePath,
(d: DrivelistDrive) => d.devicePath,
);
let selectedDrivesPaths: string[];
if (step === 'main') {
selectedDrivesPaths = availableDrives
.filter((d: DrivelistDrive) => s.selection.devices.includes(d.device))
.map((d: DrivelistDrive) => d.devicePath);
selectedDrivesPaths = getSelectedDrives()
.filter((drive) => drive.devicePath !== null)
.map((drive) => drive.devicePath) as string[];
} else {
selectedDrivesPaths = s.devicePaths;
}
@ -201,7 +193,7 @@ function stateObserver(state: typeof DEFAULT_STATE) {
availableDrives: availableDrivesPaths,
selectedDrives: selectedDrivesPaths,
failedDrives: failedDevicePaths,
};
} as LedsState;
if (!_.isEqual(newLedsState, ledsState)) {
updateLeds(newLedsState);
ledsState = newLedsState;

View File

@ -276,7 +276,7 @@ export class MainPage extends React.Component<
style={{
// Allow window to be dragged from header
// @ts-ignore
'-webkit-app-region': 'drag',
WebkitAppRegion: 'drag',
position: 'relative',
zIndex: 2,
}}
@ -304,7 +304,7 @@ export class MainPage extends React.Component<
onClick={() => this.setState({ hideSettings: false })}
style={{
// Make touch events click instead of dragging
'-webkit-app-region': 'no-drag',
WebkitAppRegion: 'no-drag',
}}
/>
{!settings.getSync('disableExternalLinks') && (
@ -319,7 +319,7 @@ export class MainPage extends React.Component<
tabIndex={6}
style={{
// Make touch events click instead of dragging
'-webkit-app-region': 'no-drag',
WebkitAppRegion: 'no-drag',
}}
/>
)}

View File

@ -104,24 +104,19 @@ export function isDriveLargeEnough(
return driveSize >= (image.size || UNKNOWN_SIZE);
}
/**
* @summary Check if a drive is disabled (i.e. not ready for selection)
*/
export function isDriveDisabled(drive: DrivelistDrive): boolean {
return drive.disabled || false;
}
/**
* @summary Check if a drive is valid, i.e. large enough for an image
*/
export function isDriveValid(
drive: DrivelistDrive,
image?: SourceMetadata,
write: boolean = true,
): boolean {
return (
isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image as SourceMetadata) &&
!isDriveDisabled(drive)
!write ||
(!drive.disabled &&
isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image as SourceMetadata))
);
}

View File

@ -514,40 +514,6 @@ describe('Shared: DriveConstraints', function () {
});
});
describe('.isDriveDisabled()', function () {
it('should return true if the drive is disabled', function () {
const result = constraints.isDriveDisabled(({
device: '/dev/disk1',
size: 1000000000,
isReadOnly: false,
disabled: true,
} as unknown) as constraints.DrivelistDrive);
expect(result).to.be.true;
});
it('should return false if the drive is not disabled', function () {
const result = constraints.isDriveDisabled(({
device: '/dev/disk1',
size: 1000000000,
isReadOnly: false,
disabled: false,
} as unknown) as constraints.DrivelistDrive);
expect(result).to.be.false;
});
it('should return false if "disabled" is undefined', function () {
const result = constraints.isDriveDisabled({
device: '/dev/disk1',
size: 1000000000,
isReadOnly: false,
} as constraints.DrivelistDrive);
expect(result).to.be.false;
});
});
describe('.isDriveSizeRecommended()', function () {
const image: SourceMetadata = {
description: 'rpi.img',