[#38: Enhance new repertory release available notification - partial] [Added FocusTrap to modals]

This commit is contained in:
2020-02-20 13:20:17 -06:00
parent e647c2c8a6
commit a8c0a272e5
19 changed files with 241 additions and 51 deletions

View File

@@ -11,6 +11,7 @@ import InfoDetails from './components/InfoDetails/InfoDetails';
import IPCContainer from './containers/IPCContainer/IPCContainer';
import Loading from './components/UI/Loading/Loading';
import MountItems from './containers/MountItems/MountItems';
import NewReleases from './components/NewReleases/NewReleases';
import {notifyError} from './redux/actions/error_actions';
import Reboot from './components/Reboot/Reboot';
import ReleaseVersionDisplay from './components/ReleaseVersionDisplay/ReleaseVersionDisplay';
@@ -111,12 +112,26 @@ class App extends IPCContainer {
!this.props.DismissDependencies &&
this.props.AllowMount;
const showNewReleases = !showConfig &&
!this.props.DisplayConfirmYesNo &&
!showDependencies &&
!this.props.DownloadActive &&
!this.props.DisplayError &&
!this.props.DisplayInfo &&
!this.props.InstallActive &&
!this.props.RebootRequired &&
!this.props.DisplaySelectAppPlatform &&
!showUpgrade &&
!this.props.DismissNewReleasesAvailable &&
(this.props.NewReleasesAvailable.length > 0);
const configDisplay = createModalConditionally(showConfig, <Configuration version={selectedVersion} remoteSupported={remoteSupported} />);
const confirmDisplay = createModalConditionally(this.props.DisplayConfirmYesNo, <YesNo/>);
const dependencyDisplay = createModalConditionally(showDependencies, <DependencyList/>);
const downloadDisplay = createModalConditionally(this.props.DownloadActive, <DownloadProgress/>);
const downloadDisplay = createModalConditionally(this.props.DownloadActive, <DownloadProgress />, false, true);
const errorDisplay = createModalConditionally(this.props.DisplayError, <ErrorDetails/>, true);
const infoDisplay = createModalConditionally(this.props.DisplayInfo, <InfoDetails/>, true);
const newReleasesDisplay = createModalConditionally(showNewReleases, <NewReleases/>);
const rebootDisplay = createModalConditionally(this.props.RebootRequired, <Reboot />);
const selectAppPlatformDisplay = createModalConditionally(this.props.DisplaySelectAppPlatform, <SelectAppPlatform/>);
const upgradeDisplay = createModalConditionally(showUpgrade, <UpgradeUI/>);
@@ -175,6 +190,7 @@ class App extends IPCContainer {
{mainContent}
</div>
</div>
{newReleasesDisplay}
{selectAppPlatformDisplay}
{dependencyDisplay}
{upgradeDisplay}
@@ -201,12 +217,14 @@ const mapStateToProps = state => {
DisplayError: state.error.DisplayError,
DisplayInfo: state.error.DisplayInfo,
DisplaySelectAppPlatform: state.common.DisplaySelectAppPlatform,
DismissNewReleasesAvailable: state.relver.DismissNewReleasesAvailable,
DownloadActive: state.download.DownloadActive,
InstallActive: state.install.InstallActive,
InstalledVersion: state.relver.InstalledVersion,
LocationsLookup: state.relver.LocationsLookup,
MissingDependencies: state.install.MissingDependencies,
MountsBusy: state.mounts.MountsBusy,
NewReleasesAvailable: state.relver.NewReleasesAvailable,
Platform: state.common.Platform,
ProviderState: state.mounts.ProviderState,
RebootRequired: state.common.RebootRequired,

View File

@@ -22,9 +22,9 @@ export default connect(mapStateToProps)(props => {
</td>
<td>
{props.AllowDownload ?
<a href={void(0)}
className={'DependencyLink'}
onClick={()=>{props.onDownload(); return false;}}><u>Install</u></a> :
<a href={'#'}
className={'DependencyLink'}
onClick={()=>{props.onDownload(); return false;}}><u>Install</u></a> :
'Installing...'}
</td>
</tr>
@@ -32,4 +32,4 @@ export default connect(mapStateToProps)(props => {
</table>
</div>
);
});
});

View File

@@ -0,0 +1,28 @@
import React from 'react';
import * as Constants from '../../../constants';
import Button from '../../UI/Button/Button';
export default ({release}) => {
return (
<div>
<h3>{'[' + Constants.RELEASE_TYPES[release.Release].toUpperCase() + '] ' + release.Display }</h3>
<table cellSpacing={0} cellPadding={0} width="97%">
<tbody>
<tr style={{height: '4px'}}/>
<tr>
<td width="50%">
<Button buttonStyles={{width: '100%'}}>Changes</Button>
</td>
<td>
<div style={{width: 'var(--default_spacing)'}}/>
</td>
<td width="50%">
<Button buttonStyles={{width: '100%'}}>Install</Button>
</td>
</tr>
<tr style={{height: 'var(--default_spacing)'}}/>
</tbody>
</table>
</div>
);
};

View File

@@ -0,0 +1,11 @@
.NewReleasesHeading {
text-align: center;
margin-bottom: 4px;
}
.NewReleasesContent {
max-height: 60vh;
min-width: 50vw;
overflow-y: auto;
margin-bottom: var(--default_spacing);
}

View File

@@ -0,0 +1,35 @@
import React from 'react';
import {connect} from 'react-redux';
import Box from '../UI/Box/Box';
import Button from '../UI/Button/Button';
import NewRelease from './NewRelease/NewRelease';
import './NewReleases.css';
import {setDismissNewReleasesAvailable} from '../../redux/actions/release_version_actions';
const mapStateToProps = state => {
return {
NewReleasesAvailable: state.relver.NewReleasesAvailable,
};
};
const mapDispatchToProps = dispatch => {
return {
dismissNewReleasesAvailable: () => dispatch(setDismissNewReleasesAvailable(true)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(props => {
const newReleases = props.NewReleasesAvailable.map(i => {
return <NewRelease key={'new_release_' + i.Release + '_' + i.Version} release={i} />;
});
return (
<Box dxDark dxStyle={{padding: 'var(--default_spacing)'}}>
<h1 className={'NewReleasesHeading'}>New Repertory Versions Available</h1>
<div className={'NewReleasesContent'}>
{newReleases}
</div>
<Button clicked={props.dismissNewReleasesAvailable}>Dismiss</Button>
</Box>
);
});

View File

@@ -4,6 +4,7 @@ import './Button.css';
export default props => {
return (
<button disabled={props.disabled}
autoFocus={props.autoFocus}
className={'Button'}
style={props.buttonStyles}
onClick={props.clicked}>{props.children}</button>

View File

@@ -6,6 +6,7 @@ export default props => {
<div className={'CheckBoxOwner'}>
<label className='CheckBoxLabel'>{props.label}
<input checked={JSON.parse(props.checked)}
autoFocus={props.autoFocus}
disabled={props.disabled}
onChange={props.changed}
type='checkbox'/>
@@ -13,4 +14,4 @@ export default props => {
</label>
</div>
);
};
};

View File

@@ -11,6 +11,7 @@ export default props => {
return (
<div className={'DropDown'}>
<select className={'DropDownSelect' + (props.auto ? ' Auto ' : '') + (props.alt ? ' Alt ' : '') }
autoFocus={props.autoFocus}
disabled={props.disabled}
onChange={props.changed}
value={props.selected}>
@@ -19,4 +20,4 @@ export default props => {
</div>
);
};
};

View File

@@ -1,5 +1,7 @@
import React from 'react';
import './Modal.css'
import FocusTrap from 'focus-trap-react';
export default props => {
let modalStyles = [];
@@ -12,11 +14,14 @@ export default props => {
}
return (
<div
className={modalStyles.join(' ')}
onClick={props.clicked}>
<div className={contentStyles.join(' ')}>
{props.children}
<FocusTrap active={!props.disableFocusTrap}>
<div
className={modalStyles.join(' ')}
onClick={props.clicked}>
<div className={contentStyles.join(' ')}>
{props.children}
</div>
</div>
</div>);
};
</FocusTrap>
);
};

View File

@@ -163,6 +163,8 @@ class Configuration extends IPCContainer {
ObjectLookup: objectLookup,
OriginalItemList: itemListCopy,
OriginalObjectLookup: objectLookupCopy,
}, () => {
});
} else {
this.props.notifyError(arg.data.Error);
@@ -262,20 +264,7 @@ class Configuration extends IPCContainer {
);
}
const configurationItems = this.state.ItemList
.map((k, i) => {
return (
((!this.state.IsRemoteMount || !k.hide_remote) && (!k.advanced || (this.state.ShowAdvanced && k.advanced))) ?
<ConfigurationItem advanced={k.advanced}
changed={e=>this.handleItemChanged(e, i)}
grouping={'Settings'}
items={this.state.Template[k.label].items}
key={i}
label={k.label}
template={this.state.Template[k.label]}
value={k.value}/> :
null)
});
let autoFocus = true;
let objectItems = [];
for (const key of Object.keys(this.state.ObjectLookup)) {
@@ -285,9 +274,12 @@ class Configuration extends IPCContainer {
<div>
{
this.state.ObjectLookup[key].map((k, i) => {
const shouldFocus = autoFocus;
autoFocus = false;
return (
(!k.advanced || (this.state.ShowAdvanced && k.advanced && !k.remote) || this.showRemoteConfigItem(k, this.state.ObjectLookup[key])) ?
<ConfigurationItem advanced={k.advanced}
autoFocus={shouldFocus}
changed={e=>this.handleObjectItemChanged(e, key, i)}
grouping={key}
items={this.state.Template[key].template[k.label].items}
@@ -304,13 +296,33 @@ class Configuration extends IPCContainer {
));
}
const configurationItems = this.state.ItemList
.map((k, i) => {
const shouldFocus = autoFocus;
autoFocus = false;
return (
((!this.state.IsRemoteMount || !k.hide_remote) && (!k.advanced || (this.state.ShowAdvanced && k.advanced))) ?
<ConfigurationItem advanced={k.advanced}
autoFocus={shouldFocus}
changed={e=>this.handleItemChanged(e, i)}
grouping={'Settings'}
items={this.state.Template[k.label].items}
key={i}
label={k.label}
template={this.state.Template[k.label]}
value={k.value}/> :
null
);
});
return (
<div className={'Configuration'}>
{confirmSave}
<Box dxDark dxStyle={{padding: 'var(--default_spacing)'}}>
<div style={{float: 'right', margin: 0, padding: 0, marginTop: '-4px', boxSizing: 'border-box', display: 'block'}}>
<b style={{cursor: 'pointer'}}
onClick={this.checkSaveRequired}>X</b>
<a href={'#'}
onClick={this.checkSaveRequired}
style={{cursor: 'pointer'}}>X</a>
</div>
<h1 style={{width: '100%', textAlign: 'center'}}>{(
this.props.DisplayRemoteConfiguration ?

View File

@@ -18,7 +18,7 @@ export default connect(null, mapDispatchToProps)(props => {
const handleChanged = (e) => {
const target = e.target;
if (target.type === 'checkbox') {
target.value = e.target.checked ? "true" : "false";
target.value = e.target.checked ? 'true' : 'false';
}
props.changed(target);
};
@@ -30,49 +30,54 @@ export default connect(null, mapDispatchToProps)(props => {
props.notifyInfo(props.label, description);
};
infoDisplay = <a href={void(0)}
className={'ConfigurationInfo'}
onClick={()=>{displayInfo(); return false;}}><FontAwesomeIcon icon={faInfoCircle}/></a>;
infoDisplay = <a href={'#'}
className={'ConfigurationInfo'}
onClick={()=>{displayInfo(); return false;}}><FontAwesomeIcon icon={faInfoCircle}/></a>;
}
let data;
switch (props.template.type) {
case "bool":
case 'bool':
data = <CheckBox changed={handleChanged}
checked={props.value}
disabled={props.readOnly}/>;
disabled={props.readOnly}
autoFocus={props.autoFocus}/>;
break;
case "double":
case 'double':
data = <input min={0.0}
autoFocus={props.autoFocus}
disabled={props.readOnly}
onChange={e=>handleChanged(e)}
step={"0.01"}
step={'0.01'}
className={'ConfigurationItemInput'}
type={'number'}
value={parseFloat(props.value).toFixed(2)}/>;
break;
case "list":
case 'list':
data = <DropDown alt
auto
autoFocus={props.autoFocus}
changed={handleChanged}
disabled={props.readOnly}
items={props.items}
selected={props.value} />;
break;
case "string":
case 'string':
data = <input onChange={e=>handleChanged(e)}
autoFocus={props.autoFocus}
className={'ConfigurationItemInput'}
disabled={props.readOnly}
type={'text'}
value={props.value}/>;
break;
case "uint8":
case 'uint8':
data = <input max={255}
min={0}
autoFocus={props.autoFocus}
disabled={props.readOnly}
onChange={e=>handleChanged(e)}
className={'ConfigurationItemInput'}
@@ -80,9 +85,10 @@ export default connect(null, mapDispatchToProps)(props => {
value={props.value}/>;
break;
case "uint16":
case 'uint16':
data = <input max={65535}
min={0}
autoFocus={props.autoFocus}
disabled={props.readOnly}
onChange={e=>handleChanged(e)}
className={'ConfigurationItemInput'}
@@ -90,9 +96,10 @@ export default connect(null, mapDispatchToProps)(props => {
value={props.value}/>;
break;
case "uint32":
case 'uint32':
data = <input max={4294967295}
min={0}
autoFocus={props.autoFocus}
disabled={props.readOnly}
onChange={e=>handleChanged(e)}
className={'ConfigurationItemInput'}
@@ -100,9 +107,10 @@ export default connect(null, mapDispatchToProps)(props => {
value={props.value}/>;
break;
case "uint64":
case 'uint64':
data = <input max={18446744073709551615}
min={0}
autoFocus={props.autoFocus}
disabled={props.readOnly}
onChange={e=>handleChanged(e)}
className={'ConfigurationItemInput'}
@@ -129,4 +137,4 @@ export default connect(null, mapDispatchToProps)(props => {
</table>
</div>
);
});
});

View File

@@ -152,8 +152,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(props => {
}
};
removeControl = (
<a col={dimensions=>dimensions.columns - 6}
href={void(0)}
<a href={'#'}
col={dimensions=>dimensions.columns - 6}
onClick={handleRemoveMount}
row={secondRow + 3}
style={removeStyle}>

View File

@@ -30,6 +30,9 @@
a {
outline: 0;
color: var(--text_color);
text-decoration: none;
font-weight: bold;
}
html, body {

View File

@@ -13,7 +13,10 @@ import {
setDismissDependencies
} from './install_actions';
import {unmountAll} from './mount_actions';
import {getIPCRenderer} from '../../utils';
import {
getIPCRenderer,
getNewReleases
} from '../../utils';
export const CLEAR_UI_UPGRADE = 'relver/clearUIUpgrade';
export const clearUIUpgrade = () => {
@@ -123,11 +126,22 @@ export const loadReleases = () => {
...response.data.Locations[appPlatform],
};
const storedReleases = localStorage.getItem('releases');
let newReleases = [];
if (storedReleases && (storedReleases.length > 0)) {
newReleases = getNewReleases(JSON.parse(storedReleases).VersionLookup, versionLookup);
}
localStorage.setItem('releases', JSON.stringify({
LocationsLookup: locationsLookup,
VersionLookup: versionLookup
}));
dispatchActions(locationsLookup, versionLookup);
dispatch(setNewReleasesAvailable(newReleases));
if (getState().relver.NewReleasesAvailable.length > 0) {
dispatch(showWindow());
}
}).catch(error => {
const releases = localStorage.getItem('releases');
if (releases && (releases.length > 0)) {
@@ -174,8 +188,10 @@ export const setActiveRelease = (release, version) => {
};
export const setAllowDismissDependencies = createAction('relver/setAllowDismissDependencies');
export const setDismissNewReleasesAvailable = createAction('relver/setDismissNewReleasesAvailable');
export const setDismissUIUpgrade = createAction('relver/setDismissUIUpgrade');
export const setInstalledVersion = createAction('relver/setInstalledVersion');
export const setNewReleasesAvailable = createAction('relver/setNewReleasesAvailable');
export const SET_RELEASE_DATA = 'relver/setReleaseData';
export const setReleaseData = (locationsLookup, versionLookup)=> {

View File

@@ -15,8 +15,10 @@ const versionLookup = Constants.RELEASE_TYPES.map(k=> {
export const releaseVersionReducer = createReducer({
AllowDismissDependencies: false,
DismissNewReleasesAvailable: true,
InstalledVersion: 'none',
LocationsLookup: {},
NewReleasesAvailable: [],
Release: 0,
ReleaseDefault: 0,
ReleaseUpgradeAvailable: false,
@@ -49,6 +51,12 @@ export const releaseVersionReducer = createReducer({
AllowDismissDependencies: action.payload,
};
},
[Actions.setDismissNewReleasesAvailable]: (state, action) => {
return {
...state,
DismissNewReleasesAvailable: action.payload,
};
},
[Actions.setDismissUIUpgrade]: (state, action) => {
return {
...state,
@@ -61,6 +69,13 @@ export const releaseVersionReducer = createReducer({
InstalledVersion: action.payload,
}
},
[Actions.setNewReleasesAvailable]: (state, action) => {
return {
...state,
DismissNewReleasesAvailable: false,
NewReleasesAvailable: action.payload,
};
},
[Actions.SET_RELEASE_DATA]: (state, action) => {
return {
...state,

View File

@@ -6,8 +6,8 @@ const ipcRenderer = (!process.versions.hasOwnProperty('electron') && window && w
window.require('electron').ipcRenderer :
null;
export const createModalConditionally = (condition, jsx, critical) => {
const modalProps = {critical: critical};
export const createModalConditionally = (condition, jsx, critical, disableFocusTrap) => {
const modalProps = {critical: critical, disableFocusTrap: disableFocusTrap};
return condition ? (<Modal {...modalProps}>{jsx}</Modal>) : null;
};
@@ -20,6 +20,37 @@ export const getIPCRenderer = () => {
return ipcRenderer;
};
export const getNewReleases = (existingReleases, newReleases) => {
const ret = [];
existingReleases = Constants.RELEASE_TYPES.reduce((map, release) => {
map[release] = [];
return map;
}, {});
if (existingReleases && newReleases) {
Constants.RELEASE_TYPES.forEach(release => {
newReleases[release]
.filter(version => !existingReleases[release].includes(version) && (version !== 'unavailable'))
.forEach(version => {
ret.splice(0, 0, {
Display: version,
Release: Constants.RELEASE_TYPES.indexOf(release),
Version: newReleases[release].indexOf(version),
});
});
});
}
ret.splice(0, 0, {
Display: '1.1.1',
Release: 0,
Version: 2,
});
return ret;
};
export const getSelectedVersionFromState = state => {
return (state.relver.Version === -1) ?
'unavailable' :