Split drive selector from target selector

Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
Lorenzo Alberto Maria Ambrosi 2020-06-22 17:15:55 +02:00
parent 07befd0bd1
commit 377dfb8e22
6 changed files with 104 additions and 62 deletions

View File

@ -33,7 +33,7 @@ import {
getDriveImageCompatibilityStatuses, getDriveImageCompatibilityStatuses,
hasListDriveImageCompatibilityStatus, hasListDriveImageCompatibilityStatus,
isDriveValid, isDriveValid,
TargetStatus, DriveStatus,
Image, Image,
} from '../../../../shared/drive-constraints'; } from '../../../../shared/drive-constraints';
import { compatibility } from '../../../../shared/messages'; import { compatibility } from '../../../../shared/messages';
@ -49,7 +49,7 @@ import { logEvent, logException } from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal, ScrollableFlex } from '../../styled-components'; import { Modal, ScrollableFlex } from '../../styled-components';
import TargetSVGIcon from '../../../assets/tgt.svg'; import DriveSVGIcon from '../../../assets/tgt.svg';
interface UsbbootDrive extends sourceDestination.UsbbootDrive { interface UsbbootDrive extends sourceDestination.UsbbootDrive {
progress: number; progress: number;
@ -64,26 +64,26 @@ interface DriverlessDrive {
linkCTA: string; linkCTA: string;
} }
type Target = scanner.adapters.DrivelistDrive | DriverlessDrive | UsbbootDrive; type Drive = scanner.adapters.DrivelistDrive | DriverlessDrive | UsbbootDrive;
function isUsbbootDrive(drive: Target): drive is UsbbootDrive { function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
return (drive as UsbbootDrive).progress !== undefined; return (drive as UsbbootDrive).progress !== undefined;
} }
function isDriverlessDrive(drive: Target): drive is DriverlessDrive { function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
return (drive as DriverlessDrive).link !== undefined; return (drive as DriverlessDrive).link !== undefined;
} }
function isDrivelistDrive( function isDrivelistDrive(
drive: Target, drive: Drive,
): drive is scanner.adapters.DrivelistDrive { ): drive is scanner.adapters.DrivelistDrive {
return typeof (drive as scanner.adapters.DrivelistDrive).size === 'number'; return typeof (drive as scanner.adapters.DrivelistDrive).size === 'number';
} }
const TargetsTable = styled(({ refFn, ...props }) => { const DrivesTable = styled(({ refFn, ...props }) => {
return ( return (
<div> <div>
<Table<Target> ref={refFn} {...props} /> <Table<Drive> ref={refFn} {...props} />
</div> </div>
); );
})` })`
@ -145,30 +145,37 @@ const InitProgress = styled(
} }
`; `;
interface TargetSelectorModalProps extends Omit<ModalProps, 'done'> { export interface DriveSelectorProps
done: (targets: scanner.adapters.DrivelistDrive[]) => void; extends Omit<ModalProps, 'done' | 'cancel'> {
multipleSelection?: boolean;
cancel: () => void;
done: (drives: scanner.adapters.DrivelistDrive[]) => void;
titleLabel: string;
emptyListLabel: string;
} }
interface TargetSelectorModalState { interface DriveSelectorState {
drives: Target[]; drives: Drive[];
image: Image; image: Image;
missingDriversModal: { drive?: DriverlessDrive }; missingDriversModal: { drive?: DriverlessDrive };
selectedList: scanner.adapters.DrivelistDrive[]; selectedList: scanner.adapters.DrivelistDrive[];
showSystemDrives: boolean; showSystemDrives: boolean;
} }
export class TargetSelectorModal extends React.Component< export class DriveSelector extends React.Component<
TargetSelectorModalProps, DriveSelectorProps,
TargetSelectorModalState DriveSelectorState
> { > {
private unsubscribe: (() => void) | undefined; private unsubscribe: (() => void) | undefined;
tableColumns: Array<TableColumn<Target>>; multipleSelection: boolean;
tableColumns: Array<TableColumn<Drive>>;
constructor(props: TargetSelectorModalProps) { constructor(props: DriveSelectorProps) {
super(props); super(props);
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {}; const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const selectedList = getSelectedDrives(); const selectedList = getSelectedDrives();
this.multipleSelection = !!this.props.multipleSelection;
this.state = { this.state = {
drives: getDrives(), drives: getDrives(),
@ -182,7 +189,7 @@ export class TargetSelectorModal extends React.Component<
{ {
field: 'description', field: 'description',
label: 'Name', label: 'Name',
render: (description: string, drive: Target) => { render: (description: string, drive: Drive) => {
return isDrivelistDrive(drive) && drive.isSystem ? ( return isDrivelistDrive(drive) && drive.isSystem ? (
<Flex alignItems="center"> <Flex alignItems="center">
<ExclamationTriangleSvg height="1em" fill="#fca321" /> <ExclamationTriangleSvg height="1em" fill="#fca321" />
@ -197,7 +204,7 @@ export class TargetSelectorModal extends React.Component<
field: 'description', field: 'description',
key: 'size', key: 'size',
label: 'Size', label: 'Size',
render: (_description: string, drive: Target) => { render: (_description: string, drive: Drive) => {
if (isDrivelistDrive(drive) && drive.size !== null) { if (isDrivelistDrive(drive) && drive.size !== null) {
return bytesToClosestUnit(drive.size); return bytesToClosestUnit(drive.size);
} }
@ -207,7 +214,7 @@ export class TargetSelectorModal extends React.Component<
field: 'description', field: 'description',
key: 'link', key: 'link',
label: 'Location', label: 'Location',
render: (_description: string, drive: Target) => { render: (_description: string, drive: Drive) => {
return ( return (
<Txt> <Txt>
{drive.displayName} {drive.displayName}
@ -231,7 +238,7 @@ export class TargetSelectorModal extends React.Component<
key: 'extra', key: 'extra',
// Space as empty string would use the field name as label // Space as empty string would use the field name as label
label: ' ', label: ' ',
render: (_description: string, drive: Target) => { render: (_description: string, drive: Drive) => {
if (isUsbbootDrive(drive)) { if (isUsbbootDrive(drive)) {
return this.renderProgress(drive.progress); return this.renderProgress(drive.progress);
} else if (isDrivelistDrive(drive)) { } else if (isDrivelistDrive(drive)) {
@ -244,7 +251,7 @@ export class TargetSelectorModal extends React.Component<
]; ];
} }
private driveShouldBeDisabled(drive: Target, image: any) { private driveShouldBeDisabled(drive: Drive, image: any) {
return ( return (
isUsbbootDrive(drive) || isUsbbootDrive(drive) ||
isDriverlessDrive(drive) || isDriverlessDrive(drive) ||
@ -252,8 +259,8 @@ export class TargetSelectorModal extends React.Component<
); );
} }
private getDisplayedTargets(targets: Target[]): Target[] { private getDisplayedDrives(drives: Drive[]): Drive[] {
return targets.filter((drive) => { return drives.filter((drive) => {
return ( return (
isUsbbootDrive(drive) || isUsbbootDrive(drive) ||
isDriverlessDrive(drive) || isDriverlessDrive(drive) ||
@ -264,7 +271,7 @@ export class TargetSelectorModal extends React.Component<
}); });
} }
private getDisabledTargets(drives: Target[], image: any): string[] { private getDisabledDrives(drives: Drive[], image: any): string[] {
return drives return drives
.filter((drive) => this.driveShouldBeDisabled(drive, image)) .filter((drive) => this.driveShouldBeDisabled(drive, image))
.map((drive) => drive.displayName); .map((drive) => drive.displayName);
@ -279,7 +286,7 @@ export class TargetSelectorModal extends React.Component<
); );
} }
private renderStatuses(statuses: TargetStatus[]) { private renderStatuses(statuses: DriveStatus[]) {
return ( return (
// the column render fn expects a single Element // the column render fn expects a single Element
<> <>
@ -324,12 +331,12 @@ export class TargetSelectorModal extends React.Component<
const { cancel, done, ...props } = this.props; const { cancel, done, ...props } = this.props;
const { selectedList, drives, image, missingDriversModal } = this.state; const { selectedList, drives, image, missingDriversModal } = this.state;
const displayedTargets = this.getDisplayedTargets(drives); const displayedDrives = this.getDisplayedDrives(drives);
const disabledTargets = this.getDisabledTargets(drives, image); const disabledDrives = this.getDisabledDrives(drives, image);
const numberOfSystemDrives = drives.filter( const numberOfSystemDrives = drives.filter(
(drive) => isDrivelistDrive(drive) && drive.isSystem, (drive) => isDrivelistDrive(drive) && drive.isSystem,
).length; ).length;
const numberOfDisplayedSystemDrives = displayedTargets.filter( const numberOfDisplayedSystemDrives = displayedDrives.filter(
(drive) => isDrivelistDrive(drive) && drive.isSystem, (drive) => isDrivelistDrive(drive) && drive.isSystem,
).length; ).length;
const numberOfHiddenSystemDrives = const numberOfHiddenSystemDrives =
@ -341,7 +348,7 @@ export class TargetSelectorModal extends React.Component<
titleElement={ titleElement={
<Flex alignItems="baseline" mb={18}> <Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left"> <Txt fontSize={24} align="left">
Select target {this.props.titleLabel}
</Txt> </Txt>
<Txt <Txt
fontSize={11} fontSize={11}
@ -372,45 +379,61 @@ export class TargetSelectorModal extends React.Component<
alignItems="center" alignItems="center"
width="100%" width="100%"
> >
<TargetSVGIcon width="40px" height="90px" /> <DriveSVGIcon width="40px" height="90px" />
<b>Plug a target drive</b> <b>{this.props.emptyListLabel}</b>
</Flex> </Flex>
) : ( ) : (
<ScrollableFlex flexDirection="column" width="100%"> <ScrollableFlex
<TargetsTable flexDirection="column"
refFn={(t: Table<Target>) => { width="100%"
>
<DrivesTable
refFn={(t: Table<Drive>) => {
if (t !== null) { if (t !== null) {
t.setRowSelection(selectedList); t.setRowSelection(selectedList);
} }
}} }}
columns={this.tableColumns} columns={this.tableColumns}
data={displayedTargets} data={displayedDrives}
disabledRows={disabledTargets} disabledRows={disabledDrives}
rowKey="displayName" rowKey="displayName"
onCheck={(rows: Target[]) => { onCheck={(rows: Drive[]) => {
const newSelection = rows.filter(isDrivelistDrive);
if (this.multipleSelection) {
this.setState({
selectedList: newSelection,
});
return;
}
this.setState({ this.setState({
selectedList: rows.filter(isDrivelistDrive), selectedList: newSelection.slice(newSelection.length - 1),
}); });
}} }}
onRowClick={(row: Target) => { onRowClick={(row: Drive) => {
if ( if (
!isDrivelistDrive(row) || !isDrivelistDrive(row) ||
this.driveShouldBeDisabled(row, image) this.driveShouldBeDisabled(row, image)
) { ) {
return; return;
} }
const newList = [...selectedList]; if (this.multipleSelection) {
const selectedIndex = selectedList.findIndex( const newList = [...selectedList];
(target) => target.device === row.device, const selectedIndex = selectedList.findIndex(
); (drive) => drive.device === row.device,
if (selectedIndex === -1) { );
newList.push(row); if (selectedIndex === -1) {
} else { newList.push(row);
// Deselect if selected } else {
newList.splice(selectedIndex, 1); // Deselect if selected
newList.splice(selectedIndex, 1);
}
this.setState({
selectedList: newList,
});
return;
} }
this.setState({ this.setState({
selectedList: newList, selectedList: [row],
}); });
}} }}
/> />

View File

@ -67,7 +67,7 @@ function DriveCompatibilityWarning({
); );
} }
export function TargetSelector(props: TargetSelectorProps) { export function TargetSelectorButton(props: TargetSelectorProps) {
const targets = getSelectedDrives(); const targets = getSelectedDrives();
if (targets.length === 1) { if (targets.length === 1) {

View File

@ -19,6 +19,10 @@ import * as React from 'react';
import { Flex } from 'rendition'; import { Flex } from 'rendition';
import { TargetSelector } from '../../components/target-selector/target-selector-button'; import { TargetSelector } from '../../components/target-selector/target-selector-button';
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal'; import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
import {
DriveSelector,
DriveSelectorProps,
} from '../drive-selector/drive-selector';
import { import {
isDriveSelected, isDriveSelected,
getImage, getImage,
@ -50,6 +54,16 @@ const getDriveSelectionStateSlice = () => ({
image: getImage(), image: getImage(),
}); });
export const TargetSelectorModal = (
props: Omit<DriveSelectorProps, 'titleLabel' | 'emptyListLabel'>,
) => (
<DriveSelector
titleLabel="Select target"
emptyListLabel="Plug a target drive"
{...props}
/>
);
export const selectAllTargets = ( export const selectAllTargets = (
modalTargets: scanner.adapters.DrivelistDrive[], modalTargets: scanner.adapters.DrivelistDrive[],
) => { ) => {
@ -79,17 +93,17 @@ export const selectAllTargets = (
}); });
}; };
interface DriveSelectorProps { interface TargetSelectorProps {
disabled: boolean; disabled: boolean;
hasDrive: boolean; hasDrive: boolean;
flashing: boolean; flashing: boolean;
} }
export const DriveSelector = ({ export const TargetSelector = ({
disabled, disabled,
hasDrive, hasDrive,
flashing, flashing,
}: DriveSelectorProps) => { }: TargetSelectorProps) => {
// TODO: inject these from redux-connector // TODO: inject these from redux-connector
const [ const [
{ showDrivesButton, driveListLabel, targets, image }, { showDrivesButton, driveListLabel, targets, image },
@ -115,7 +129,7 @@ export const DriveSelector = ({
}} }}
/> />
<TargetSelector <TargetSelectorButton
disabled={disabled} disabled={disabled}
show={!hasDrive && showDrivesButton} show={!hasDrive && showDrivesButton}
tooltip={driveListLabel} tooltip={driveListLabel}
@ -138,7 +152,7 @@ export const DriveSelector = ({
selectAllTargets(modalTargets); selectAllTargets(modalTargets);
setShowTargetSelectorModal(false); setShowTargetSelectorModal(false);
}} }}
></TargetSelectorModal> />
)} )}
</Flex> </Flex>
); );

View File

@ -24,7 +24,6 @@ import * as constraints from '../../../../shared/drive-constraints';
import * as messages from '../../../../shared/messages'; import * as messages from '../../../../shared/messages';
import { ProgressButton } from '../../components/progress-button/progress-button'; import { ProgressButton } from '../../components/progress-button/progress-button';
import { SourceOptions } from '../../components/source-selector/source-selector'; import { SourceOptions } from '../../components/source-selector/source-selector';
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
import * as availableDrives from '../../models/available-drives'; import * as availableDrives from '../../models/available-drives';
import * as flashState from '../../models/flash-state'; import * as flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state'; import * as selection from '../../models/selection-state';
@ -32,7 +31,10 @@ import * as analytics from '../../modules/analytics';
import { scanner as driveScanner } from '../../modules/drive-scanner'; import { scanner as driveScanner } from '../../modules/drive-scanner';
import * as imageWriter from '../../modules/image-writer'; import * as imageWriter from '../../modules/image-writer';
import * as notification from '../../os/notification'; import * as notification from '../../os/notification';
import { selectAllTargets } from './DriveSelector'; import {
selectAllTargets,
TargetSelectorModal,
} from '../../components/target-selector/target-selector';
import FlashSvg from '../../../assets/flash.svg'; import FlashSvg from '../../../assets/flash.svg';
@ -333,7 +335,7 @@ export class FlashStep extends React.PureComponent<
selectAllTargets(modalTargets); selectAllTargets(modalTargets);
this.setState({ showDriveSelectorModal: false }); this.setState({ showDriveSelectorModal: false });
}} }}
></TargetSelectorModal> />
)} )}
</> </>
); );

View File

@ -44,7 +44,10 @@ import {
import { bytesToClosestUnit } from '../../../../shared/units'; import { bytesToClosestUnit } from '../../../../shared/units';
import { DriveSelector, getDriveListLabel } from './DriveSelector'; import {
TargetSelector,
getDriveListLabel,
} from '../../components/target-selector/target-selector';
import { FlashStep } from './Flash'; import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg'; import EtcherSvg from '../../../assets/etcher.svg';
@ -252,7 +255,7 @@ export class MainPage extends React.Component<
<Flex> <Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left /> <StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex> </Flex>
<DriveSelector <TargetSelector
disabled={shouldDriveStepBeDisabled} disabled={shouldDriveStepBeDisabled}
hasDrive={this.state.hasDrive} hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing} flashing={this.state.isFlashing}

View File

@ -276,7 +276,7 @@ export function hasListDriveImageCompatibilityStatus(
return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length); return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length);
} }
export interface TargetStatus { export interface DriveStatus {
message: string; message: string;
type: number; type: number;
} }