Merge pull request #244 from pybricks/dlech

add help
This commit is contained in:
David Lechner
2021-01-17 15:04:25 -06:00
committed by GitHub
51 changed files with 1249 additions and 142 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 The Pybricks Authors
Copyright (c) 2020-2021 The Pybricks Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+227
View File
@@ -1,3 +1,5 @@
// Configuration file to override create-react-app.
//
// https://github.com/gsoft-inc/craco/blob/master/packages/craco/README.md#configuration
const {
@@ -6,9 +8,234 @@ const {
getLoaders,
loaderByName,
} = require('@craco/craco');
const LicensePlugin = require('license-webpack-plugin').LicenseWebpackPlugin;
const satisfies = require('spdx-satisfies');
// Permissive licenses can be added here. We would like to avoid copyleft.
const approvedLicenses = ['0BSD', 'Apache-2.0', 'BSD-3-Clause', 'ISC', 'MIT'];
/** Converts a package.json "person" to a string.
*
* Ref: https://docs.npmjs.com/cli/v6/configuring-npm/package-json#people-fields-author-contributors
*
* @param person a string or "person" object or undefined
* @returns A string or undefined.
*/
function personToString(person) {
if (typeof person === 'string' || typeof person === 'undefined') {
return person;
}
let str = person.name;
if (person.email) {
str += ` <${person.email}>`;
}
if (person.url) {
str += ` (${person.url})`;
}
return str;
}
// Some packages don't have a separate license file, so we have to copy the
// license here e.g. from the README.
function fedorIndutnyLicense(year) {
return `This software is licensed under the MIT License.
Copyright Fedor Indutny, ${year}.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.`;
}
const pybricksLicense = `MIT License
Copyright (c) 2018-2021 The Pybricks Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
`;
const shopifyLicense = `MIT License
Copyright (c) 2021 Shopify
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`;
const licenseTextOverrides = {
'@pybricks/firmware': pybricksLicense,
'@pybricks/mpy-cross-v5': pybricksLicense,
'@shopify/dates': shopifyLicense,
'@shopify/decorators': shopifyLicense,
'@shopify/function-enhancers': shopifyLicense,
'@shopify/i18n': shopifyLicense,
'@shopify/react': shopifyLicense,
'@shopify/react-hooks': shopifyLicense,
'@shopify/react-i18n': shopifyLicense,
'bn.js': fedorIndutnyLicense(2015),
brorand: fedorIndutnyLicense(2014),
'des.js': fedorIndutnyLicense(2015),
elliptic: fedorIndutnyLicense(2014),
'hash.js': fedorIndutnyLicense(2014),
'hmac-drbg': fedorIndutnyLicense(2017),
'miller-rabin': fedorIndutnyLicense(2014),
'minimalistic-crypto-utils': fedorIndutnyLicense(2017),
gud: `MIT License
Copyright (c)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`,
isarray: `(MIT)
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`,
'popper.js': `The MIT License (MIT)
Copyright (c) 2019 Federico Zivolo
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
'zen-push': `Copyright (c) 2018 zenparsing (Kevin Smith)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
};
module.exports = {
webpack: {
plugins: [
new LicensePlugin({
outputFilename: 'static/oss-licenses.json',
perChunkOutput: false,
renderLicenses: (modules) => {
return JSON.stringify(
modules
.map((m) => ({
name: m.packageJson.name,
version: m.packageJson.version,
author: personToString(m.packageJson.author),
license: m.licenseId,
licenseText: m.licenseText,
}))
.sort((a, b) => a.name.localeCompare(b.name, 'en')),
);
},
licenseTextOverrides,
additionalModules: [
{ name: '@pybricks/pybricks-code', directory: __dirname },
],
unacceptableLicenseTest: (licenseType) =>
!satisfies(licenseType, `(${approvedLicenses.join(' OR ')})`),
}),
],
configure: {
resolve: {
// need 'esnext' first to avoid compile errors
+9 -2
View File
@@ -1,7 +1,12 @@
{
"name": "pybricks-code",
"name": "@pybricks/pybricks-code",
"version": "0.1.0",
"private": true,
"license": "MIT",
"author": "The Pybricks Authors",
"repository": {
"type": "git",
"url": "https://github.com/pybricks/pybricks-code"
},
"dependencies": {
"@blueprintjs/core": "^3.36.0",
"@craco/craco": "^6.0.0",
@@ -24,6 +29,7 @@
"@types/zen-push": "^0.1.1",
"ace-builds": "^1.4.12",
"file-saver": "^2.0.5",
"license-webpack-plugin": "^2.3.11",
"node-sass": "^4.14.1",
"prop-types": "^15.7.2",
"react": "^16.13.1",
@@ -36,6 +42,7 @@
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
"redux-saga": "^1.1.3",
"spdx-satisfies": "^5.0.0",
"typescript": "~4.1.3",
"web-vitals": "^1.0.1",
"xterm": "^4.9.0",
+41 -8
View File
@@ -13,8 +13,14 @@ export enum AppActionType {
OpenSettings = 'app.action.openSettings',
/** Close settings dialog. */
CloseSettings = 'app.action.closeSettings',
/** Toggle documentation visibility. */
ToggleDocs = 'app.action.toggleDocs',
/** Open about dialog. */
OpenAboutDialog = 'app.action.openAboutDialog',
/** Close about dialog. */
CloseAboutDialog = 'app.action.closeAboutDialog',
/** Open license dialog. */
OpenLicenseDialog = 'app.action.openLicenseDialog',
/** Close license dialog. */
CloseLicenseDialog = 'app.action.closeLicenseDialog',
}
/** Action that indicates the app has just started. */
@@ -41,12 +47,36 @@ export function closeSettings(): AppCloseSettingsAction {
return { type: AppActionType.CloseSettings };
}
/** Action to toggle documentation visibility. */
export type AppToggleDocsAction = Action<AppActionType.ToggleDocs>;
/** Action to open the about dialog. */
export type AppOpenAboutDialogAction = Action<AppActionType.OpenAboutDialog>;
/** Creates an action to toggle documentation visibility. */
export function toggleDocs(): AppToggleDocsAction {
return { type: AppActionType.ToggleDocs };
/** Creates an action to open the about dialog. */
export function openAboutDialog(): AppOpenAboutDialogAction {
return { type: AppActionType.OpenAboutDialog };
}
/** Action to close the about dialog. */
export type AppCloseAboutDialogAction = Action<AppActionType.CloseAboutDialog>;
/** Creates an action to close the about dialog. */
export function closeAboutDialog(): AppCloseAboutDialogAction {
return { type: AppActionType.CloseAboutDialog };
}
/** Action to open the license dialog. */
export type AppOpenLicenseDialogAction = Action<AppActionType.OpenLicenseDialog>;
/** Creates an action to open the license dialog. */
export function openLicenseDialog(): AppOpenLicenseDialogAction {
return { type: AppActionType.OpenLicenseDialog };
}
/** Action to close the license dialog. */
export type AppCloseLicenseDialogAction = Action<AppActionType.CloseLicenseDialog>;
/** Creates an action to close the license dialog. */
export function closeLicenseDialog(): AppCloseLicenseDialogAction {
return { type: AppActionType.CloseLicenseDialog };
}
/** common type for all app actions. */
@@ -54,4 +84,7 @@ export type AppAction =
| AppStartupAction
| AppOpenSettingsAction
| AppCloseSettingsAction
| AppToggleDocsAction;
| AppOpenAboutDialogAction
| AppCloseAboutDialogAction
| AppOpenLicenseDialogAction
| AppCloseLicenseDialogAction;
+2
View File
@@ -8,6 +8,7 @@ import { BleUartAction } from './ble-uart';
import { EditorAction } from './editor';
import { FlashFirmwareAction } from './flash-firmware';
import { HubAction, HubMessageAction } from './hub';
import { LicenseAction } from './license';
import {
BootloaderConnectionAction,
BootloaderDidRequestAction,
@@ -36,6 +37,7 @@ export type Action =
| FlashFirmwareAction
| HubAction
| HubMessageAction
| LicenseAction
| MpyAction
| NotificationAction
| ServiceWorkerAction
+40
View File
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { Action } from 'redux';
import { LicenseInfo, LicenseList } from '../reducers/license';
export enum LicenseActionType {
DidFetchList = 'license.action.didFetchList',
DidFailToFetchList = 'license.action.didFailToFetchList',
Select = 'license.action.select',
}
export type LicenseDidFetchListAction = Action<LicenseActionType.DidFetchList> & {
list: LicenseList;
};
export function didFetchList(list: LicenseList): LicenseDidFetchListAction {
return { type: LicenseActionType.DidFetchList, list };
}
export type LicenseDidFailToFetchListAction = Action<LicenseActionType.DidFailToFetchList> & {
reason: Response;
};
export function didFailToFetchList(reason: Response): LicenseDidFailToFetchListAction {
return { type: LicenseActionType.DidFailToFetchList, reason };
}
export type LicenseSelectAction = Action<LicenseActionType.Select> & {
info: LicenseInfo;
};
export function select(info: LicenseInfo): LicenseSelectAction {
return { type: LicenseActionType.Select, info };
}
export type LicenseAction =
| LicenseDidFetchListAction
| LicenseDidFailToFetchListAction
| LicenseSelectAction;
+8 -8
View File
@@ -4,8 +4,8 @@
import { Action } from 'redux';
export enum ServiceWorkerActionType {
Update = 'serviceWorker.update',
Success = 'serviceWorker.success',
DidUpdate = 'serviceWorker.didUpdate',
DidSucceed = 'serviceWorker.didSucceed',
}
export type ServiceWorkerAction<
@@ -14,14 +14,14 @@ export type ServiceWorkerAction<
registration: ServiceWorkerRegistration;
};
export function update(
export function didUpdate(
registration: ServiceWorkerRegistration,
): ServiceWorkerAction<ServiceWorkerActionType.Update> {
return { type: ServiceWorkerActionType.Update, registration };
): ServiceWorkerAction<ServiceWorkerActionType.DidUpdate> {
return { type: ServiceWorkerActionType.DidUpdate, registration };
}
export function success(
export function didSucceed(
registration: ServiceWorkerRegistration,
): ServiceWorkerAction<ServiceWorkerActionType.Success> {
return { type: ServiceWorkerActionType.Success, registration };
): ServiceWorkerAction<ServiceWorkerActionType.DidSucceed> {
return { type: ServiceWorkerActionType.DidSucceed, registration };
}
+1 -1
View File
@@ -2,7 +2,7 @@
// Copyright (c) 2021 The Pybricks Authors
import { Action } from 'redux';
import { SettingId } from '../settings';
import { SettingId } from '../settings/user';
/** Actions related to settings. */
export enum SettingsActionType {
+78
View File
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// The about dialog
import { AnchorButton, Button, Classes, Dialog } from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import React from 'react';
import { connect } from 'react-redux';
import { Action, Dispatch } from '../actions';
import { closeAboutDialog, openLicenseDialog } from '../actions/app';
import { RootState } from '../reducers';
import {
appName,
legoDisclaimer,
pybricksCopyright,
pybricksWebsiteUrl,
} from '../settings/ui';
import ExternalLinkIcon from './ExternalLinkIcon';
import LicenseDialog from './LicenseDialog';
import { AboutStringId } from './about-i18n';
import en from './about-i18n.en.json';
import './about.scss';
type StateProps = { showAboutDialog: boolean };
type DispatchProps = { onClose: () => void; onLicenseButtonClick: () => void };
type AboutDialogProps = StateProps & DispatchProps & WithI18nProps;
class AboutDialog extends React.Component<AboutDialogProps> {
render(): JSX.Element {
const { i18n, showAboutDialog, onClose, onLicenseButtonClick } = this.props;
return (
<Dialog title={appName} isOpen={showAboutDialog} onClose={() => onClose()}>
<div className={Classes.DIALOG_BODY}>
<div className="pb-about-icon">
<img src="favicon.ico" />
</div>
<p>
<strong>{i18n.translate(AboutStringId.Description)}</strong>
</p>
<p>{pybricksCopyright}</p>
</div>
<div className={Classes.DIALOG_FOOTER}>
<p>
<small>{legoDisclaimer}</small>
</p>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={() => onLicenseButtonClick()}>
{i18n.translate(AboutStringId.LicenseButtonLabel)}
</Button>
<AnchorButton href={pybricksWebsiteUrl} target="blank_">
{i18n.translate(AboutStringId.WebsiteButtonLabel)}&nbsp;
<ExternalLinkIcon />
</AnchorButton>
</div>
</div>
<LicenseDialog />
</Dialog>
);
}
}
const mapStateToProps = (state: RootState): StateProps => ({
showAboutDialog: state.app.showAboutDialog,
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onClose: (): Action => dispatch(closeAboutDialog()),
onLicenseButtonClick: (): Action => dispatch(openLicenseDialog()),
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(withI18n({ id: 'about', fallback: en, translations: { en } })(AboutDialog));
+6 -1
View File
@@ -12,6 +12,7 @@ import {
} from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import React from 'react';
import { tooltipDelay } from '../settings/ui';
import { TooltipId } from './button-i18n';
import en from './button-i18n.en.json';
@@ -42,7 +43,11 @@ class ActionButton extends React.Component<Props> {
tooltipText += ` (${this.props.keyboardShortcut})`;
}
return (
<Tooltip content={tooltipText} position={Position.BOTTOM}>
<Tooltip
content={tooltipText}
position={Position.BOTTOM}
hoverOpenDelay={tooltipDelay}
>
<Button
ref={this.buttonRef}
intent={Intent.PRIMARY}
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Icon for indicating external links
import { Icon } from '@blueprintjs/core';
import React from 'react';
import './external-link.scss';
class ExternalLinkIcon extends React.Component {
render(): JSX.Element {
return (
<sup className="pb-external-link">
<Icon icon="share" iconSize={12} />
</sup>
);
}
}
export default ExternalLinkIcon;
+163
View File
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// The license dialog
import {
Button,
ButtonGroup,
Callout,
Card,
Classes,
Dialog,
NonIdealState,
} from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import React from 'react';
import { connect } from 'react-redux';
import { Action, Dispatch } from '../actions';
import { closeLicenseDialog } from '../actions/app';
import { select } from '../actions/license';
import { RootState } from '../reducers';
import { LicenseInfo, LicenseList } from '../reducers/license';
import { appName } from '../settings/ui';
import { LicenseStringId } from './license-i18n';
import en from './license-i18n.en.json';
import './license.scss';
type StateProps = {
showLicenseDialog: boolean;
licenseList: LicenseList | null;
licenseInfo: LicenseInfo | null;
};
type DispatchProps = {
onClose: () => void;
onSelectPackage: (info: LicenseInfo) => void;
};
type LicenseDialogProps = StateProps & DispatchProps & WithI18nProps;
class LicenseDialog extends React.Component<LicenseDialogProps> {
render(): JSX.Element {
const infoDiv = React.createRef<HTMLDivElement>();
const {
i18n,
showLicenseDialog,
onClose,
licenseList,
licenseInfo,
onSelectPackage,
} = this.props;
return (
<Dialog
title={i18n.translate(LicenseStringId.Title)}
isOpen={showLicenseDialog}
onClose={() => onClose()}
className="pb-license-dialog"
>
<div className={Classes.DIALOG_BODY}>
<Callout className={Classes.INTENT_PRIMARY} icon="info-sign">
{i18n.translate(LicenseStringId.Description, {
name: appName,
})}
</Callout>
<Callout className="pb-license-browser">
<div className="pb-license-list">
{licenseList === null ? (
// TODO: this should be translated and hooked to
// state indicating if download is in progress
// or there was an actual failure.
<NonIdealState>
Failed to load license data.
</NonIdealState>
) : (
<ButtonGroup
minimal={true}
vertical={true}
alignText="left"
>
{licenseList.map((info) => (
<Button
key={`${info.name}@${info.version}`}
onClick={() => {
infoDiv.current?.scrollTo(0, 0);
onSelectPackage(info);
}}
>
{info.name}
</Button>
))}
</ButtonGroup>
)}
</div>
<div className="pb-license-info" ref={infoDiv}>
{licenseInfo == null ? (
<NonIdealState>
{i18n.translate(LicenseStringId.SelectPackageHelp)}
</NonIdealState>
) : (
<div>
<Card>
<p>
<strong>
{i18n.translate(
LicenseStringId.PackageLabel,
)}
</strong>{' '}
{licenseInfo.name}{' '}
<span className={Classes.TEXT_MUTED}>
v{licenseInfo.version}
</span>
</p>
{licenseInfo.author && (
<p>
<strong>
{' '}
{i18n.translate(
LicenseStringId.AuthorLabel,
)}
</strong>{' '}
{licenseInfo.author}
</p>
)}
<p>
<strong>
{' '}
{i18n.translate(
LicenseStringId.LicenseLabel,
)}
</strong>{' '}
{licenseInfo.license}
</p>
</Card>
<div className="pb-license-text">
<pre>{licenseInfo.licenseText}</pre>
</div>
</div>
)}
</div>
</Callout>
</div>
</Dialog>
);
}
}
const mapStateToProps = (state: RootState): StateProps => ({
showLicenseDialog: state.app.showLicenseDialog,
licenseList: state.license.list,
licenseInfo: state.license.selected,
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onClose: (): Action => dispatch(closeLicenseDialog()),
onSelectPackage: (info): Action => dispatch(select(info)),
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(withI18n({ id: 'license', fallback: en, translations: { en } })(LicenseDialog));
+2
View File
@@ -4,6 +4,7 @@
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import React from 'react';
import { tooltipDelay } from '../settings/ui';
import { TooltipId } from './button-i18n';
import en from './button-i18n.en.json';
@@ -28,6 +29,7 @@ class LinkButton extends React.Component<Props> {
<Tooltip
content={this.props.i18n.translate(this.props.tooltip)}
position={Position.BOTTOM}
hoverOpenDelay={tooltipDelay}
>
<Button
intent={Intent.PRIMARY}
+2
View File
@@ -5,6 +5,7 @@ import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import React from 'react';
import Dropzone, { FileRejection } from 'react-dropzone';
import { tooltipDelay } from '../settings/ui';
import { TooltipId } from './button-i18n';
import en from './button-i18n.en.json';
@@ -81,6 +82,7 @@ class OpenFileButton extends React.Component<Props> {
<Tooltip
content={this.props.i18n.translate(this.props.tooltip)}
position={Position.BOTTOM}
hoverOpenDelay={tooltipDelay}
>
<Button
{...getRootProps()}
+145 -79
View File
@@ -1,23 +1,37 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { Drawer, FormGroup, Position, Switch, Tooltip } from '@blueprintjs/core';
import {
AnchorButton,
Button,
ButtonGroup,
Classes,
Drawer,
FormGroup,
Position,
Switch,
Tooltip,
} from '@blueprintjs/core';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import React from 'react';
import { connect } from 'react-redux';
import { Action, Dispatch } from '../actions';
import { closeSettings } from '../actions/app';
import { closeSettings, openAboutDialog } from '../actions/app';
import { setBoolean } from '../actions/settings';
import { RootState } from '../reducers';
import { SettingId } from '../settings';
import {
pybricksBugReportsUrl,
pybricksGitterUrl,
pybricksSupportUrl,
tooltipDelay,
} from '../settings/ui';
import { SettingId } from '../settings/user';
import { isMacOS } from '../utils/os';
import AboutDialog from './AboutDialog';
import ExternalLinkIcon from './ExternalLinkIcon';
import { SettingsStringId } from './settings-i18n';
import en from './settings-i18n.en.json';
import './settings.scss';
const tooltipDelay = 1000;
type StateProps = {
open: boolean;
showDocs: boolean;
@@ -30,6 +44,7 @@ type DispatchProps = {
onShowDocsChanged: (checked: boolean) => void;
onDarkModeChanged: (checked: boolean) => void;
onFlashCurrentProgramChanged: (checked: boolean) => void;
onAbout: () => void;
};
type SettingsProps = StateProps & DispatchProps & WithI18nProps;
@@ -46,6 +61,7 @@ class SettingsDrawer extends React.PureComponent<SettingsProps> {
onDarkModeChanged,
flashCurrentProgram,
onFlashCurrentProgramChanged,
onAbout,
} = this.props;
return (
<Drawer
@@ -55,83 +71,132 @@ class SettingsDrawer extends React.PureComponent<SettingsProps> {
title={i18n.translate(SettingsStringId.Title)}
onClose={() => onClose()}
>
<div className="pb-settings">
<FormGroup
label={i18n.translate(SettingsStringId.AppearanceTitle)}
helperText={i18n.translate(
SettingsStringId.AppearanceZoomHelp,
{
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
},
)}
>
<Tooltip
content={i18n.translate(
SettingsStringId.AppearanceDocumentationTooltip,
<div className={Classes.DRAWER_BODY}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={i18n.translate(SettingsStringId.AppearanceTitle)}
helperText={i18n.translate(
SettingsStringId.AppearanceZoomHelp,
{
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
},
)}
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDocumentationLabel,
<Tooltip
content={i18n.translate(
SettingsStringId.AppearanceDocumentationTooltip,
)}
large={true}
checked={showDocs}
onChange={(e) =>
onShowDocsChanged(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip>
<Tooltip
content={i18n.translate(
SettingsStringId.AppearanceDarkModeTooltip,
)}
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDocumentationLabel,
)}
checked={showDocs}
onChange={(e) =>
onShowDocsChanged(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip>
<Tooltip
content={i18n.translate(
SettingsStringId.AppearanceDarkModeTooltip,
)}
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDarkModeLabel,
)}
checked={darkMode}
onChange={(e) =>
onDarkModeChanged(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip>
</FormGroup>
<FormGroup
label={i18n.translate(SettingsStringId.FirmwareTitle)}
>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDarkModeLabel,
<Tooltip
content={i18n.translate(
SettingsStringId.FirmwareCurrentProgramTooltip,
)}
large={true}
checked={darkMode}
onChange={(e) =>
onDarkModeChanged(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip>
</FormGroup>
<FormGroup label={i18n.translate(SettingsStringId.FirmwareTitle)}>
<Tooltip
content={i18n.translate(
SettingsStringId.FirmwareCurrentProgramTooltip,
)}
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.FirmwareCurrentProgramLabel,
)}
large={true}
checked={flashCurrentProgram}
onChange={(e) =>
onFlashCurrentProgramChanged(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip>
</FormGroup>
boundary="window"
position={Position.LEFT}
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
SettingsStringId.FirmwareCurrentProgramLabel,
)}
checked={flashCurrentProgram}
onChange={(e) =>
onFlashCurrentProgramChanged(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip>
</FormGroup>
<FormGroup label={i18n.translate(SettingsStringId.HelpTitle)}>
<ButtonGroup
minimal={true}
vertical={true}
alignText="left"
>
<AnchorButton
icon="help"
href={pybricksSupportUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpSupportLabel)}
&nbsp;
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="chat"
href={pybricksGitterUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpChatLabel)}
&nbsp;
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="virus"
href={pybricksBugReportsUrl}
target="blank_"
>
{i18n.translate(SettingsStringId.HelpBugsLabel)}
&nbsp;
<ExternalLinkIcon />
</AnchorButton>
<Button
icon="info-sign"
onClick={() => {
onAbout();
return true;
}}
>
{i18n.translate(SettingsStringId.HelpAboutLabel)}
</Button>
<AboutDialog />
</ButtonGroup>
</FormGroup>
</div>
</div>
</Drawer>
);
@@ -153,6 +218,7 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
dispatch(setBoolean(SettingId.DarkMode, checked)),
onFlashCurrentProgramChanged: (checked): Action =>
dispatch(setBoolean(SettingId.FlashCurrentProgram, checked)),
onAbout: (): Action => dispatch(openAboutDialog()),
});
export default connect(
+7
View File
@@ -0,0 +1,7 @@
{
"about": {
"description": "A simple web app for programming LEGO® Powered Up smart hubs using Pybricks MicroPython.",
"licenseButton": { "label": "Open Source Licenses" },
"websiteButton": { "label": "Pybricks Website" }
}
}
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { lookup } from '../../test';
import { AboutStringId } from './about-i18n';
import en from './about-i18n.en.json';
describe('Ensure .json file has matches for AboutStringId', () => {
test.each(Object.values(AboutStringId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+10
View File
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// About dialog translation keys.
export enum AboutStringId {
Description = 'about.description',
LicenseButtonLabel = 'about.licenseButton.label',
WebsiteButtonLabel = 'about.websiteButton.label',
}
+7
View File
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
.pb-about-icon {
float: left;
margin-right: 10px;
}
+2 -2
View File
@@ -5,7 +5,7 @@
@import '../variables.scss';
.bp3-dark .splitter-layout > .layout-splitter {
.#{$ns}-dark .splitter-layout > .layout-splitter {
// make layout splitter match app color scheme
background-color: $pt-dark-app-background-color;
}
@@ -15,7 +15,7 @@
background-color: $pt-app-background-color;
}
.bp3-dark .terminal-padding {
.#{$ns}-dark .terminal-padding {
background-color: black;
}
+1 -1
View File
@@ -5,7 +5,7 @@
@import '../variables.scss';
.bp3-dark .ace_gutter {
.#{$ns}-dark .ace_gutter {
// make ace editor match app backgound color
background-color: $pt-dark-app-background-color;
}
+9
View File
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
@import '../variables.scss';
// override some button magic in case this is used in a button
.#{$ns}-button .pb-external-link .#{$ns}-icon:first-child:last-child {
margin: 0;
}
+12
View File
@@ -0,0 +1,12 @@
{
"license": {
"title": "Open Source Licenses",
"description": "{name} is built on open source software. By using {name} you are agreeing to the terms and conditions of all of the included software licenses.",
"packageLabel": "Package:",
"authorLabel": "Author:",
"licenseLabel": "License:",
"help": {
"selectPackage": "Select a package to view the license."
}
}
}
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { lookup } from '../../test';
import { LicenseStringId } from './license-i18n';
import en from './license-i18n.en.json';
describe('Ensure .json file has matches for LicenseStringId', () => {
test.each(Object.values(LicenseStringId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+13
View File
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// License dialog translation keys.
export enum LicenseStringId {
Title = 'license.title',
Description = 'license.description',
PackageLabel = 'license.packageLabel',
AuthorLabel = 'license.authorLabel',
LicenseLabel = 'license.licenseLabel',
SelectPackageHelp = 'license.help.selectPackage',
}
+33
View File
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
// Custom styling for the LicenseDialog control.
@import '../variables.scss';
.pb-license-dialog {
width: 1200px;
}
.pb-license-browser {
margin-top: 16px;
display: flex;
height: 600px;
}
.pb-license-list {
width: 25%;
flex-flow: column;
overflow: auto;
}
.pb-license-info {
width: 75%;
flex-flow: column;
padding: 0px 16px;
overflow: auto;
}
.pb-license-text {
padding: 0px 16px;
}
+16 -1
View File
@@ -1,6 +1,6 @@
{
"settings": {
"title": "Settings",
"title": "Settings & Help",
"appearance": {
"title": "Appearance",
"documentation": {
@@ -21,6 +21,21 @@
"label": "Include current program",
"tooltip": "Select to include your program when installing the firmware."
}
},
"help": {
"title": "Help",
"support": {
"label": "Support"
},
"chat": {
"label": "Chat"
},
"bugs": {
"label": "Bug Reports"
},
"about": {
"label": "About"
}
}
}
}
+5
View File
@@ -14,4 +14,9 @@ export enum SettingsStringId {
FirmwareTitle = 'settings.firmware.title',
FirmwareCurrentProgramLabel = 'settings.firmware.flash-current-program.label',
FirmwareCurrentProgramTooltip = 'settings.firmware.flash-current-program.tooltip',
HelpTitle = 'settings.help.title',
HelpSupportLabel = 'settings.help.support.label',
HelpChatLabel = 'settings.help.chat.label',
HelpBugsLabel = 'settings.help.bugs.label',
HelpAboutLabel = 'settings.help.about.label',
}
-17
View File
@@ -1,17 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Custom styling for the Settings* controls.
.pb-settings .bp3-form-group {
margin: 25px;
}
.pb-settings .bp3-label {
font-size: 18px;
font-weight: bolder;
}
.pb-settings .bp3-form-helper-text {
font-size: 14px;
}
+1 -1
View File
@@ -20,7 +20,7 @@
margin-left: 10px;
}
.bp3-progress-bar.status-bar-item {
.#{$ns}-progress-bar.status-bar-item {
// override progress bar default gray1 backgound
background-color: $pt-app-background-color;
}
+3 -1
View File
@@ -3,7 +3,9 @@
// Custom styling for the Toolbar control.
.bp3-navbar-divider {
@import '../variables.scss';
.#{$ns}-navbar-divider {
// don't draw vertical line since we are just using button groups
border-left: unset;
}
+39
View File
@@ -36,3 +36,42 @@ body {
.no-box-shadow {
box-shadow: unset !important;
}
// global style tweaks
.#{$ns}-form-group > .#{$ns}-label {
font-weight: bolder;
}
::-webkit-scrollbar {
width: 16px;
}
.#{$ns}-dark ::-webkit-scrollbar-track {
background: $pt-dark-app-background-color;
}
::-webkit-scrollbar-track {
background: $pt-app-background-color;
}
.#{$ns}-dark ::-webkit-scrollbar-thumb {
border-color: $pt-dark-app-background-color;
background: $pt-dark-icon-color;
}
::-webkit-scrollbar-thumb {
border-width: 3px;
border-style: solid;
border-radius: 8px;
border-color: $pt-app-background-color;
background: $pt-icon-color;
}
.#{$ns}-dark ::-webkit-scrollbar-thumb:hover {
background: $pt-dark-icon-color-hover;
}
::-webkit-scrollbar-thumb:hover {
background: $pt-icon-color-hover;
}
+6 -6
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
import { ResizeSensor } from '@blueprintjs/core';
import { Classes, ResizeSensor } from '@blueprintjs/core';
import { I18nContext, I18nManager } from '@shopify/react-i18n';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -10,7 +10,7 @@ import { applyMiddleware, createStore } from 'redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import './index.scss';
import { success, update } from './actions/service-worker';
import { didSucceed, didUpdate } from './actions/service-worker';
import App from './components/App';
import NotificationStack from './components/NotificationStack';
import rootReducer from './reducers';
@@ -38,9 +38,9 @@ store.subscribe(() => {
const newDarkMode = store.getState().settings.darkMode;
if (newDarkMode !== oldDarkMode) {
if (newDarkMode) {
document.body.classList.add('bp3-dark');
document.body.classList.add(Classes.DARK);
} else {
document.body.classList.remove('bp3-dark');
document.body.classList.remove(Classes.DARK);
}
oldDarkMode = newDarkMode;
}
@@ -76,8 +76,8 @@ ReactDOM.render(
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.register({
onUpdate: (r) => store.dispatch(update(r)),
onSuccess: (r) => store.dispatch(success(r)),
onUpdate: (r) => store.dispatch(didUpdate(r)),
onSuccess: (r) => store.dispatch(didSucceed(r)),
});
// If you want to start measuring performance in your app, pass a function
+28 -4
View File
@@ -7,6 +7,12 @@ import { Reducer, combineReducers } from 'redux';
import { Action } from '../actions';
import { AppActionType } from '../actions/app';
export interface AppState {
readonly showSettings: boolean;
readonly showAboutDialog: boolean;
readonly showLicenseDialog: boolean;
}
const showSettings: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case AppActionType.OpenSettings:
@@ -18,8 +24,26 @@ const showSettings: Reducer<boolean, Action> = (state = false, action) => {
}
};
export interface AppState {
readonly showSettings: boolean;
}
const showAboutDialog: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case AppActionType.OpenAboutDialog:
return true;
case AppActionType.CloseAboutDialog:
return false;
default:
return state;
}
};
export default combineReducers({ showSettings });
const showLicenseDialog: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case AppActionType.OpenLicenseDialog:
return true;
case AppActionType.CloseLicenseDialog:
return false;
default:
return state;
}
};
export default combineReducers({ showSettings, showAboutDialog, showLicenseDialog });
+3
View File
@@ -7,6 +7,7 @@ import ble, { BleState } from './ble';
import bootloader, { BootloaderState } from './bootloader';
import editor, { EditorState } from './editor';
import hub, { HubState } from './hub';
import license, { LicenseState } from './license';
import notification, { NotificationState } from './notification';
import settings, { SettingsState } from './settings';
import status, { StatusState } from './status';
@@ -21,6 +22,7 @@ export interface RootState {
readonly ble: BleState;
readonly editor: EditorState;
readonly hub: HubState;
readonly license: LicenseState;
readonly notification: NotificationState;
readonly settings: SettingsState;
readonly status: StatusState;
@@ -33,6 +35,7 @@ export default combineReducers({
ble,
editor,
hub,
license,
notification,
settings,
status,
+41
View File
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { Reducer, combineReducers } from 'redux';
import { Action } from '../actions';
import { LicenseActionType } from '../actions/license';
export interface LicenseInfo {
readonly name: string;
readonly version: string;
readonly author: string | undefined;
readonly license: string;
readonly licenseText: string;
}
export type LicenseList = LicenseInfo[];
export interface LicenseState {
readonly list: LicenseList | null;
readonly selected: LicenseInfo | null;
}
const list: Reducer<LicenseList | null, Action> = (state = null, action) => {
switch (action.type) {
case LicenseActionType.DidFetchList:
return action.list;
default:
return state;
}
};
const selected: Reducer<LicenseInfo | null, Action> = (state = null, action) => {
switch (action.type) {
case LicenseActionType.Select:
return action.info;
default:
return state;
}
};
export default combineReducers({ list, selected });
+2 -2
View File
@@ -147,7 +147,7 @@ const list: Reducer<NotificationList, Action> = (state = [], action) => {
];
case NotificationActionType.Remove:
return state.filter((e) => e.id !== action.id);
case ServiceWorkerActionType.Update:
case ServiceWorkerActionType.DidUpdate:
return append(
state,
Level.Info,
@@ -155,7 +155,7 @@ const list: Reducer<NotificationList, Action> = (state = [], action) => {
undefined,
'https://github.com/pybricks/pybricks-code/issues/102',
);
case ServiceWorkerActionType.Success:
case ServiceWorkerActionType.DidSucceed:
return append(state, Level.Info, MessageId.ServiceWorkerSuccess);
default:
return state;
+1 -1
View File
@@ -4,7 +4,7 @@
import { Reducer, combineReducers } from 'redux';
import { Action } from '../actions';
import { SettingsActionType } from '../actions/settings';
import { SettingId, getDefaultBooleanValue } from '../settings';
import { SettingId, getDefaultBooleanValue } from '../settings/user';
export interface SettingsState {
readonly darkMode: boolean;
+15
View File
@@ -7,6 +7,7 @@ import {
didFailToConnect as bleDidFailToConnect,
} from '../actions/ble';
import { didFailToWrite } from '../actions/ble-uart';
import { didFailToFetchList } from '../actions/license';
import {
BootloaderConnectionFailureReason,
didError,
@@ -68,3 +69,17 @@ test('bootloaderDidError', async () => {
await saga.end();
});
test('licenseDidFailToFetch', async () => {
const saga = new AsyncSaga(errorLog);
console.error = jest.fn();
saga.put(
didFailToFetchList(
new Response(undefined, { status: 404, statusText: 'not found' }),
),
);
expect(console.error).toHaveBeenCalledTimes(1);
await saga.end();
});
+6
View File
@@ -8,6 +8,7 @@ import {
BleDeviceFailToConnectReasonType,
} from '../actions/ble';
import { BleUartActionType, BleUartDidFailToWriteAction } from '../actions/ble-uart';
import { LicenseActionType, LicenseDidFailToFetchListAction } from '../actions/license';
import {
BootloaderConnectionActionType,
BootloaderConnectionDidErrorAction,
@@ -39,6 +40,10 @@ function bootloaderDidError(action: BootloaderConnectionDidErrorAction): void {
console.error(action.err);
}
function licenseDidFailToFetch(action: LicenseDidFailToFetchListAction): void {
console.error(`Failed to fetch licenses: ${action.reason.statusText}`);
}
export default function* (): Generator {
yield takeEvery(BleDeviceActionType.DidFailToConnect, bleDeviceDidFailToConnect);
yield takeEvery(BleUartActionType.DidFailToWrite, bleDataDidFailToWrite);
@@ -47,4 +52,5 @@ export default function* (): Generator {
bootloaderDidFailToConnect,
);
yield takeEvery(BootloaderConnectionActionType.DidError, bootloaderDidError);
yield takeEvery(LicenseActionType.DidFailToFetchList, licenseDidFailToFetch);
}
+2
View File
@@ -8,6 +8,7 @@ import editor from './editor';
import errorLog from './error-log';
import flashFirmware from './flash-firmware';
import hub from './hub';
import license from './license';
import lwp3BootloaderBle from './lwp3-bootloader-ble';
import lwp3BootloaderProtocol from './lwp3-bootloader-protocol';
import mpy from './mpy';
@@ -24,6 +25,7 @@ export default function* (): Generator {
errorLog(),
flashFirmware(),
hub(),
license(),
mpy(),
settings(),
terminal(),
+67
View File
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
// Tests for license sagas.
import { AsyncSaga, delay } from '../../test';
import { openLicenseDialog } from '../actions/app';
import { didFailToFetchList, didFetchList } from '../actions/license';
import { LicenseList, LicenseState } from '../reducers/license';
import license from './license';
afterAll(() => {
jest.restoreAllMocks();
});
describe('fetchLicenses', () => {
test('first call', async () => {
const testLicenseList: LicenseList = [];
const saga = new AsyncSaga(license);
jest.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(JSON.stringify(testLicenseList)),
);
// initially, license list starts as null, so fetch is called to get
// the list
saga.setState({ license: { list: null } as LicenseState });
saga.put(openLicenseDialog());
const action = await saga.take();
expect(action).toEqual(didFetchList(testLicenseList));
await saga.end();
});
test('second call', async () => {
const testLicenseList: LicenseList = [];
const saga = new AsyncSaga(license);
jest.spyOn(globalThis, 'fetch').mockRejectedValue(
'fetch () should not have been called',
);
// after we have the list, we don't fetch it again since it will
// always be the same list
saga.setState({ license: { list: testLicenseList } as LicenseState });
saga.put(openLicenseDialog());
// have to yield to be sure fetch call would have taken place on error
await delay(0);
await saga.end();
});
test('failed fetch', async () => {
const failResponse = new Response(undefined, { status: 404 });
const saga = new AsyncSaga(license);
jest.spyOn(globalThis, 'fetch').mockResolvedValue(failResponse);
saga.setState({ license: { list: null } as LicenseState });
saga.put(openLicenseDialog());
const action = await saga.take();
expect(action).toEqual(didFailToFetchList(failResponse));
await saga.end();
});
});
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { call, put, select, takeEvery } from 'redux-saga/effects';
import { AppActionType } from '../actions/app';
import { didFailToFetchList, didFetchList } from '../actions/license';
import { RootState } from '../reducers';
import { LicenseList } from '../reducers/license';
function* fetchLicenses(): Generator {
const licenses = (yield select(
(s: RootState) => s.license.list,
)) as LicenseList | null;
// if we already have license list, nothing to do
if (licenses !== null) {
return;
}
const response = (yield call(() => fetch('static/oss-licenses.json'))) as Response;
if (!response.ok || response.body === null) {
yield put(didFailToFetchList(response));
return;
}
const list = (yield call(() => response.json())) as LicenseList;
yield put(didFetchList(list));
}
export default function* (): Generator {
yield takeEvery(AppActionType.OpenLicenseDialog, fetchLicenses);
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { AsyncSaga } from '../../test';
import { startup } from '../actions/app';
import { didBooleanChange, didFailToSetBoolean, setBoolean } from '../actions/settings';
import { SettingsState } from '../reducers/settings';
import { SettingId } from '../settings';
import { SettingId } from '../settings/user';
import settings from './settings';
afterAll(() => {
+1 -1
View File
@@ -15,7 +15,7 @@ import {
didFailToSetBoolean,
} from '../actions/settings';
import { RootState } from '../reducers';
import { SettingId, getDefaultBooleanValue } from '../settings';
import { SettingId, getDefaultBooleanValue } from '../settings/user';
function stringToBoolean(value: string): boolean {
return value.toLowerCase().match(/(true|yes|1)/) !== null;
+30
View File
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Definitions for compile-time UI settings.
/** Time in milliseconds before showing tooltip popover. */
export const tooltipDelay = 1000;
/** Official name of the app. */
export const appName = 'Pybricks Code';
/** URL to main Pybricks website. */
export const pybricksWebsiteUrl = 'https://pybricks.com';
/** URL to Pybricks support site. */
export const pybricksSupportUrl = 'https://github.com/pybricks/support/discussions';
/** URL to Pybricks bug report site. */
export const pybricksBugReportsUrl = 'https://github.com/pybricks/support/issues';
/** URL for Pybricks community chat on Gitter */
export const pybricksGitterUrl = 'https://gitter.im/pybricks/community';
/** Pybricks copyright statement. */
export const pybricksCopyright = 'Copyright (c) 2020-2021 The Pybricks Authors';
// https://www.lego.com/en-us/legal/notices-and-policies/fair-play/
/** LEGO "fair play" disclaimer */
export const legoDisclaimer =
'LEGO® is a trademark of the LEGO Group of companies which does not sponsor, authorize or endorse this site.';
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
// Definitions for user settings.
// Definitions for user selectable settings.
import { prefersDarkMode } from '../utils/os';
export enum SettingId {
ShowDocs = 'showDocs',
@@ -14,6 +16,7 @@ export function getDefaultBooleanValue(id: SettingId): boolean {
case SettingId.ShowDocs:
return window.innerWidth >= 1024;
case SettingId.DarkMode:
return prefersDarkMode();
case SettingId.FlashCurrentProgram:
return false;
// istanbul ignore next: it is a programmer error if we hit this
+20 -1
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { isMacOS } from './os';
import { isMacOS, prefersDarkMode } from './os';
describe('isMacOS', () => {
test('is true', () => {
@@ -13,3 +13,22 @@ describe('isMacOS', () => {
expect(isMacOS()).toBeFalsy();
});
});
describe('prefersDarkMode', () => {
test('is true', () => {
window.matchMedia = jest.fn().mockReturnValue({
matches: true,
} as MediaQueryList);
expect(prefersDarkMode()).toBeTruthy();
});
test('is false', () => {
// @ts-expect-error 2790
delete window.matchMedia;
expect(prefersDarkMode()).toBeFalsy();
window.matchMedia = jest.fn().mockReturnValue({
matches: false,
} as MediaQueryList);
expect(prefersDarkMode()).toBeFalsy();
});
});
+16
View File
@@ -3,6 +3,22 @@
// Utility functions for dealing with operating systems.
/**
* Tests if we are running on macOS.
* @returns `true` if running on macOS, otherwise `false`.
*/
export function isMacOS(): boolean {
return /mac/i.test(navigator.platform);
}
/**
* Tests if the OS is set to dark mode.
* @returns: `true` if dark mode should be preferred, otherwise `false`.
*/
export function prefersDarkMode(): boolean {
if (!window.matchMedia) {
return false;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
+4
View File
@@ -6,6 +6,10 @@
// override variables here
// See https://blueprintjs.com/docs/#core/variables
$pt-font-size: $pt-grid-size * 1.6;
$pt-font-size-large: $pt-grid-size * 1.8;
$pt-font-size-small: $pt-grid-size * 1.4;
$pt-navbar-height: 72px;
$pb-status-bar-height: 3vh;
+42 -2
View File
@@ -2101,6 +2101,15 @@
"@types/source-list-map" "*"
source-map "^0.7.3"
"@types/webpack-sources@^0.1.5":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.8.tgz#078d75410435993ec8a0a2855e88706f3f751f81"
integrity sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==
dependencies:
"@types/node" "*"
"@types/source-list-map" "*"
source-map "^0.6.1"
"@types/webpack@^4.41.8":
version "4.41.25"
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.25.tgz#4d3b5aecc4e44117b376280fbfd2dc36697968c4"
@@ -2637,7 +2646,7 @@ arr-union@^3.1.0:
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
array-find-index@^1.0.1:
array-find-index@^1.0.1, array-find-index@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
@@ -7601,6 +7610,14 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
license-webpack-plugin@^2.3.11:
version "2.3.11"
resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.11.tgz#0d93188a31fce350a44c86212badbaf33dcd29d8"
integrity sha512-0iVGoX5vx0WDy8dmwTTpOOMYiGqILyUbDeVMFH52AjgBlS58lHwOlFMSoqg5nY8Kxl6+FRKyUZY/UdlQaOyqDw==
dependencies:
"@types/webpack-sources" "^0.1.5"
webpack-sources "^1.2.0"
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
@@ -11144,6 +11161,15 @@ sourcemap-codec@^1.4.4:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
spdx-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/spdx-compare/-/spdx-compare-1.0.0.tgz#2c55f117362078d7409e6d7b08ce70a857cd3ed7"
integrity sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==
dependencies:
array-find-index "^1.0.2"
spdx-expression-parse "^3.0.0"
spdx-ranges "^2.0.0"
spdx-correct@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
@@ -11170,6 +11196,20 @@ spdx-license-ids@^3.0.0:
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65"
integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==
spdx-ranges@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/spdx-ranges/-/spdx-ranges-2.1.1.tgz#87573927ba51e92b3f4550ab60bfc83dd07bac20"
integrity sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==
spdx-satisfies@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/spdx-satisfies/-/spdx-satisfies-5.0.0.tgz#d740b8f14caeada36fb307629dee87146970a256"
integrity sha512-/hGhwh20BeGmkA+P/lm06RvXD94JduwNxtx/oX3B5ClPt1/u/m5MCaDNo1tV3Y9laLkQr/NRde63b9lLMhlNfw==
dependencies:
spdx-compare "^1.0.0"
spdx-expression-parse "^3.0.0"
spdx-ranges "^2.0.0"
spdy-transport@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
@@ -12418,7 +12458,7 @@ webpack-merge@^4.2.2:
dependencies:
lodash "^4.17.15"
webpack-sources@^1.1.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==