Merge pull request #1934 from davemfish/feature/WB-1793-settings-layout
convert settings modal to a dropdown menu, etc
This commit is contained in:
commit
a08d9b555f
|
@ -53,6 +53,18 @@ export default class InvestJob {
|
|||
return InvestJob.getJobStore();
|
||||
}
|
||||
|
||||
static async deleteJob(hash) {
|
||||
await investJobStore.removeItem(hash);
|
||||
// also remove item from the array that tracks the order of the jobs
|
||||
const sortedJobHashes = await investJobStore.getItem(HASH_ARRAY_KEY);
|
||||
const idx = sortedJobHashes.indexOf(hash);
|
||||
if (idx > -1) {
|
||||
sortedJobHashes.splice(idx, 1); // remove one item only
|
||||
}
|
||||
await investJobStore.setItem(HASH_ARRAY_KEY, sortedJobHashes);
|
||||
return InvestJob.getJobStore();
|
||||
}
|
||||
|
||||
static async saveJob(job) {
|
||||
job.hash = window.crypto.getRandomValues(
|
||||
new Uint32Array(1)
|
||||
|
|
|
@ -19,10 +19,12 @@ import { AiOutlineTrademarkCircle } from 'react-icons/ai';
|
|||
|
||||
import HomeTab from './components/HomeTab';
|
||||
import InvestTab from './components/InvestTab';
|
||||
import AppMenu from './components/AppMenu';
|
||||
import SettingsModal from './components/SettingsModal';
|
||||
import DataDownloadModal from './components/DataDownloadModal';
|
||||
import DownloadProgressBar from './components/DownloadProgressBar';
|
||||
import PluginModal from './components/PluginModal';
|
||||
import MetadataModal from './components/MetadataModal';
|
||||
import InvestJob from './InvestJob';
|
||||
import { dragOverHandlerNone } from './utils';
|
||||
import { ipcMainChannels } from '../main/ipcMainChannels';
|
||||
|
@ -45,8 +47,11 @@ export default class App extends React.Component {
|
|||
investList: null,
|
||||
recentJobs: [],
|
||||
showDownloadModal: false,
|
||||
showPluginModal: false,
|
||||
downloadedNofN: null,
|
||||
showChangelog: false,
|
||||
showSettingsModal: false,
|
||||
showMetadataModal: false,
|
||||
changelogDismissed: false,
|
||||
};
|
||||
this.switchTabs = this.switchTabs.bind(this);
|
||||
|
@ -54,8 +59,12 @@ export default class App extends React.Component {
|
|||
this.closeInvestModel = this.closeInvestModel.bind(this);
|
||||
this.updateJobProperties = this.updateJobProperties.bind(this);
|
||||
this.saveJob = this.saveJob.bind(this);
|
||||
this.deleteJob = this.deleteJob.bind(this);
|
||||
this.clearRecentJobs = this.clearRecentJobs.bind(this);
|
||||
this.showDownloadModal = this.showDownloadModal.bind(this);
|
||||
this.toggleDownloadModal = this.toggleDownloadModal.bind(this);
|
||||
this.toggleSettingsModal = this.toggleSettingsModal.bind(this);
|
||||
this.toggleMetadataModal = this.toggleMetadataModal.bind(this);
|
||||
this.togglePluginModal = this.togglePluginModal.bind(this);
|
||||
this.updateInvestList = this.updateInvestList.bind(this);
|
||||
}
|
||||
|
||||
|
@ -86,8 +95,8 @@ export default class App extends React.Component {
|
|||
ipcRenderer.removeAllListeners('download-status');
|
||||
}
|
||||
|
||||
/** Change the tab that is currently visible.
|
||||
*
|
||||
/**
|
||||
* Change the tab that is currently visible.
|
||||
* @param {string} key - the value of one of the Nav.Link eventKey.
|
||||
*/
|
||||
switchTabs(key) {
|
||||
|
@ -96,7 +105,7 @@ export default class App extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
showDownloadModal(shouldShow) {
|
||||
toggleDownloadModal(shouldShow) {
|
||||
this.setState({
|
||||
showDownloadModal: shouldShow,
|
||||
});
|
||||
|
@ -116,8 +125,26 @@ export default class App extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
/** Push data for a new InvestTab component to an array.
|
||||
*
|
||||
togglePluginModal(show) {
|
||||
this.setState({
|
||||
showPluginModal: show
|
||||
});
|
||||
}
|
||||
|
||||
toggleMetadataModal(show) {
|
||||
this.setState({
|
||||
showMetadataModal: show
|
||||
});
|
||||
}
|
||||
|
||||
toggleSettingsModal(show) {
|
||||
this.setState({
|
||||
showSettingsModal: show
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Push data for a new InvestTab component to an array.
|
||||
* @param {InvestJob} job - as constructed by new InvestJob()
|
||||
*/
|
||||
openInvestModel(job) {
|
||||
|
@ -135,7 +162,6 @@ export default class App extends React.Component {
|
|||
|
||||
/**
|
||||
* Click handler for the close-tab button on an Invest model tab.
|
||||
*
|
||||
* @param {string} tabID - the eventKey of the tab containing the
|
||||
* InvestTab component that will be removed.
|
||||
*/
|
||||
|
@ -163,8 +189,8 @@ export default class App extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
/** Update properties of an open InvestTab.
|
||||
*
|
||||
/**
|
||||
* Update properties of an open InvestTab.
|
||||
* @param {string} tabID - the unique identifier of an open tab
|
||||
* @param {obj} jobObj - key-value pairs of any job properties to be updated
|
||||
*/
|
||||
|
@ -176,10 +202,8 @@ export default class App extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
/** Save data describing an invest job to a persistent store.
|
||||
*
|
||||
* And update the app's view of that store.
|
||||
*
|
||||
/**
|
||||
* Save data describing an invest job to a persistent store.
|
||||
* @param {string} tabID - the unique identifier of an open InvestTab.
|
||||
*/
|
||||
async saveJob(tabID) {
|
||||
|
@ -190,6 +214,20 @@ export default class App extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the job record from the store.
|
||||
* @param {string} jobHash - the unique identifier of a saved Job.
|
||||
*/
|
||||
async deleteJob(jobHash) {
|
||||
const recentJobs = await InvestJob.deleteJob(jobHash);
|
||||
this.setState({
|
||||
recentJobs: recentJobs,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the jobs from the store.
|
||||
*/
|
||||
async clearRecentJobs() {
|
||||
const recentJobs = await InvestJob.clearStore();
|
||||
this.setState({
|
||||
|
@ -221,7 +259,10 @@ export default class App extends React.Component {
|
|||
openTabIDs,
|
||||
activeTab,
|
||||
showDownloadModal,
|
||||
showPluginModal,
|
||||
showChangelog,
|
||||
showSettingsModal,
|
||||
showMetadataModal,
|
||||
downloadedNofN,
|
||||
} = this.state;
|
||||
|
||||
|
@ -325,17 +366,38 @@ export default class App extends React.Component {
|
|||
{showDownloadModal && (
|
||||
<DataDownloadModal
|
||||
show={showDownloadModal}
|
||||
closeModal={() => this.showDownloadModal(false)}
|
||||
closeModal={() => this.toggleDownloadModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showPluginModal && (
|
||||
<PluginModal
|
||||
show={showPluginModal}
|
||||
closeModal={() => this.togglePluginModal(false)}
|
||||
openModal={() => this.togglePluginModal(true)}
|
||||
updateInvestList={this.updateInvestList}
|
||||
closeInvestModel={this.closeInvestModel}
|
||||
openJobs={openJobs}
|
||||
/>
|
||||
)}
|
||||
{showChangelog && (
|
||||
<Changelog
|
||||
show={showChangelog}
|
||||
close={() => this.closeChangelogModal()}
|
||||
/>
|
||||
)}
|
||||
{showMetadataModal && (
|
||||
<MetadataModal
|
||||
show={showMetadataModal}
|
||||
close={() => this.toggleMetadataModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showSettingsModal && (
|
||||
<SettingsModal
|
||||
show={showSettingsModal}
|
||||
close={() => this.toggleSettingsModal(false)}
|
||||
nCPU={this.props.nCPU}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
showChangelog && (
|
||||
<Changelog
|
||||
show={showChangelog}
|
||||
close={() => this.closeChangelogModal()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<TabContainer activeKey={activeTab}>
|
||||
<Navbar
|
||||
onDragOver={dragOverHandlerNone}
|
||||
|
@ -377,16 +439,12 @@ export default class App extends React.Component {
|
|||
)
|
||||
: <div />
|
||||
}
|
||||
<PluginModal
|
||||
updateInvestList={this.updateInvestList}
|
||||
closeInvestModel={this.closeInvestModel}
|
||||
openJobs={openJobs}
|
||||
/>
|
||||
<SettingsModal
|
||||
className="mx-3"
|
||||
clearJobsStorage={this.clearRecentJobs}
|
||||
showDownloadModal={() => this.showDownloadModal(true)}
|
||||
nCPU={this.props.nCPU}
|
||||
<AppMenu
|
||||
openDownloadModal={() => this.toggleDownloadModal(true)}
|
||||
openPluginModal={() => this.togglePluginModal(true)}
|
||||
openChangelogModal={() => this.setState({ showChangelog: true })}
|
||||
openSettingsModal={() => this.toggleSettingsModal(true)}
|
||||
openMetadataModal={() => this.toggleMetadataModal(true)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -407,6 +465,8 @@ export default class App extends React.Component {
|
|||
openInvestModel={this.openInvestModel}
|
||||
recentJobs={recentJobs}
|
||||
batchUpdateArgs={this.batchUpdateArgs}
|
||||
deleteJob={this.deleteJob}
|
||||
clearRecentJobs={this.clearRecentJobs}
|
||||
/>
|
||||
) : <div />}
|
||||
</TabPane>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
|
||||
import Dropdown from 'react-bootstrap/Dropdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GiHamburgerMenu } from 'react-icons/gi';
|
||||
|
||||
export default function AppMenu(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="app-menu-button"
|
||||
aria-label="menu"
|
||||
childBsPrefix="outline-secondary"
|
||||
>
|
||||
<GiHamburgerMenu />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu
|
||||
align="right"
|
||||
className="shadow"
|
||||
>
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
onClick={props.openPluginModal}
|
||||
>
|
||||
Manage Plugins
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
onClick={props.openDownloadModal}
|
||||
>
|
||||
Download Sample Data
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
onClick={props.openMetadataModal}
|
||||
>
|
||||
Configure Metadata
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
onClick={props.openChangelogModal}
|
||||
>
|
||||
View Changelog
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
as="button"
|
||||
onClick={props.openSettingsModal}
|
||||
>
|
||||
Settings
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
|
@ -88,8 +88,7 @@ export default function Changelog(props) {
|
|||
and not, for example, sourced from user input. */}
|
||||
<Modal.Body
|
||||
dangerouslySetInnerHTML={htmlContent}
|
||||
>
|
||||
</Modal.Body>
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import Alert from 'react-bootstrap/Alert';
|
|||
import Table from 'react-bootstrap/Table';
|
||||
import {
|
||||
MdErrorOutline,
|
||||
MdClose,
|
||||
} from 'react-icons/md';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -19,7 +20,7 @@ const { logger } = window.Workbench;
|
|||
|
||||
// A URL for sampledata to use in devMode, when the token containing the URL
|
||||
// associated with a production build of the Workbench does not exist.
|
||||
const BASE_URL = 'https://storage.googleapis.com/releases.naturalcapitalproject.org/invest/3.13.0/data';
|
||||
const BASE_URL = 'https://storage.googleapis.com/releases.naturalcapitalproject.org/invest/3.15.1/data';
|
||||
const DEFAULT_FILESIZE = 0;
|
||||
|
||||
/** Render a dialog with a form for configuring global invest settings */
|
||||
|
@ -263,10 +264,20 @@ class DataDownloadModal extends React.Component {
|
|||
<p className="mb-0"><em>{this.state.alertPath}</em></p>
|
||||
</Alert>
|
||||
)
|
||||
: <Modal.Title id="download-modal-title">
|
||||
: (
|
||||
<Modal.Title id="download-modal-title">
|
||||
{t("Download InVEST sample data")}
|
||||
</Modal.Title>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
variant="secondary-outline"
|
||||
onClick={this.closeDialog}
|
||||
className="float-right"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<MdClose />
|
||||
</Button>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Table
|
||||
|
@ -294,12 +305,6 @@ class DataDownloadModal extends React.Component {
|
|||
</Table>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={this.closeDialog}
|
||||
>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={this.handleSubmit}
|
||||
|
|
|
@ -7,7 +7,11 @@ import Card from 'react-bootstrap/Card';
|
|||
import Container from 'react-bootstrap/Container';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
MdClose,
|
||||
} from 'react-icons/md';
|
||||
|
||||
import OpenButton from '../OpenButton';
|
||||
import InvestJob from '../../InvestJob';
|
||||
|
@ -35,7 +39,13 @@ export default class HomeTab extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { recentJobs, investList, openInvestModel } = this.props;
|
||||
const {
|
||||
recentJobs,
|
||||
investList,
|
||||
openInvestModel,
|
||||
deleteJob,
|
||||
clearRecentJobs
|
||||
} = this.props;
|
||||
let sortedModelIds = {};
|
||||
if (investList) {
|
||||
// sort the model list alphabetically, by the model title,
|
||||
|
@ -74,9 +84,10 @@ export default class HomeTab extends React.Component {
|
|||
name={modelTitle}
|
||||
action
|
||||
onClick={() => this.handleClick(modelID)}
|
||||
className="invest-button"
|
||||
>
|
||||
{ badge }
|
||||
<span className="invest-button">{modelTitle}</span>
|
||||
<span>{modelTitle}</span>
|
||||
</ListGroup.Item>
|
||||
);
|
||||
});
|
||||
|
@ -86,6 +97,16 @@ export default class HomeTab extends React.Component {
|
|||
<Col md={6} className="invest-list-container">
|
||||
<ListGroup className="invest-list-group">
|
||||
{investButtons}
|
||||
<ListGroup.Item
|
||||
key="browse"
|
||||
className="py-2 border-0"
|
||||
>
|
||||
<OpenButton
|
||||
className="w-100 border-1 py-2 pl-3 text-left text-truncate"
|
||||
openInvestModel={openInvestModel}
|
||||
investList={investList}
|
||||
/>
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
</Col>
|
||||
<Col className="recent-job-card-col">
|
||||
|
@ -93,6 +114,8 @@ export default class HomeTab extends React.Component {
|
|||
openInvestModel={openInvestModel}
|
||||
recentJobs={recentJobs}
|
||||
investList={investList}
|
||||
deleteJob={deleteJob}
|
||||
clearRecentJobs={clearRecentJobs}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -117,13 +140,20 @@ HomeTab.propTypes = {
|
|||
status: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
deleteJob: PropTypes.func.isRequired,
|
||||
clearRecentJobs: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a button for each recent invest job.
|
||||
*/
|
||||
function RecentInvestJobs(props) {
|
||||
const { recentJobs, openInvestModel, investList } = props;
|
||||
const {
|
||||
recentJobs,
|
||||
openInvestModel,
|
||||
deleteJob,
|
||||
clearRecentJobs
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = (jobMetadata) => {
|
||||
|
@ -143,16 +173,26 @@ function RecentInvestJobs(props) {
|
|||
}
|
||||
recentButtons.push(
|
||||
<Card
|
||||
className="text-left recent-job-card"
|
||||
as="button"
|
||||
className="text-left recent-job-card mr-2 w-100"
|
||||
key={job.hash}
|
||||
onClick={() => handleClick(job)}
|
||||
>
|
||||
<Card.Body>
|
||||
<Card.Header>
|
||||
{badge}
|
||||
<span className="header-title">{job.modelTitle}</span>
|
||||
</Card.Header>
|
||||
<Card.Header>
|
||||
{badge}
|
||||
<span className="header-title">{job.modelTitle}</span>
|
||||
<Button
|
||||
variant="outline-light"
|
||||
onClick={() => deleteJob(job.hash)}
|
||||
className="float-right p-1 mr-1 border-0"
|
||||
aria-label="delete"
|
||||
>
|
||||
<MdClose />
|
||||
</Button>
|
||||
</Card.Header>
|
||||
<Card.Body
|
||||
className="text-left border-0"
|
||||
as="button"
|
||||
onClick={() => handleClick(job)}
|
||||
>
|
||||
<Card.Title>
|
||||
<span className="text-heading">{'Workspace: '}</span>
|
||||
<span className="text-mono">{job.argsValues.workspace_dir}</span>
|
||||
|
@ -177,37 +217,43 @@ function RecentInvestJobs(props) {
|
|||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className="recent-header-col">
|
||||
{recentButtons.length
|
||||
? (
|
||||
<h4>
|
||||
{t('Recent runs:')}
|
||||
</h4>
|
||||
)
|
||||
: (
|
||||
<div className="default-text">
|
||||
{t("Set up a model from a sample datastack file (.json) " +
|
||||
"or from an InVEST model's logfile (.txt): ")}
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col className="open-button-col">
|
||||
{investList
|
||||
? (
|
||||
<OpenButton
|
||||
className="mr-2"
|
||||
openInvestModel={openInvestModel}
|
||||
investList={investList}
|
||||
/>
|
||||
) : ''}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
{recentButtons}
|
||||
</>
|
||||
<Container>
|
||||
<Row>
|
||||
{recentButtons.length
|
||||
? <div />
|
||||
: (
|
||||
<Card
|
||||
className="text-left recent-job-card mr-2 w-100"
|
||||
key="placeholder"
|
||||
>
|
||||
<Card.Header>
|
||||
<span className="header-title">{t('Welcome!')}</span>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Card.Title>
|
||||
<span className="text-heading">
|
||||
{t('After running a model, find your recent model runs here.')}
|
||||
</span>
|
||||
</Card.Title>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)}
|
||||
{recentButtons}
|
||||
</Row>
|
||||
{recentButtons.length
|
||||
? (
|
||||
<Row>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={clearRecentJobs}
|
||||
className="mr-2 w-100"
|
||||
>
|
||||
{t('Clear all model runs')}
|
||||
</Button>
|
||||
</Row>
|
||||
)
|
||||
: <div />}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -222,4 +268,6 @@ RecentInvestJobs.propTypes = {
|
|||
})
|
||||
).isRequired,
|
||||
openInvestModel: PropTypes.func.isRequired,
|
||||
deleteJob: PropTypes.func.isRequired,
|
||||
clearRecentJobs: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -6,13 +6,15 @@ import Col from 'react-bootstrap/Col';
|
|||
import Alert from 'react-bootstrap/Alert';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import { MdClose } from 'react-icons/md';
|
||||
|
||||
import {
|
||||
getGeoMetaMakerProfile,
|
||||
setGeoMetaMakerProfile,
|
||||
} from '../../../server_requests';
|
||||
} from '../../server_requests';
|
||||
|
||||
import { openLinkInBrowser } from '../../../utils';
|
||||
import { openLinkInBrowser } from '../../utils';
|
||||
|
||||
function AboutMetadataDiv() {
|
||||
const { t } = useTranslation();
|
||||
|
@ -67,9 +69,9 @@ function FormRow(label, value, handler) {
|
|||
}
|
||||
|
||||
/**
|
||||
* A form for submitting GeoMetaMaker profile data.
|
||||
* A Modal with form for submitting GeoMetaMaker profile data.
|
||||
*/
|
||||
export default function MetadataForm() {
|
||||
export default function MetadataModal(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [contactName, setContactName] = useState('');
|
||||
|
@ -80,7 +82,6 @@ export default function MetadataForm() {
|
|||
const [licenseURL, setLicenseURL] = useState('');
|
||||
const [alertMsg, setAlertMsg] = useState('');
|
||||
const [alertError, setAlertError] = useState(false);
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadProfile() {
|
||||
|
@ -127,65 +128,74 @@ export default function MetadataForm() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div id="metadata-form">
|
||||
{
|
||||
(showInfo)
|
||||
? <AboutMetadataDiv />
|
||||
: (
|
||||
<Form onSubmit={handleSubmit} onChange={handleChange}>
|
||||
<fieldset>
|
||||
<legend>{t('Contact Information')}</legend>
|
||||
<Form.Group controlId="name">
|
||||
{FormRow(t('Full name'), contactName, setContactName)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="email">
|
||||
{FormRow(t('Email address'), contactEmail, setContactEmail)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="job-title">
|
||||
{FormRow(t('Job title'), contactPosition, setContactPosition)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="organization">
|
||||
{FormRow(t('Organization name'), contactOrg, setContactOrg)}
|
||||
</Form.Group>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{t('Data License Information')}</legend>
|
||||
<Form.Group controlId="license-title">
|
||||
{FormRow(t('Title'), licenseTitle, setLicenseTitle)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="license-url">
|
||||
{FormRow('URL', licenseURL, setLicenseURL)}
|
||||
</Form.Group>
|
||||
</fieldset>
|
||||
<Form.Row>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="my-1 py2 mx-2"
|
||||
<Modal
|
||||
show={props.show}
|
||||
onHide={props.close}
|
||||
size="lg"
|
||||
aria-labelledby="metadata-modal-title"
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title id="metadata-modal-title">
|
||||
{t('Configure Metadata')}
|
||||
</Modal.Title>
|
||||
<Button
|
||||
variant="secondary-outline"
|
||||
onClick={props.close}
|
||||
className="float-right"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<MdClose />
|
||||
</Button>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<AboutMetadataDiv />
|
||||
<hr />
|
||||
<Form onSubmit={handleSubmit} onChange={handleChange}>
|
||||
<fieldset>
|
||||
<legend>{t('Contact Information')}</legend>
|
||||
<Form.Group controlId="name">
|
||||
{FormRow(t('Full name'), contactName, setContactName)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="email">
|
||||
{FormRow(t('Email address'), contactEmail, setContactEmail)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="job-title">
|
||||
{FormRow(t('Job title'), contactPosition, setContactPosition)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="organization">
|
||||
{FormRow(t('Organization name'), contactOrg, setContactOrg)}
|
||||
</Form.Group>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{t('Data License Information')}</legend>
|
||||
<Form.Group controlId="license-title">
|
||||
{FormRow(t('Title'), licenseTitle, setLicenseTitle)}
|
||||
</Form.Group>
|
||||
<Form.Group controlId="license-url">
|
||||
{FormRow('URL', licenseURL, setLicenseURL)}
|
||||
</Form.Group>
|
||||
</fieldset>
|
||||
<Form.Row>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="my-1 py2 mx-2"
|
||||
>
|
||||
{t('Save Metadata')}
|
||||
</Button>
|
||||
{
|
||||
(alertMsg) && (
|
||||
<Alert
|
||||
className="my-1 py-2"
|
||||
variant={alertError ? 'danger' : 'success'}
|
||||
>
|
||||
{t('Save Metadata')}
|
||||
</Button>
|
||||
{
|
||||
(alertMsg) && (
|
||||
<Alert
|
||||
className="my-1 py-2"
|
||||
variant={alertError ? 'danger' : 'success'}
|
||||
>
|
||||
{alertMsg}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
</Form.Row>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
className="my-1 py-2 mx-2 info-toggle"
|
||||
onClick={() => setShowInfo((prevState) => !prevState)}
|
||||
>
|
||||
{showInfo ? t('Hide Info') : t('More Info')}
|
||||
</Button>
|
||||
</div>
|
||||
{alertMsg}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
</Form.Row>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -49,19 +49,21 @@ class OpenButton extends React.Component {
|
|||
|
||||
render() {
|
||||
const { t, className } = this.props;
|
||||
const tipText = t('Browse to a datastack (.json) or InVEST logfile (.txt)');
|
||||
const tipText = t(
|
||||
`Open an InVEST model by loading input parameters from
|
||||
a .json, .tgz, or InVEST logfile (.txt)`);
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="left"
|
||||
placement="right"
|
||||
delay={{ show: 250, hide: 400 }}
|
||||
overlay={<Tooltip>{tipText}</Tooltip>}
|
||||
>
|
||||
<Button
|
||||
className={className}
|
||||
onClick={this.browseFile}
|
||||
variant="outline-dark"
|
||||
variant="outline-primary"
|
||||
>
|
||||
{t('Open')}
|
||||
{t('Browse to a datastack or InVEST logfile')}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
|
|
@ -7,14 +7,21 @@ import Form from 'react-bootstrap/Form';
|
|||
import Modal from 'react-bootstrap/Modal';
|
||||
import Spinner from 'react-bootstrap/Spinner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MdClose } from 'react-icons/md';
|
||||
|
||||
import { ipcMainChannels } from '../../../main/ipcMainChannels';
|
||||
|
||||
const { ipcRenderer } = window.Workbench.electron;
|
||||
|
||||
export default function PluginModal(props) {
|
||||
const { updateInvestList, closeInvestModel, openJobs } = props;
|
||||
const [showPluginModal, setShowPluginModal] = useState(false);
|
||||
const {
|
||||
updateInvestList,
|
||||
closeInvestModel,
|
||||
openJobs,
|
||||
show,
|
||||
closeModal,
|
||||
openModal,
|
||||
} = props;
|
||||
const [url, setURL] = useState('');
|
||||
const [revision, setRevision] = useState('');
|
||||
const [path, setPath] = useState('');
|
||||
|
@ -33,15 +40,7 @@ export default function PluginModal(props) {
|
|||
setRevision('');
|
||||
setInstallErr('');
|
||||
setUninstallErr('');
|
||||
setShowPluginModal(false);
|
||||
};
|
||||
const handleModalOpen = () => {
|
||||
if (window.Workbench.OS === 'win32') {
|
||||
ipcRenderer.invoke(ipcMainChannels.HAS_MSVC).then((hasMSVC) => {
|
||||
setNeedsMSVC(!hasMSVC);
|
||||
});
|
||||
}
|
||||
setShowPluginModal(true);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const addPlugin = () => {
|
||||
|
@ -84,11 +83,21 @@ export default function PluginModal(props) {
|
|||
};
|
||||
|
||||
const downloadMSVC = () => {
|
||||
setShowPluginModal(false);
|
||||
closeModal();
|
||||
ipcRenderer.invoke(ipcMainChannels.DOWNLOAD_MSVC).then(
|
||||
() => { handleModalOpen(); }
|
||||
openModal()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
if (window.Workbench.OS === 'win32') {
|
||||
ipcRenderer.invoke(ipcMainChannels.HAS_MSVC).then((hasMSVC) => {
|
||||
setNeedsMSVC(!hasMSVC);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer.invoke(ipcMainChannels.GET_SETTING, 'plugins').then(
|
||||
|
@ -286,22 +295,27 @@ export default function PluginModal(props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button onClick={handleModalOpen} variant="outline-dark" aria-label="plugins">
|
||||
{t('Manage plugins')}
|
||||
</Button>
|
||||
|
||||
<Modal show={showPluginModal} onHide={handleModalClose} contentClassName="plugin-modal">
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('Manage plugins')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
{modalBody}
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
<Modal show={show} onHide={handleModalClose} contentClassName="plugin-modal">
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('Manage plugins')}</Modal.Title>
|
||||
<Button
|
||||
variant="secondary-outline"
|
||||
onClick={handleModalClose}
|
||||
className="float-right"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<MdClose />
|
||||
</Button>
|
||||
</Modal.Header>
|
||||
{modalBody}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
PluginModal.propTypes = {
|
||||
show: PropTypes.bool.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
openModal: PropTypes.func.isRequired,
|
||||
updateInvestList: PropTypes.func.isRequired,
|
||||
closeInvestModel: PropTypes.func.isRequired,
|
||||
openJobs: PropTypes.shape({
|
||||
|
|
|
@ -8,7 +8,6 @@ import Form from 'react-bootstrap/Form';
|
|||
import Button from 'react-bootstrap/Button';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import {
|
||||
MdSettings,
|
||||
MdClose,
|
||||
MdTranslate,
|
||||
} from 'react-icons/md';
|
||||
|
@ -17,7 +16,6 @@ import { withTranslation } from 'react-i18next';
|
|||
|
||||
import { ipcMainChannels } from '../../../main/ipcMainChannels';
|
||||
import { getSupportedLanguages } from '../../server_requests';
|
||||
import MetadataForm from './MetadataForm';
|
||||
|
||||
const { ipcRenderer } = window.Workbench.electron;
|
||||
|
||||
|
@ -26,21 +24,17 @@ class SettingsModal extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
show: false,
|
||||
languageOptions: null,
|
||||
loggingLevel: null,
|
||||
taskgraphLoggingLevel: null,
|
||||
nWorkers: null,
|
||||
loggingLevel: '',
|
||||
taskgraphLoggingLevel: '',
|
||||
nWorkers: -1,
|
||||
language: window.Workbench.LANGUAGE,
|
||||
showConfirmLanguageChange: false,
|
||||
};
|
||||
this.handleShow = this.handleShow.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleChangeNumber = this.handleChangeNumber.bind(this);
|
||||
this.loadSettings = this.loadSettings.bind(this);
|
||||
this.handleChangeLanguage = this.handleChangeLanguage.bind(this);
|
||||
this.switchToDownloadModal = this.switchToDownloadModal.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
|
@ -51,16 +45,6 @@ class SettingsModal extends React.Component {
|
|||
this.loadSettings();
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.setState({
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleShow() {
|
||||
this.setState({ show: true });
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
const { name, value } = event.currentTarget;
|
||||
this.setState({ [name]: value });
|
||||
|
@ -97,14 +81,8 @@ class SettingsModal extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
switchToDownloadModal() {
|
||||
this.props.showDownloadModal();
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
show,
|
||||
languageOptions,
|
||||
language,
|
||||
loggingLevel,
|
||||
|
@ -112,7 +90,7 @@ class SettingsModal extends React.Component {
|
|||
nWorkers,
|
||||
showConfirmLanguageChange,
|
||||
} = this.state;
|
||||
const { clearJobsStorage, nCPU, t } = this.props;
|
||||
const { show, close, nCPU, t } = this.props;
|
||||
|
||||
const nWorkersOptions = [
|
||||
[-1, `${t('Synchronous')} (-1)`],
|
||||
|
@ -129,28 +107,18 @@ class SettingsModal extends React.Component {
|
|||
};
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
aria-label="settings"
|
||||
className="settings-icon-btn"
|
||||
onClick={this.handleShow}
|
||||
>
|
||||
<MdSettings
|
||||
className="settings-icon"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
className="settings-modal"
|
||||
show={show}
|
||||
onHide={this.handleClose}
|
||||
onHide={close}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('InVEST Settings')}</Modal.Title>
|
||||
<Button
|
||||
variant="secondary-outline"
|
||||
onClick={this.handleClose}
|
||||
onClick={close}
|
||||
className="float-right"
|
||||
aria-label="close settings"
|
||||
aria-label="close modal"
|
||||
>
|
||||
<MdClose />
|
||||
</Button>
|
||||
|
@ -273,44 +241,14 @@ class SettingsModal extends React.Component {
|
|||
: <div />
|
||||
}
|
||||
<hr />
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={this.switchToDownloadModal}
|
||||
className="w-100"
|
||||
>
|
||||
{t('Download Sample Data')}
|
||||
</Button>
|
||||
<hr />
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={clearJobsStorage}
|
||||
className="mr-2 w-100"
|
||||
>
|
||||
{t('Clear Recent Jobs')}
|
||||
</Button>
|
||||
<span><em>{t('*no invest workspaces will be deleted')}</em></span>
|
||||
<hr />
|
||||
<Accordion>
|
||||
<Accordion.Toggle
|
||||
as={Button}
|
||||
variant="outline-secondary"
|
||||
eventKey="0"
|
||||
className="mr-2 w-100"
|
||||
>
|
||||
{t('Configure Metadata')}
|
||||
<BsChevronDown className="mx-1" />
|
||||
</Accordion.Toggle>
|
||||
<Accordion.Collapse eventKey="0">
|
||||
<MetadataForm />
|
||||
</Accordion.Collapse>
|
||||
</Accordion>
|
||||
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
{
|
||||
(languageOptions) ? (
|
||||
<Modal show={showConfirmLanguageChange} className="confirm-modal" >
|
||||
<Modal show={showConfirmLanguageChange} className="confirm-modal">
|
||||
<Modal.Header>
|
||||
<Modal.Title as="h5" >{t('Warning')}</Modal.Title>
|
||||
<Modal.Title as="h5">{t('Warning')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
|
@ -336,8 +274,8 @@ class SettingsModal extends React.Component {
|
|||
}
|
||||
|
||||
SettingsModal.propTypes = {
|
||||
clearJobsStorage: PropTypes.func.isRequired,
|
||||
showDownloadModal: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
nCPU: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ body {
|
|||
.navbar {
|
||||
border-bottom: 3px solid var(--invest-green);
|
||||
padding: 0;
|
||||
height: var(--header-height)
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
.navbar .row {
|
||||
|
@ -156,15 +156,22 @@ body {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-icon-btn, .settings-icon-btn:hover {
|
||||
.app-menu-button {
|
||||
color: gray;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
color: black;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
vertical-align: text-bottom;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.app-menu-button:hover{
|
||||
color: white;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.app-menu-button:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.language-icon {
|
||||
|
@ -206,10 +213,6 @@ exceed 100% of window.*/
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.invest-list-group .list-group-item:last-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.invest-button {
|
||||
color: var(--invest-green);
|
||||
font-weight: 600;
|
||||
|
@ -254,23 +257,30 @@ exceed 100% of window.*/
|
|||
|
||||
.recent-job-card {
|
||||
width: inherit;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0;
|
||||
height: fit-content;
|
||||
filter: drop-shadow(2px 2px 2px grey);
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0;
|
||||
padding-top: 1rem;
|
||||
padding-left: 0rem;
|
||||
padding-right: 0rem;
|
||||
padding-bottom: 0rem;
|
||||
width: inherit;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: var(--invest-green);
|
||||
filter: opacity(0.75);
|
||||
margin-bottom: 1rem;
|
||||
margin-left: -0.1rem;
|
||||
margin-right: -0.05rem;
|
||||
padding-right: 0;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-header .header-title {
|
||||
|
@ -280,8 +290,14 @@ exceed 100% of window.*/
|
|||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.card-header .btn:hover {
|
||||
background-color: white;
|
||||
color: var(--invest-green);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
padding-left: 1.25rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.card-title .text-heading {
|
||||
|
@ -290,7 +306,7 @@ exceed 100% of window.*/
|
|||
|
||||
.card-title .text-mono {
|
||||
font-family: monospace;
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
|
|
|
@ -181,7 +181,7 @@ test('Run a real invest model', async () => {
|
|||
'aria/[name="Download InVEST sample data"][role="dialog"]'
|
||||
);
|
||||
const downloadModalCancel = await downloadModal.waitForSelector(
|
||||
'aria/[name="Cancel"][role="button"]');
|
||||
'aria/[name="Close modal"][role="button"]');
|
||||
await page.waitForTimeout(WAIT_TO_CLICK); // waiting for click handler to be ready
|
||||
await downloadModalCancel.click();
|
||||
|
||||
|
@ -240,7 +240,7 @@ test('Run a real invest model', async () => {
|
|||
await page.screenshot({ path: `${SCREENSHOT_PREFIX}6-run-canceled.png` });
|
||||
}, 240000); // >2x the sum of all the max timeouts within this test
|
||||
|
||||
test('Check local userguide links', async () => {
|
||||
test.only('Check local userguide links', async () => {
|
||||
// On GHA MacOS, we seem to have to wait a long time for the browser
|
||||
// to be ready. Maybe related to https://github.com/natcap/invest-workbench/issues/158
|
||||
let i = 0;
|
||||
|
@ -262,7 +262,7 @@ test('Check local userguide links', async () => {
|
|||
'aria/[name="Download InVEST sample data"][role="dialog"]'
|
||||
);
|
||||
const downloadModalCancel = await downloadModal.waitForSelector(
|
||||
'aria/[name="Cancel"][role="button"]');
|
||||
'aria/[name="Close modal"][role="button"]');
|
||||
await page.waitForTimeout(WAIT_TO_CLICK); // waiting for click handler to be ready
|
||||
await downloadModalCancel.click();
|
||||
|
||||
|
@ -275,7 +275,7 @@ test('Check local userguide links', async () => {
|
|||
await changelogModalClose.click();
|
||||
|
||||
const investList = await page.waitForSelector('.invest-list-group');
|
||||
const modelButtons = await investList.$$('aria/[role="button"]');
|
||||
const modelButtons = await investList.$$('button.invest-button');
|
||||
|
||||
await page.waitForTimeout(WAIT_TO_CLICK); // first btn click does not register w/o this pause
|
||||
for (const btn of modelButtons) {
|
||||
|
@ -323,7 +323,7 @@ test.skip('Install and run a plugin', async () => {
|
|||
await page.screenshot({ path: `${SCREENSHOT_PREFIX}1-page-load.png` });
|
||||
const downloadModal = await page.waitForSelector('.modal-dialog');
|
||||
const downloadModalCancel = await downloadModal.waitForSelector(
|
||||
'aria/[name="Cancel"][role="button"]'
|
||||
'aria/[name="close modal"][role="button"]'
|
||||
);
|
||||
await page.waitForTimeout(WAIT_TO_CLICK); // waiting for click handler to be ready
|
||||
await downloadModalCancel.click();
|
||||
|
@ -334,9 +334,11 @@ test.skip('Install and run a plugin', async () => {
|
|||
await page.waitForTimeout(WAIT_TO_CLICK);
|
||||
await changelogModalCancel.click();
|
||||
|
||||
const addPluginButton = await page.waitForSelector('aria/[name="plugins"][role="button"]');
|
||||
await addPluginButton.click();
|
||||
console.log('clicked add plugin');
|
||||
const dropdownButton = await page.waitForSelector('aria/[name="menu"][role="button"]');
|
||||
await dropdownButton.click();
|
||||
const pluginsModalButton = await page.waitForSelector('aria/[name="Manage plugins"][role="button"]');
|
||||
await pluginsModalButton.click();
|
||||
console.log('opened plugin modal');
|
||||
const urlInputField = await page.waitForSelector('aria/[name="Git URL"][role="textbox"]');
|
||||
console.log('found url field');
|
||||
await urlInputField.type(TEST_PLUGIN_GIT_URL, { delay: TYPE_DELAY });
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import crypto from 'crypto';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import '../src/renderer/i18n/i18n';
|
||||
|
|
|
@ -13,16 +13,11 @@ import {
|
|||
fetchValidation,
|
||||
fetchDatastackFromFile,
|
||||
fetchArgsEnabled,
|
||||
getSupportedLanguages,
|
||||
getGeoMetaMakerProfile,
|
||||
} from '../../src/renderer/server_requests';
|
||||
import InvestJob from '../../src/renderer/InvestJob';
|
||||
import {
|
||||
settingsStore,
|
||||
setupSettingsHandlers
|
||||
} from '../../src/main/settingsStore';
|
||||
import { ipcMainChannels } from '../../src/main/ipcMainChannels';
|
||||
import { removeIpcMainListeners } from '../../src/main/main';
|
||||
import pkg from '../../package.json';
|
||||
|
||||
jest.mock('../../src/renderer/server_requests');
|
||||
|
||||
|
@ -145,7 +140,8 @@ describe('Various ways to open and close InVEST models', () => {
|
|||
<App />
|
||||
);
|
||||
|
||||
const openButton = await findByRole('button', { name: 'Open' });
|
||||
const openButton = await findByRole(
|
||||
'button', { name: /browse to a datastack or invest logfile/i });
|
||||
expect(openButton).not.toBeDisabled();
|
||||
await userEvent.click(openButton);
|
||||
const executeButton = await findByRole('button', { name: /Run/ });
|
||||
|
@ -175,7 +171,8 @@ describe('Various ways to open and close InVEST models', () => {
|
|||
<App />
|
||||
);
|
||||
|
||||
const openButton = await findByRole('button', { name: 'Open' });
|
||||
const openButton = await findByRole(
|
||||
'button', { name: /browse to a datastack or invest logfile/i });
|
||||
await userEvent.click(openButton);
|
||||
const homeTab = await findByRole('tabpanel', { name: 'home tab' });
|
||||
// expect we're on the same tab we started on instead of switching to Setup
|
||||
|
@ -305,7 +302,7 @@ describe('Display recently executed InVEST jobs on Home tab', () => {
|
|||
await waitFor(() => {
|
||||
initialJobs.forEach((job, idx) => {
|
||||
const recent = recentJobs[idx];
|
||||
const card = getByText(job.modelTitle)
|
||||
const card = getByText(job.argsValues.workspace_dir)
|
||||
.closest('button');
|
||||
expect(within(card).getByText(job.argsValues.workspace_dir))
|
||||
.toBeInTheDocument();
|
||||
|
@ -369,7 +366,7 @@ describe('Display recently executed InVEST jobs on Home tab', () => {
|
|||
const { findByText, queryByText } = render(<App />);
|
||||
|
||||
expect(queryByText(job1.modelTitle)).toBeNull();
|
||||
expect(await findByText(/Set up a model from a sample datastack file/))
|
||||
expect(await findByText('Welcome!'))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
@ -378,13 +375,11 @@ describe('Display recently executed InVEST jobs on Home tab', () => {
|
|||
<App />
|
||||
);
|
||||
|
||||
const node = await findByText(/Set up a model from a sample datastack file/);
|
||||
const node = await findByText('Welcome!');
|
||||
expect(node).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Recent Jobs: cleared by button', async () => {
|
||||
// we need this mock because the settings dialog is opened
|
||||
getGeoMetaMakerProfile.mockResolvedValue({});
|
||||
test('Recent Jobs: cleared by clear all button', async () => {
|
||||
const job1 = new InvestJob({
|
||||
modelID: MOCK_MODEL_ID,
|
||||
modelTitle: 'Carbon Sequestration',
|
||||
|
@ -405,100 +400,167 @@ describe('Display recently executed InVEST jobs on Home tab', () => {
|
|||
.toBeTruthy();
|
||||
});
|
||||
});
|
||||
await userEvent.click(getByRole('button', { name: 'settings' }));
|
||||
await userEvent.click(getByText('Clear Recent Jobs'));
|
||||
const node = await findByText(/Set up a model from a sample datastack file/);
|
||||
await userEvent.click(getByText(/clear all model runs/i));
|
||||
const node = await findByText('Welcome!');
|
||||
expect(node).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Recent Jobs: delete single job', async () => {
|
||||
const job1 = new InvestJob({
|
||||
modelID: MOCK_MODEL_ID,
|
||||
modelTitle: 'Carbon Sequestration',
|
||||
argsValues: {
|
||||
workspace_dir: 'work1',
|
||||
},
|
||||
status: 'success',
|
||||
// leave out the 'type' attribute to make sure it defaults to core
|
||||
// for backwards compatibility
|
||||
});
|
||||
const recentJobs = await InvestJob.saveJob(job1);
|
||||
|
||||
const { getByText, findByText, getByRole } = render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
recentJobs.forEach((job) => {
|
||||
expect(getByText(job.argsValues.workspace_dir))
|
||||
.toBeTruthy();
|
||||
});
|
||||
});
|
||||
await userEvent.click(getByRole('button', { name: 'delete' }));
|
||||
const node = await findByText('Welcome!');
|
||||
expect(node).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InVEST global settings: dialog interactions', () => {
|
||||
const nWorkersLabelText = 'Taskgraph n_workers parameter';
|
||||
const loggingLabelText = 'Logging threshold';
|
||||
const tgLoggingLabelText = 'Taskgraph logging threshold';
|
||||
const languageLabelText = 'Language';
|
||||
|
||||
beforeAll(() => {
|
||||
setupSettingsHandlers();
|
||||
describe('Main menu interactions', () => {
|
||||
beforeEach(() => {
|
||||
getInvestModelIDs.mockResolvedValue(MOCK_INVEST_LIST);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
removeIpcMainListeners();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
getInvestModelIDs.mockResolvedValue({});
|
||||
getSupportedLanguages.mockResolvedValue({ en: 'english', es: 'spanish' });
|
||||
getGeoMetaMakerProfile.mockResolvedValue({});
|
||||
});
|
||||
|
||||
test('Invest settings save on change', async () => {
|
||||
const nWorkersLabel = 'Threaded task management (0)';
|
||||
const nWorkersValue = 0;
|
||||
const loggingLevel = 'DEBUG';
|
||||
const tgLoggingLevel = 'DEBUG';
|
||||
const languageValue = 'es';
|
||||
const spyInvoke = jest.spyOn(ipcRenderer, 'invoke');
|
||||
|
||||
test('Open sampledata download Modal from menu', async () => {
|
||||
const {
|
||||
getByText, getByLabelText, findByRole, findByText,
|
||||
findByText, findByRole,
|
||||
} = render(
|
||||
<App />
|
||||
);
|
||||
|
||||
await userEvent.click(await findByRole('button', { name: 'settings' }));
|
||||
const nWorkersInput = getByLabelText(nWorkersLabelText, { exact: false });
|
||||
const loggingInput = getByLabelText(loggingLabelText);
|
||||
const tgLoggingInput = getByLabelText(tgLoggingLabelText);
|
||||
const dropdownBtn = await findByRole('button', { name: 'menu' });
|
||||
await userEvent.click(dropdownBtn);
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: /Download Sample Data/i })
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(nWorkersInput, [getByText(nWorkersLabel)]);
|
||||
await waitFor(() => { expect(nWorkersInput).toHaveValue(nWorkersValue.toString()); });
|
||||
await userEvent.selectOptions(loggingInput, [loggingLevel]);
|
||||
await waitFor(() => { expect(loggingInput).toHaveValue(loggingLevel); });
|
||||
await userEvent.selectOptions(tgLoggingInput, [tgLoggingLevel]);
|
||||
await waitFor(() => { expect(tgLoggingInput).toHaveValue(tgLoggingLevel); });
|
||||
|
||||
// Check values were saved
|
||||
expect(settingsStore.get('nWorkers')).toBe(nWorkersValue);
|
||||
expect(settingsStore.get('loggingLevel')).toBe(loggingLevel);
|
||||
expect(settingsStore.get('taskgraphLoggingLevel')).toBe(tgLoggingLevel);
|
||||
|
||||
// language is handled differently; changing it triggers electron to restart
|
||||
const languageInput = getByLabelText(languageLabelText, { exact: false });
|
||||
await userEvent.selectOptions(languageInput, [languageValue]);
|
||||
await userEvent.click(await findByText('Change to spanish'));
|
||||
expect(spyInvoke)
|
||||
.toHaveBeenCalledWith(ipcMainChannels.CHANGE_LANGUAGE, languageValue);
|
||||
expect(await findByText(/Download InVEST sample data/i))
|
||||
.toBeInTheDocument();
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: /close modal/i })
|
||||
);
|
||||
});
|
||||
|
||||
test('Access sampledata download Modal from settings', async () => {
|
||||
test('Open Metadata Modal from menu', async () => {
|
||||
const {
|
||||
findByText, findByRole, queryByText,
|
||||
findByText, findByRole,
|
||||
} = render(
|
||||
<App />
|
||||
);
|
||||
|
||||
const settingsBtn = await findByRole('button', { name: 'settings' });
|
||||
await userEvent.click(settingsBtn);
|
||||
const dropdownBtn = await findByRole('button', { name: 'menu' });
|
||||
await userEvent.click(dropdownBtn);
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: 'Download Sample Data' })
|
||||
await findByRole('button', { name: /Configure Metadata/i })
|
||||
);
|
||||
|
||||
expect(await findByText('Download InVEST sample data'))
|
||||
expect(await findByText(/contact information/i))
|
||||
.toBeInTheDocument();
|
||||
expect(queryByText('Settings')).toBeNull();
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: /close modal/i })
|
||||
);
|
||||
});
|
||||
|
||||
test('Access metadata form from settings', async () => {
|
||||
const { findByRole } = render(<App />);
|
||||
test('Open Plugins Modal from menu', async () => {
|
||||
ipcRenderer.invoke.mockImplementation((channel) => {
|
||||
if (channel === ipcMainChannels.HAS_MSVC) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
const settingsBtn = await findByRole('button', { name: 'settings' });
|
||||
await userEvent.click(settingsBtn);
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: 'Configure Metadata' })
|
||||
const {
|
||||
findByText, findByRole,
|
||||
} = render(
|
||||
<App />
|
||||
);
|
||||
|
||||
expect(await findByRole('button', { name: 'Save Metadata' }))
|
||||
const dropdownBtn = await findByRole('button', { name: 'menu' });
|
||||
await userEvent.click(dropdownBtn);
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: /Manage Plugins/i })
|
||||
);
|
||||
|
||||
expect(await findByText(/add a plugin/i))
|
||||
.toBeInTheDocument();
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: /close modal/i })
|
||||
);
|
||||
});
|
||||
|
||||
test('Open Changelog Modal from menu', async () => {
|
||||
const currentVersion = pkg.version;
|
||||
const nonexistentVersion = '1.0.0';
|
||||
jest.spyOn(window, 'fetch')
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => `
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<section>
|
||||
<h1>${currentVersion}</h1>
|
||||
</section>
|
||||
<section>
|
||||
<h1>${nonexistentVersion}</h1>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
});
|
||||
|
||||
const {
|
||||
findByText, findByRole,
|
||||
} = render(
|
||||
<App />
|
||||
);
|
||||
|
||||
const dropdownBtn = await findByRole('button', { name: 'menu' });
|
||||
await userEvent.click(dropdownBtn);
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: /view changelog/i })
|
||||
);
|
||||
|
||||
expect(await findByText(/new in this version/i))
|
||||
.toBeInTheDocument();
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: /close modal/i })
|
||||
);
|
||||
});
|
||||
|
||||
test('Open Settings Modal from menu', async () => {
|
||||
const {
|
||||
findByText, findByRole,
|
||||
} = render(
|
||||
<App />
|
||||
);
|
||||
|
||||
const dropdownBtn = await findByRole('button', { name: 'menu' });
|
||||
await userEvent.click(dropdownBtn);
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: /settings/i })
|
||||
);
|
||||
|
||||
expect(await findByText(/invest settings/i))
|
||||
.toBeInTheDocument();
|
||||
await userEvent.click(
|
||||
await findByRole('button', { name: /close modal/i })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,10 +10,10 @@ import { getInvestModelIDs } from '../../src/renderer/server_requests';
|
|||
jest.mock('../../src/renderer/server_requests');
|
||||
|
||||
const MOCK_MODEL_TITLE = 'Carbon';
|
||||
const MOCK_MODEL_RUN_NAME = 'carbon';
|
||||
const MOCK_MODEL_ID = 'carbon';
|
||||
const MOCK_INVEST_LIST = {
|
||||
[MOCK_MODEL_TITLE]: {
|
||||
model_name: MOCK_MODEL_RUN_NAME,
|
||||
[MOCK_MODEL_ID]: {
|
||||
model_title: MOCK_MODEL_TITLE,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -48,7 +48,7 @@ describe('Changelog', () => {
|
|||
});
|
||||
|
||||
test('On first run (of any version), Changelog modal opens after Download modal is closed', async () => {
|
||||
const { findByRole, getByText } = render(<App isFirstRun isNewVersion />);
|
||||
const { findByRole, getByRole } = render(<App isFirstRun isNewVersion />);
|
||||
|
||||
let changelogModalFound = true;
|
||||
try {
|
||||
|
@ -61,7 +61,7 @@ describe('Changelog', () => {
|
|||
const downloadModal = await findByRole('dialog', { name: 'Download InVEST sample data' });
|
||||
expect(downloadModal).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(getByText('Cancel'));
|
||||
await userEvent.click(getByRole('button', { name: /close modal/i }));
|
||||
expect(downloadModal).not.toBeInTheDocument();
|
||||
const changelogModal = await findByRole('dialog', { name: 'New in this version' });
|
||||
expect(changelogModal).toBeInTheDocument();
|
||||
|
|
|
@ -29,12 +29,12 @@ describe('Sample Data Download Form', () => {
|
|||
test('Modal displays immediately on user`s first run', async () => {
|
||||
const {
|
||||
findByText,
|
||||
getByText,
|
||||
getByRole,
|
||||
} = render(<App isFirstRun />);
|
||||
|
||||
const modalTitle = await findByText('Download InVEST sample data');
|
||||
expect(modalTitle).toBeInTheDocument();
|
||||
userEvent.click(getByText('Cancel'));
|
||||
await userEvent.click(getByRole('button', { name: /close modal/i }));
|
||||
await waitFor(() => {
|
||||
expect(modalTitle).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
|
@ -115,4 +115,22 @@ describe('InvestJob', () => {
|
|||
recentJobs = await InvestJob.clearStore();
|
||||
expect(recentJobs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('deleteJob deletes the job', async () => {
|
||||
const job1 = new InvestJob({
|
||||
modelID: 'foo',
|
||||
modelTitle: 'Foo',
|
||||
argsValues: baseArgsValues,
|
||||
});
|
||||
const job2 = new InvestJob({
|
||||
modelID: 'foo',
|
||||
modelTitle: 'Foo',
|
||||
argsValues: baseArgsValues,
|
||||
});
|
||||
let recentJobs = await InvestJob.saveJob(job1);
|
||||
recentJobs = await InvestJob.saveJob(job2);
|
||||
expect(recentJobs).toHaveLength(2);
|
||||
recentJobs = await InvestJob.deleteJob(job1.hash);
|
||||
expect(recentJobs).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import { render, waitFor } from '@testing-library/react';
|
|||
import '@testing-library/jest-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import MetadataForm from '../../src/renderer/components/SettingsModal/MetadataForm';
|
||||
import MetadataModal from '../../src/renderer/components/MetadataModal';
|
||||
import {
|
||||
getGeoMetaMakerProfile,
|
||||
setGeoMetaMakerProfile,
|
||||
|
@ -36,8 +36,12 @@ test('Metadata form interact and submit', async () => {
|
|||
findByRole,
|
||||
getByLabelText,
|
||||
getByRole,
|
||||
getByText,
|
||||
} = render(<MetadataForm />);
|
||||
} = render(
|
||||
<MetadataModal
|
||||
show
|
||||
close={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
// The form should render with content from an existing profile
|
||||
const nameInput = getByLabelText('Full name');
|
||||
|
@ -56,11 +60,6 @@ test('Metadata form interact and submit', async () => {
|
|||
await user.type(nameInput, name);
|
||||
await user.type(licenseInput, license);
|
||||
|
||||
// Exercise the "more info" button
|
||||
await user.click(getByRole('button', { name: /more info/i }));
|
||||
expect(getByText('Metadata for InVEST results')).toBeInTheDocument();
|
||||
await user.click(getByRole('button', { name: /hide info/i }));
|
||||
|
||||
const submit = getByRole('button', { name: /save metadata/i });
|
||||
await user.click(submit);
|
||||
|
||||
|
@ -104,7 +103,12 @@ test('Metadata form error on submit', async () => {
|
|||
findByRole,
|
||||
getByLabelText,
|
||||
getByRole,
|
||||
} = render(<MetadataForm />);
|
||||
} = render(
|
||||
<MetadataModal
|
||||
show
|
||||
close={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const submit = getByRole('button', { name: /save metadata/i });
|
||||
await user.click(submit);
|
||||
|
|
|
@ -19,9 +19,10 @@ test('Open File: displays a tooltip on hover', async () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const openButton = await findByRole('button', { name: 'Open' });
|
||||
const openButton = await findByRole(
|
||||
'button', { name: /Browse to a datastack or InVEST logfile/i });
|
||||
await userEvent.hover(openButton);
|
||||
const hoverText = 'Browse to a datastack (.json) or InVEST logfile (.txt)';
|
||||
const hoverText = /Open an InVEST model by loading/i;
|
||||
expect(await findByText(hoverText)).toBeInTheDocument();
|
||||
await userEvent.unhover(openButton);
|
||||
await waitFor(() => {
|
||||
|
@ -53,7 +54,8 @@ test('Open File: sends correct payload', async () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const openButton = await findByRole('button', { name: 'Open' });
|
||||
const openButton = await findByRole(
|
||||
'button', { name: /Browse to a datastack or InVEST logfile/i });
|
||||
await userEvent.click(openButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ipcRenderer } from 'electron';
|
||||
import React from 'react';
|
||||
import {
|
||||
act, within, render, waitFor
|
||||
within, render, waitFor
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import '@testing-library/jest-dom';
|
||||
|
@ -62,10 +62,11 @@ describe('Add plugin modal', () => {
|
|||
return Promise.resolve();
|
||||
});
|
||||
const {
|
||||
findByText, findByLabelText, findByRole, queryByRole,
|
||||
findByText, findByLabelText, findByRole,
|
||||
} = render(<App />);
|
||||
|
||||
const managePluginsButton = await findByText('Manage plugins');
|
||||
await userEvent.click(await findByRole('button', { name: 'menu' }));
|
||||
const managePluginsButton = await findByText(/Manage plugins/i);
|
||||
userEvent.click(managePluginsButton);
|
||||
|
||||
const urlField = await findByLabelText('Git URL');
|
||||
|
@ -105,9 +106,7 @@ describe('Add plugin modal', () => {
|
|||
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||
const { findByRole } = render(<App />);
|
||||
const pluginButton = await findByRole('button', { name: /Foo/ });
|
||||
await act(async () => {
|
||||
userEvent.click(pluginButton);
|
||||
});
|
||||
await userEvent.click(pluginButton);
|
||||
const executeButton = await findByRole('button', { name: /Run/ });
|
||||
expect(executeButton).toBeEnabled();
|
||||
// Nothing is really different about plugin tabs on the renderer side, so
|
||||
|
@ -151,7 +150,8 @@ describe('Add plugin modal', () => {
|
|||
const pluginButton = await findByRole('button', { name: /Foo/ });
|
||||
await userEvent.click(pluginButton);
|
||||
|
||||
const managePluginsButton = await findByText('Manage plugins');
|
||||
await userEvent.click(await findByRole('button', { name: 'menu' }));
|
||||
const managePluginsButton = await findByText(/Manage plugins/i);
|
||||
await userEvent.click(managePluginsButton);
|
||||
|
||||
const pluginDropdown = await findByLabelText('Plugin name');
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import React from 'react';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import {
|
||||
render, waitFor
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import {
|
||||
settingsStore,
|
||||
setupSettingsHandlers
|
||||
} from '../../src/main/settingsStore';
|
||||
import { ipcMainChannels } from '../../src/main/ipcMainChannels';
|
||||
import { removeIpcMainListeners } from '../../src/main/main';
|
||||
|
||||
import { getSupportedLanguages } from '../../src/renderer/server_requests';
|
||||
import SettingsModal from '../../src/renderer/components/SettingsModal';
|
||||
|
||||
jest.mock('../../src/renderer/server_requests');
|
||||
|
||||
describe('InVEST global settings: dialog interactions', () => {
|
||||
const nWorkersLabelText = 'Taskgraph n_workers parameter';
|
||||
const loggingLabelText = 'Logging threshold';
|
||||
const tgLoggingLabelText = 'Taskgraph logging threshold';
|
||||
const languageLabelText = 'Language';
|
||||
|
||||
beforeAll(() => {
|
||||
setupSettingsHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
removeIpcMainListeners();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
getSupportedLanguages.mockResolvedValue({ en: 'english', es: 'spanish' });
|
||||
});
|
||||
|
||||
test('Invest settings save on change', async () => {
|
||||
const nWorkersLabel = 'Threaded task management (0)';
|
||||
const nWorkersValue = 0;
|
||||
const loggingLevel = 'DEBUG';
|
||||
const tgLoggingLevel = 'DEBUG';
|
||||
const languageValue = 'es';
|
||||
const spyInvoke = jest.spyOn(ipcRenderer, 'invoke');
|
||||
|
||||
const {
|
||||
getByText, getByLabelText, findByText,
|
||||
} = render(
|
||||
<SettingsModal
|
||||
show
|
||||
close={() => {}}
|
||||
nCPU={4}
|
||||
/>
|
||||
);
|
||||
|
||||
const nWorkersInput = getByLabelText(nWorkersLabelText, { exact: false });
|
||||
const loggingInput = getByLabelText(loggingLabelText);
|
||||
const tgLoggingInput = getByLabelText(tgLoggingLabelText);
|
||||
|
||||
await userEvent.selectOptions(nWorkersInput, [getByText(nWorkersLabel)]);
|
||||
await waitFor(() => { expect(nWorkersInput).toHaveValue(nWorkersValue.toString()); });
|
||||
await userEvent.selectOptions(loggingInput, [loggingLevel]);
|
||||
await waitFor(() => { expect(loggingInput).toHaveValue(loggingLevel); });
|
||||
await userEvent.selectOptions(tgLoggingInput, [tgLoggingLevel]);
|
||||
await waitFor(() => { expect(tgLoggingInput).toHaveValue(tgLoggingLevel); });
|
||||
|
||||
// Check values were saved
|
||||
expect(settingsStore.get('nWorkers')).toBe(nWorkersValue);
|
||||
expect(settingsStore.get('loggingLevel')).toBe(loggingLevel);
|
||||
expect(settingsStore.get('taskgraphLoggingLevel')).toBe(tgLoggingLevel);
|
||||
|
||||
// language is handled differently; changing it triggers electron to restart
|
||||
const languageInput = getByLabelText(languageLabelText, { exact: false });
|
||||
await userEvent.selectOptions(languageInput, [languageValue]);
|
||||
await userEvent.click(await findByText('Change to spanish'));
|
||||
expect(spyInvoke)
|
||||
.toHaveBeenCalledWith(ipcMainChannels.CHANGE_LANGUAGE, languageValue);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue