This commit is contained in:
Alexis Svinartchouk 2019-07-12 18:59:06 +02:00
parent 96c865f14a
commit 1398ca2931
12 changed files with 404 additions and 285 deletions

View File

@ -21,12 +21,12 @@
import * as angular from 'angular';
import { react2angular } from 'react2angular';
const MODULE_NAME = 'Etcher.Components.TargetSelector'
const SelectTargetButton = angular.module(MODULE_NAME, [])
const MODULE_NAME = 'Etcher.Components.TargetSelector';
const SelectTargetButton = angular.module(MODULE_NAME, []);
SelectTargetButton.component(
'targetSelector',
react2angular(require('./target-selector.jsx'))
)
react2angular(require('./target-selector.jsx')),
);
export = MODULE_NAME;

View File

@ -15,21 +15,12 @@
*/
import { Meter } from 'grommet';
import { sortBy } from 'lodash';
import * as React from 'react';
import { Badge, Modal, Table } from 'rendition';
import { getDrives } from '../../models/available-drives';
import {
COMPATIBILITY_STATUS_TYPES,
getDriveImageCompatibilityStatuses,
isDriveValid,
} from '../../modules/drive-constraints';
import {
deselectDrive,
getImage,
isDriveSelected,
selectDrive,
} from '../../models/selection-state';
import { COMPATIBILITY_STATUS_TYPES } from '../../modules/drive-constraints';
import { subscribe } from '../../models/store';
import { ThemedProvider } from '../../styled-components';
import { bytesToClosestUnit } from '../../modules/units';
@ -46,15 +37,6 @@ interface Drive {
displayName: string;
}
interface Image {
path: string;
size: number;
url: string;
name: string;
supportUrl: string;
recommendedDriveSize: number;
}
interface CompatibilityStatus {
type: number;
message: string;
@ -62,13 +44,16 @@ interface CompatibilityStatus {
interface DriveSelectorProps {
close: () => void;
unique: boolean; // TODO
selectDrive: (drive: Drive) => void;
deselectDrive: (drive: Drive) => void;
isDriveSelected: (drive: Drive) => boolean;
isDriveValid: (drive: Drive) => boolean;
getDriveBadges: (drive: Drive) => CompatibilityStatus[];
}
interface DriveSelectorState {
drives: Drive[];
selected: Drive[];
image: Image;
disabledDrives: string[];
}
@ -79,11 +64,11 @@ const modalStyle = {
paddingLeft: '30px',
paddingRight: '30px',
paddingBottom: '11px',
}
};
const titleStyle = {
color: '#2a506f',
}
};
const subtitleStyle = {
marginLeft: '10px',
@ -95,14 +80,17 @@ const wrapperStyle = {
height: '250px',
overflowX: 'hidden' as 'hidden',
overflowY: 'auto' as 'auto',
}
};
export class DriveSelector2 extends React.Component<DriveSelectorProps, DriveSelectorState> {
export class DriveSelector2 extends React.Component<
DriveSelectorProps,
DriveSelectorState
> {
private table: Table<Drive> | null = null;
private columns: {
field: keyof Drive,
label: string,
render?: (value: any, row: Drive) => string | number | JSX.Element | null,
field: keyof Drive;
label: string;
render?: (value: any, row: Drive) => string | number | JSX.Element | null;
}[];
private unsubscribe?: () => void;
@ -147,14 +135,16 @@ export class DriveSelector2 extends React.Component<DriveSelectorProps, DriveSel
}
private getNewState() {
const drives: Drive[] = getDrives();
let drives: Drive[] = getDrives();
for (let i = 0; i < drives.length; i++) {
drives[i] = {...drives[i]};
drives[i] = { ...drives[i] };
}
const selected = drives.filter(d => isDriveSelected(d.device));
const image = getImage();
const disabledDrives = drives.filter(d => !isDriveValid(d, image)).map(d => d.device);
return { drives, disabledDrives, image, selected };
drives = sortBy(drives, 'device');
const selected = drives.filter(d => this.props.isDriveSelected(d));
const disabledDrives = drives
.filter(d => !this.props.isDriveValid(d))
.map(d => d.device);
return { drives, disabledDrives, selected };
}
private update() {
@ -187,48 +177,57 @@ export class DriveSelector2 extends React.Component<DriveSelectorProps, DriveSel
private renderBadges(_value: any, row: Drive) {
const result = [];
if (row.progress !== undefined) {
result.push(<Meter
result.push(
<Meter
size="small"
thickness="xxsmall"
values={
[
values={[
{
value: row.progress,
label: row.progress + '%',
color: '#2297de',
},
]}
/>,
);
}
]
}
/>);
}
result.push(...getDriveImageCompatibilityStatuses(row, this.state.image).map((status: CompatibilityStatus) => {
const props: { key: string, xsmall: true, danger?: boolean, warning?: boolean} = { xsmall: true, key: status.message };
result.push(
...this.props.getDriveBadges(row).map(
(status: CompatibilityStatus) => {
const props: {
key: string;
xsmall: true;
danger?: boolean;
warning?: boolean;
} = { xsmall: true, key: status.message };
if (status.type === COMPATIBILITY_STATUS_TYPES.ERROR) {
props.danger = true;
} else if (status.type === COMPATIBILITY_STATUS_TYPES.WARNING) {
props.warning = true;
}
return <Badge {...props}>{status.message}</Badge>
}))
return <Badge {...props}>{status.message}</Badge>;
},
),
);
return <React.Fragment>{result}</React.Fragment>;
}
private renderTbodyPrefix() {
if (this.state.drives.length === 0) {
return <tr>
<td
colSpan={this.columns.length}
style={{ textAlign: 'center' }}
>
return (
<tr>
<td colSpan={this.columns.length} style={{ textAlign: 'center' }}>
<b>Connect a drive</b>
<div>No removable drive detected.</div>
</td>
</tr>
);
}
}
public render() {
return <ThemedProvider>
return (
<ThemedProvider>
<Modal
titleElement={
<div style={titleStyle}>
@ -244,30 +243,30 @@ export class DriveSelector2 extends React.Component<DriveSelectorProps, DriveSel
>
<div style={wrapperStyle}>
<Table<Drive>
ref={(t) => {
ref={t => {
this.table = t;
this.updateTableSelection();
}}
rowKey='device'
onCheck={this.onCheck}
rowKey="device"
onCheck={this.onCheck.bind(this)}
columns={this.columns}
data={this.state.drives}
disabledRows={this.state.disabledDrives}
tbodyPrefix={this.renderTbodyPrefix()}
>
</Table>
/>
</div>
</Modal>
</ThemedProvider>
);
}
private onCheck(checkedDrives: Drive[]): void {
const checkedDevices = checkedDrives.map(d => d.device);
for (const drive of getDrives()) {
if (checkedDevices.indexOf(drive.device) !== -1) {
selectDrive(drive.device);
this.props.selectDrive(drive);
} else {
deselectDrive(drive.device);
this.props.deselectDrive(drive);
}
}
}

View File

@ -19,10 +19,16 @@ import { react2angular } from 'react2angular';
import { DriveSelector2 } from './drive-selector.tsx';
const MODULE_NAME = 'Etcher.Components.DriveSelector2'
const MODULE_NAME = 'Etcher.Components.DriveSelector2';
angular
.module(MODULE_NAME, [])
.component('driveSelector2', react2angular(DriveSelector2, ['close']))
.component(
'driveSelector2',
react2angular(
DriveSelector2,
['close', 'selectDrive', 'deselectDrive', 'isDriveSelected', 'isDriveValid', 'getDriveBadges']
)
)
export = MODULE_NAME;

View File

@ -19,7 +19,8 @@
/* eslint-disable no-unused-vars */
const React = require('react')
const propTypes = require('prop-types')
const { Badge, Select } = require('rendition')
const { Badge, DropDownButton, Select } = require('rendition')
const { default: styled } = require('styled-components')
const middleEllipsis = require('./../../utils/middle-ellipsis')
@ -35,6 +36,17 @@ const {
ThemedProvider
} = require('./../../styled-components')
const DropdownItem = styled.p`
padding-top: 10px;
text-align: left;
width: 150px;
cursor: pointer;
`
const DropdownItemIcon = styled.i`
padding-right: 10px;
`
const SelectImageButton = (props) => {
if (props.hasImage) {
return (
@ -51,9 +63,9 @@ const SelectImageButton = (props) => {
<ChangeButton
plain
mb={14}
onClick={props.reselectImage}
onClick={props.deselectImage}
>
Change
Remove
</ChangeButton>
}
<DetailsText>
@ -65,18 +77,26 @@ const SelectImageButton = (props) => {
return (
<ThemedProvider>
<StepSelection>
<Select
value={props.sourceType}
onChange={(e) => {console.log('changed')}}
<DropDownButton
primary
label={
<div onClick={props.openImageSelector}>Select image</div>
}
style={{height: '48px'}}
>
<option value={'image'}>Select image file</option>
<option value={'drive'}>Duplicate drive</option>
</Select>
<StepButton
<DropdownItem
onClick={props.openImageSelector}
>
Select image
</StepButton>
<DropdownItemIcon className="far fa-file"/>
Select image file
</DropdownItem>
<DropdownItem
onClick={props.openDriveSelector}
>
<DropdownItemIcon className="far fa-copy"/>
Duplicate drive
</DropdownItem>
</DropDownButton>
<Footer>
{ props.mainSupportedExtensions.join(', ') }, and{' '}
<Underline
@ -92,13 +112,14 @@ const SelectImageButton = (props) => {
SelectImageButton.propTypes = {
openImageSelector: propTypes.func,
openDriveSelector: propTypes.func,
mainSupportedExtensions: propTypes.array,
extraSupportedExtensions: propTypes.array,
hasImage: propTypes.bool,
showSelectedImageDetails: propTypes.func,
imageName: propTypes.string,
imageBasename: propTypes.string,
reselectImage: propTypes.func,
deselectImage: propTypes.func,
flashing: propTypes.bool,
imageSize: propTypes.number,
sourceType: propTypes.string

View File

@ -68,10 +68,7 @@ const flashStateNoNilFields = [
* @constant
* @private
*/
const selectImageNoNilFields = [
'path',
'extension'
]
const selectImageNoNilFields = [ 'path' ]
/**
* @summary Application default state
@ -385,6 +382,7 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
})
}
if (!action.data.isDrive) { // We don't care about extensions if the source is a drive
if (!_.isString(action.data.extension)) {
throw errors.createError({
title: `Invalid image extension: ${action.data.extension}`
@ -423,6 +421,7 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
})
}
}
}
const MINIMUM_IMAGE_SIZE = 0

View File

@ -23,6 +23,7 @@ const store = require('../../../models/store')
// eslint-disable-next-line node/no-missing-require
const settings = require('../../../models/settings')
const selectionState = require('../../../models/selection-state')
const driveConstraints = require('../../../modules/drive-constraints')
const analytics = require('../../../modules/analytics')
const exceptionReporter = require('../../../modules/exception-reporter')
// eslint-disable-next-line node/no-missing-require
@ -169,4 +170,25 @@ module.exports = function ($timeout, DriveSelectorService) {
// Trigger re-render
$timeout()
}
this.selectDrive = (drive) => {
return selectionState.selectDrive(drive.device)
}
this.deselectDrive = (drive) => {
return selectionState.deselectDrive(drive.device)
}
this.isDriveSelected = (drive) => {
return selectionState.isDriveSelected(drive.device)
}
this.isDriveValid = (drive) => {
return driveConstraints.isDriveValid(drive, selectionState.getImage());
}
this.getDriveBadges = (drive) => {
return driveConstraints.getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
}
}

View File

@ -247,6 +247,11 @@ module.exports = function (
this.openImageSelector()
}
this.deselectImage = () => {
selectionState.deselectImage()
$timeout()
}
/**
* @summary Get the basename of the selected image
* @function
@ -264,4 +269,44 @@ module.exports = function (
return path.basename(selectionState.getImagePath())
}
this.driveSelectorModalOpen = false
this.openDriveSelector = () => {
this.driveSelectorModalOpen = true;
$timeout()
}
this.closeDriveSelector = () => {
this.driveSelectorModalOpen = false;
$timeout()
}
this.selectDrive = (drive) => {
selectionState.selectImage({
path: drive.device,
size: drive.size,
isDrive: true
})
}
this.deselectDrive = (drive) => {
if (this.isDriveSelected(drive)) {
this.deselectImage()
}
}
this.isDriveSelected = (drive) => {
const image = selectionState.getImage()
return image && image.isDrive && image.path === drive.device
}
this.isDriveValid = (drive) => {
return true // TODO: not valid if already a destination drive
}
this.getDriveBadges = (drive) => {
return [] // TODO: selected as destination (same as above)
}
}

View File

@ -1,5 +1,15 @@
<div class="page-main row around-xs">
<div class="col-xs" ng-controller="ImageSelectionController as image">
<drive-selector-2
ng-if="image.driveSelectorModalOpen"
close="image.closeDriveSelectorModal"
select-drive="image.selectDrive"
deselect-drive="image.deselectDrive"
is-drive-selected="image.isDriveSelected"
is-drive-valid="image.isDriveValid"
get-drive-badges="image.getDriveBadges"
>
</drive-selector-2>
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
<div class="center-block">
@ -10,12 +20,13 @@
<image-selector
has-image="main.selection.hasImage()"
open-image-selector="image.openImageSelector"
open-drive-selector="image.openDriveSelector"
main-supported-extensions="image.mainSupportedExtensions"
extra-supported-extensions="image.extraSupportedExtensions"
show-selected-image-details="main.showSelectedImageDetails"
image-name="main.selection.getImageName()"
image-basename="image.getImageBasename()"
reselect-image="image.reselectImage"
deselect-image="image.deselectImage"
flashing="main.state.isFlashing()"
image-size="main.selection.getImageSize()"
>
@ -29,7 +40,11 @@
<drive-selector-2
ng-if="drive.driveSelectorModalOpen"
close="drive.closeDriveSelectorModal"
wololo="wololo"
select-drive="drive.selectDrive"
deselect-drive="drive.deselectDrive"
is-drive-selected="drive.isDriveSelected"
is-drive-valid="drive.isDriveValid"
get-drive-badges="drive.getDriveBadges"
>
</drive-selector-2>
<div class="box text-center relative">

View File

@ -45,6 +45,7 @@ $fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free-webfonts/web
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fontawesome";
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-solid";
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-regular";
@font-face {
font-family: 'Nunito';

View File

@ -9860,6 +9860,17 @@ readers do not read off random characters that represent icons */
font-family: 'Font Awesome 5 Free';
font-weight: 900; }
@font-face {
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 400;
src: url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.eot");
src: url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.woff2") format("woff2"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.woff") format("woff"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.ttf") format("truetype"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
.far {
font-family: 'Font Awesome 5 Free';
font-weight: 400; }
@font-face {
font-family: 'Nunito';
src: url("Nunito-Regular.eot");

View File

@ -20,7 +20,7 @@
},
"scripts": {
"test": "make lint test sanity-checks",
"prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\"",
"prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\" \"lib/**/*.tsx\"",
"start": "./node_modules/.bin/electron .",
"postshrinkwrap": "node ./scripts/clean-shrinkwrap.js",
"configure": "node-gyp configure",

@ -1 +1 @@
Subproject commit 1b5bb595fe00a81e9a12df654c9909e674997dd9
Subproject commit 022f4509c58b3e2a5ca32a78d10bb7d59a2747a4