Add Activities

This adds a new Activities tab list and view similar to VS code. The
Explorer and Settings are migrated to this view.
This commit is contained in:
David Lechner
2022-05-12 12:33:21 -05:00
parent 65623f801d
commit edd8ebfd64
22 changed files with 673 additions and 468 deletions
@@ -5,7 +5,7 @@ import { cleanup, getByLabelText, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { testRender } from '../../test';
import SettingsDrawer from './SettingsDrawer';
import Settings from './Settings';
afterEach(() => {
cleanup();
@@ -14,9 +14,7 @@ afterEach(() => {
describe('showDocs setting switch', () => {
it('should toggle setting', async () => {
const [settings] = testRender(
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
);
const [settings] = testRender(<Settings />);
const showDocs = settings.getByLabelText('Documentation');
expect(showDocs).toBeChecked();
@@ -28,9 +26,7 @@ describe('showDocs setting switch', () => {
describe('darkMode setting switch', () => {
it('should toggle setting', async () => {
const [settings] = testRender(
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
);
const [settings] = testRender(<Settings />);
const darkMode = settings.getByLabelText('Dark mode');
expect(darkMode).not.toBeChecked();
@@ -45,11 +41,10 @@ describe('darkMode setting switch', () => {
expect(localStorage.getItem('usehooks-ts-ternary-dark-mode')).toBe('"light"');
});
});
describe('flashCurrentProgram setting switch', () => {
it('should toggle the setting', () => {
const [settings] = testRender(
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
);
const [settings] = testRender(<Settings />);
expect(localStorage.getItem('setting.flashCurrentProgram')).toBe(null);
@@ -66,9 +61,7 @@ describe('hubName setting', () => {
// old settings did not use json format, so lack quotes
localStorage.setItem('setting.hubName', 'old name');
const [settings] = testRender(
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
);
const [settings] = testRender(<Settings />);
const textBox = settings.getByLabelText('Hub name');
@@ -76,9 +69,7 @@ describe('hubName setting', () => {
});
it('should update the setting', () => {
const [settings] = testRender(
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
);
const [settings] = testRender(<Settings />);
expect(localStorage.getItem('setting.hubName')).toBe(null);
@@ -91,9 +82,7 @@ describe('hubName setting', () => {
describe('about dialog', () => {
it('should open the dialog when the button is clicked', async () => {
const [settings] = testRender(
<SettingsDrawer isOpen={true} onClose={() => undefined} />,
);
const [settings] = testRender(<Settings />);
const appName = process.env.REACT_APP_NAME;
+259
View File
@@ -0,0 +1,259 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2022 The Pybricks Authors
import {
AnchorButton,
Button,
ButtonGroup,
ControlGroup,
FormGroup,
Icon,
InputGroup,
Intent,
Label,
Switch,
} from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useTernaryDarkMode } from 'usehooks-ts';
import AboutDialog from '../about/AboutDialog';
import { appCheckForUpdate, appReload, appShowInstallPrompt } from '../app/actions';
import {
pybricksBugReportsUrl,
pybricksGitterUrl,
pybricksProjectsUrl,
pybricksSupportUrl,
tooltipDelay,
} from '../app/constants';
import { pseudolocalize } from '../i18n';
import { useSelector } from '../reducers';
import ExternalLinkIcon from '../utils/ExternalLinkIcon';
import { isMacOS } from '../utils/os';
import {
useSettingFlashCurrentProgram,
useSettingHubName,
useSettingIsShowDocsEnabled,
} from './hooks';
import { I18nId } from './i18n';
import './settings.scss';
const Settings: React.VoidFunctionComponent = () => {
const { isSettingShowDocsEnabled, setIsSettingShowDocsEnabled } =
useSettingIsShowDocsEnabled();
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
const { isDarkMode, setTernaryDarkMode } = useTernaryDarkMode();
const [isFlashCurrentProgramEnabled, setIsFlashCurrentProgramEnabled] =
useSettingFlashCurrentProgram();
const isServiceWorkerRegistered = useSelector(
(s) => s.app.isServiceWorkerRegistered,
);
const checkingForUpdate = useSelector((s) => s.app.checkingForUpdate);
const updateAvailable = useSelector((s) => s.app.updateAvailable);
const hasUnresolvedInstallPrompt = useSelector(
(s) => s.app.hasUnresolvedInstallPrompt,
);
const promptingInstall = useSelector((s) => s.app.promptingInstall);
const readyForOfflineUse = useSelector((s) => s.app.readyForOfflineUse);
const { hubName, isHubNameValid, setHubName } = useSettingHubName();
const dispatch = useDispatch();
// istanbul ignore next: babel-loader rewrites this line
const [i18n] = useI18n();
return (
<div aria-label={i18n.translate(I18nId.Title)}>
<FormGroup
label={i18n.translate(I18nId.AppearanceTitle)}
helperText={i18n.translate(I18nId.AppearanceZoomHelp, {
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
})}
>
<Tooltip2
content={i18n.translate(I18nId.AppearanceDocumentationTooltip)}
rootBoundary="document"
placement="left"
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(I18nId.AppearanceDocumentationLabel)}
checked={isSettingShowDocsEnabled}
onChange={(e) =>
setIsSettingShowDocsEnabled(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip2>
<Tooltip2
content={i18n.translate(I18nId.AppearanceDarkModeTooltip)}
rootBoundary="document"
placement="left"
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(I18nId.AppearanceDarkModeLabel)}
checked={isDarkMode}
onChange={(e) =>
setTernaryDarkMode(
(e.target as HTMLInputElement).checked
? 'dark'
: 'light',
)
}
/>
</Tooltip2>
</FormGroup>
<FormGroup label={i18n.translate(I18nId.FirmwareTitle)}>
<Tooltip2
content={i18n.translate(I18nId.FirmwareCurrentProgramTooltip)}
rootBoundary="document"
placement="left"
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(I18nId.FirmwareCurrentProgramLabel)}
checked={isFlashCurrentProgramEnabled}
onChange={(e) =>
setIsFlashCurrentProgramEnabled(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip2>
<ControlGroup vertical={true}>
<Tooltip2
content={i18n.translate(I18nId.FirmwareHubNameTooltip)}
rootBoundary="document"
placement="left"
targetTagName="div"
hoverOpenDelay={tooltipDelay}
openOnTargetFocus={false}
>
<Label htmlFor="hub-name-input">
{i18n.translate(I18nId.FirmwareHubNameLabel)}
</Label>
</Tooltip2>
<InputGroup
id="hub-name-input"
value={hubName}
onChange={(e) => setHubName(e.currentTarget.value)}
onMouseOver={(e) => e.preventDefault()}
className="pb-hub-name-input"
intent={isHubNameValid ? Intent.NONE : Intent.DANGER}
placeholder="Pybricks Hub"
rightElement={
isHubNameValid ? undefined : (
<Tooltip2
content={i18n.translate(
I18nId.FirmwareHubNameErrorTooltip,
)}
rootBoundary="document"
placement="bottom"
targetTagName="div"
>
<Icon
icon="error"
intent={Intent.DANGER}
itemType="div"
/>
</Tooltip2>
)
}
/>
</ControlGroup>
</FormGroup>
<FormGroup label={i18n.translate(I18nId.HelpTitle)}>
<ButtonGroup minimal={true} vertical={true} alignText="left">
<AnchorButton
icon="lightbulb"
href={pybricksProjectsUrl}
target="blank_"
>
{i18n.translate(I18nId.HelpProjectsLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton icon="help" href={pybricksSupportUrl} target="blank_">
{i18n.translate(I18nId.HelpSupportLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton icon="chat" href={pybricksGitterUrl} target="blank_">
{i18n.translate(I18nId.HelpChatLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="virus"
href={pybricksBugReportsUrl}
target="blank_"
>
{i18n.translate(I18nId.HelpBugsLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AboutDialog
isOpen={isAboutDialogOpen}
onClose={() => setIsAboutDialogOpen(false)}
/>
</ButtonGroup>
</FormGroup>
<FormGroup
label={i18n.translate(I18nId.AppTitle)}
helperText={
readyForOfflineUse && i18n.translate(I18nId.AppOfflineUseHelp)
}
>
<ButtonGroup minimal={true} vertical={true} alignText="left">
{hasUnresolvedInstallPrompt && (
<Button
icon="add"
onClick={() => dispatch(appShowInstallPrompt())}
loading={promptingInstall}
>
{i18n.translate(I18nId.AppInstallLabel)}
</Button>
)}
{isServiceWorkerRegistered && !updateAvailable && (
<Button
icon="refresh"
onClick={() => dispatch(appCheckForUpdate())}
loading={checkingForUpdate}
>
{i18n.translate(I18nId.AppCheckForUpdateLabel)}
</Button>
)}
{isServiceWorkerRegistered && updateAvailable && (
<Button icon="refresh" onClick={() => dispatch(appReload())}>
{i18n.translate(I18nId.AppRestartLabel)}
</Button>
)}
<Button
icon="info-sign"
onClick={() => {
setIsAboutDialogOpen(true);
return true;
}}
>
{i18n.translate(I18nId.AppAboutLabel)}
</Button>
</ButtonGroup>
</FormGroup>
{process.env.NODE_ENV === 'development' && (
<FormGroup label="Developer">
<Switch
checked={i18n.pseudolocalize !== false}
onChange={() => pseudolocalize(!i18n.pseudolocalize)}
label="Pseudolocalize"
/>
</FormGroup>
)}
</div>
);
};
export default Settings;
-317
View File
@@ -1,317 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2022 The Pybricks Authors
import {
AnchorButton,
Button,
ButtonGroup,
Classes,
ControlGroup,
Drawer,
DrawerSize,
FormGroup,
Icon,
InputGroup,
Intent,
Label,
Switch,
} from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useTernaryDarkMode } from 'usehooks-ts';
import AboutDialog from '../about/AboutDialog';
import { appCheckForUpdate, appReload, appShowInstallPrompt } from '../app/actions';
import {
pybricksBugReportsUrl,
pybricksGitterUrl,
pybricksProjectsUrl,
pybricksSupportUrl,
tooltipDelay,
} from '../app/constants';
import { pseudolocalize } from '../i18n';
import { useSelector } from '../reducers';
import ExternalLinkIcon from '../utils/ExternalLinkIcon';
import { isMacOS } from '../utils/os';
import {
useSettingFlashCurrentProgram,
useSettingHubName,
useSettingIsShowDocsEnabled,
} from './hooks';
import { I18nId } from './i18n';
import './settings.scss';
type SettingsProps = {
isOpen: boolean;
onClose(): void;
};
const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
isOpen,
onClose,
}) => {
const { isSettingShowDocsEnabled, setIsSettingShowDocsEnabled } =
useSettingIsShowDocsEnabled();
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
const { isDarkMode, setTernaryDarkMode } = useTernaryDarkMode();
const [isFlashCurrentProgramEnabled, setIsFlashCurrentProgramEnabled] =
useSettingFlashCurrentProgram();
const isServiceWorkerRegistered = useSelector(
(s) => s.app.isServiceWorkerRegistered,
);
const checkingForUpdate = useSelector((s) => s.app.checkingForUpdate);
const updateAvailable = useSelector((s) => s.app.updateAvailable);
const hasUnresolvedInstallPrompt = useSelector(
(s) => s.app.hasUnresolvedInstallPrompt,
);
const promptingInstall = useSelector((s) => s.app.promptingInstall);
const readyForOfflineUse = useSelector((s) => s.app.readyForOfflineUse);
const { hubName, isHubNameValid, setHubName } = useSettingHubName();
const dispatch = useDispatch();
// istanbul ignore next: babel-loader rewrites this line
const [i18n] = useI18n();
// HACK: set additional attributes that are not supported via Drawer props
const handleDrawerOpening = useCallback<(node: HTMLElement) => void>((n) => {
n.setAttribute('role', 'dialog');
n.setAttribute('aria-modal', 'true');
n.setAttribute('aria-labelledby', 'settings-drawer-dialog-title');
}, []);
return (
<Drawer
isOpen={isOpen}
icon="cog"
size={DrawerSize.SMALL}
title={
<span id="settings-drawer-dialog-title">
{i18n.translate(I18nId.Title)}
</span>
}
onOpening={handleDrawerOpening}
onClose={onClose}
// work around https://github.com/palantir/blueprint/issues/5169
shouldReturnFocusOnClose={false}
>
<div className={Classes.DRAWER_BODY}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={i18n.translate(I18nId.AppearanceTitle)}
helperText={i18n.translate(I18nId.AppearanceZoomHelp, {
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
})}
>
<Tooltip2
content={i18n.translate(
I18nId.AppearanceDocumentationTooltip,
)}
rootBoundary="document"
placement="left"
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
I18nId.AppearanceDocumentationLabel,
)}
checked={isSettingShowDocsEnabled}
onChange={(e) =>
setIsSettingShowDocsEnabled(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip2>
<Tooltip2
content={i18n.translate(I18nId.AppearanceDarkModeTooltip)}
rootBoundary="document"
placement="left"
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(I18nId.AppearanceDarkModeLabel)}
checked={isDarkMode}
onChange={(e) =>
setTernaryDarkMode(
(e.target as HTMLInputElement).checked
? 'dark'
: 'light',
)
}
/>
</Tooltip2>
</FormGroup>
<FormGroup label={i18n.translate(I18nId.FirmwareTitle)}>
<Tooltip2
content={i18n.translate(
I18nId.FirmwareCurrentProgramTooltip,
)}
rootBoundary="document"
placement="left"
targetTagName="div"
hoverOpenDelay={tooltipDelay}
>
<Switch
label={i18n.translate(
I18nId.FirmwareCurrentProgramLabel,
)}
checked={isFlashCurrentProgramEnabled}
onChange={(e) =>
setIsFlashCurrentProgramEnabled(
(e.target as HTMLInputElement).checked,
)
}
/>
</Tooltip2>
<ControlGroup>
<Tooltip2
content={i18n.translate(I18nId.FirmwareHubNameTooltip)}
rootBoundary="document"
placement="left"
targetTagName="div"
hoverOpenDelay={tooltipDelay}
openOnTargetFocus={false}
>
<Label
className={Classes.INLINE}
htmlFor="hub-name-input"
>
{i18n.translate(I18nId.FirmwareHubNameLabel)}
</Label>
</Tooltip2>
<InputGroup
id="hub-name-input"
value={hubName}
onChange={(e) => setHubName(e.currentTarget.value)}
onMouseOver={(e) => e.preventDefault()}
className="pb-hub-name-input"
intent={isHubNameValid ? Intent.NONE : Intent.DANGER}
placeholder="Pybricks Hub"
rightElement={
isHubNameValid ? undefined : (
<Tooltip2
content={i18n.translate(
I18nId.FirmwareHubNameErrorTooltip,
)}
rootBoundary="document"
placement="bottom"
targetTagName="div"
>
<Icon
icon="error"
intent={Intent.DANGER}
itemType="div"
/>
</Tooltip2>
)
}
/>
</ControlGroup>
</FormGroup>
<FormGroup label={i18n.translate(I18nId.HelpTitle)}>
<ButtonGroup minimal={true} vertical={true} alignText="left">
<AnchorButton
icon="lightbulb"
href={pybricksProjectsUrl}
target="blank_"
>
{i18n.translate(I18nId.HelpProjectsLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="help"
href={pybricksSupportUrl}
target="blank_"
>
{i18n.translate(I18nId.HelpSupportLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="chat"
href={pybricksGitterUrl}
target="blank_"
>
{i18n.translate(I18nId.HelpChatLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AnchorButton
icon="virus"
href={pybricksBugReportsUrl}
target="blank_"
>
{i18n.translate(I18nId.HelpBugsLabel)}
<ExternalLinkIcon />
</AnchorButton>
<AboutDialog
isOpen={isAboutDialogOpen}
onClose={() => setIsAboutDialogOpen(false)}
/>
</ButtonGroup>
</FormGroup>
<FormGroup
label={i18n.translate(I18nId.AppTitle)}
helperText={
readyForOfflineUse &&
i18n.translate(I18nId.AppOfflineUseHelp)
}
>
<ButtonGroup minimal={true} vertical={true} alignText="left">
{hasUnresolvedInstallPrompt && (
<Button
icon="add"
onClick={() => dispatch(appShowInstallPrompt())}
loading={promptingInstall}
>
{i18n.translate(I18nId.AppInstallLabel)}
</Button>
)}
{isServiceWorkerRegistered && !updateAvailable && (
<Button
icon="refresh"
onClick={() => dispatch(appCheckForUpdate())}
loading={checkingForUpdate}
>
{i18n.translate(I18nId.AppCheckForUpdateLabel)}
</Button>
)}
{isServiceWorkerRegistered && updateAvailable && (
<Button
icon="refresh"
onClick={() => dispatch(appReload())}
>
{i18n.translate(I18nId.AppRestartLabel)}
</Button>
)}
<Button
icon="info-sign"
onClick={() => {
setIsAboutDialogOpen(true);
return true;
}}
>
{i18n.translate(I18nId.AppAboutLabel)}
</Button>
</ButtonGroup>
</FormGroup>
{process.env.NODE_ENV === 'development' && (
<FormGroup label="Developer">
<Switch
checked={i18n.pseudolocalize !== false}
onChange={() => pseudolocalize(!i18n.pseudolocalize)}
label="Pseudolocalize"
/>
</FormGroup>
)}
</div>
</div>
</Drawer>
);
};
export default SettingsDrawer;