mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
Show notification when program is changed in another window
This commit is contained in:
committed by
David Lechner
parent
f721f959f0
commit
6d81da0df0
+36
-1
@@ -17,6 +17,14 @@ export enum EditorActionType {
|
||||
* Open a file.
|
||||
*/
|
||||
Open = 'editor.action.open',
|
||||
/**
|
||||
* Storage was changed outside of the app.
|
||||
*/
|
||||
StorageChanged = 'editor.action.storageChanged',
|
||||
/**
|
||||
* Reload program from local storage.
|
||||
*/
|
||||
ReloadProgram = 'editor.action.reloadProgram',
|
||||
}
|
||||
|
||||
export interface CurrentEditorAction extends Action<EditorActionType.Current> {
|
||||
@@ -61,7 +69,34 @@ export function open(data: ArrayBuffer): EditorOpenAction {
|
||||
return { type: EditorActionType.Open, data };
|
||||
}
|
||||
|
||||
/**Action that indicates the local storage has changed. */
|
||||
export interface EditorStorageChangedAction
|
||||
extends Action<EditorActionType.StorageChanged> {
|
||||
newValue: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action that indicates the local storage has changed.
|
||||
* @param newValue The new program.
|
||||
*/
|
||||
export function storageChanged(newValue: string): EditorStorageChangedAction {
|
||||
return { type: EditorActionType.StorageChanged, newValue };
|
||||
}
|
||||
|
||||
/** Action to request reloading the program from local storage. */
|
||||
export type EditorReloadProgramAction = Action<EditorActionType.ReloadProgram>;
|
||||
|
||||
/** Creates and action to request reloading the program from local storage. */
|
||||
export function reloadProgram(): EditorReloadProgramAction {
|
||||
return { type: EditorActionType.ReloadProgram };
|
||||
}
|
||||
|
||||
/**
|
||||
* Common type for all editor actions.
|
||||
*/
|
||||
export type EditorAction = CurrentEditorAction | EditorOpenAction | EditorSaveAsAction;
|
||||
export type EditorAction =
|
||||
| CurrentEditorAction
|
||||
| EditorOpenAction
|
||||
| EditorSaveAsAction
|
||||
| EditorStorageChangedAction
|
||||
| EditorReloadProgramAction;
|
||||
|
||||
@@ -10,7 +10,7 @@ import React from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import { connect } from 'react-redux';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { setEditSession } from '../actions/editor';
|
||||
import { setEditSession, storageChanged } from '../actions/editor';
|
||||
import { EditorStringId } from './editor';
|
||||
import en from './editor.en.json';
|
||||
|
||||
@@ -20,7 +20,10 @@ import 'ace-builds/src-noconflict/ext-searchbox';
|
||||
import 'ace-builds/src-noconflict/ext-keybinding_menu';
|
||||
import 'ace-builds/src-noconflict/ext-language_tools';
|
||||
|
||||
type DispatchProps = { onSessionChanged: (session?: Ace.EditSession) => void };
|
||||
type DispatchProps = {
|
||||
onSessionChanged: (session?: Ace.EditSession) => void;
|
||||
onProgramStorageChanged: (newValue: string) => void;
|
||||
};
|
||||
|
||||
type EditorProps = DispatchProps & WithI18nProps;
|
||||
|
||||
@@ -34,6 +37,24 @@ class Editor extends React.Component<EditorProps> {
|
||||
this.editorRef = React.createRef();
|
||||
}
|
||||
|
||||
onStorage = (e: StorageEvent): void => {
|
||||
if (
|
||||
e.key === 'program' &&
|
||||
e.newValue &&
|
||||
e.newValue !== this.editorRef.current?.editor.getValue()
|
||||
) {
|
||||
this.props.onProgramStorageChanged(e.newValue);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
window.addEventListener('storage', this.onStorage);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
window.removeEventListener('storage', this.onStorage);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { i18n, onSessionChanged } = this.props;
|
||||
const editor = this.editorRef.current?.editor;
|
||||
@@ -49,7 +70,7 @@ class Editor extends React.Component<EditorProps> {
|
||||
height="100%"
|
||||
focus={true}
|
||||
placeholder={i18n.translate(EditorStringId.Placeholder)}
|
||||
defaultValue={sessionStorage.getItem('program') || undefined}
|
||||
defaultValue={localStorage.getItem('program') || undefined}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
setOptions={{
|
||||
enableBasicAutocompletion: true,
|
||||
@@ -70,7 +91,7 @@ class Editor extends React.Component<EditorProps> {
|
||||
onSessionChanged(e?.session);
|
||||
}}
|
||||
onChange={(v): void => {
|
||||
sessionStorage.setItem('program', v);
|
||||
localStorage.setItem('program', v);
|
||||
}}
|
||||
commands={[
|
||||
{
|
||||
@@ -141,6 +162,7 @@ class Editor extends React.Component<EditorProps> {
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onSessionChanged: (s): Action => dispatch(setEditSession(s)),
|
||||
onProgramStorageChanged: (v): Action => dispatch(storageChanged(v)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -5,12 +5,13 @@ import { IconName, Intent, Toast } from '@blueprintjs/core';
|
||||
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from '../actions';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { NotificationLevel, remove } from '../actions/notification';
|
||||
import { Level } from '../reducers/notification';
|
||||
import { Level, MessageAction } from '../reducers/notification';
|
||||
import en from './notification.en.json';
|
||||
|
||||
interface DispatchProps {
|
||||
onAction: (action: Action) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ interface OwnProps {
|
||||
message?: string;
|
||||
messageId?: string;
|
||||
helpUrl?: string;
|
||||
action?: MessageAction;
|
||||
}
|
||||
|
||||
type NotificationProps = DispatchProps & OwnProps & WithI18nProps;
|
||||
@@ -52,25 +54,33 @@ function mapIcon(level: NotificationLevel): IconName | undefined {
|
||||
|
||||
class Notification extends React.Component<NotificationProps> {
|
||||
render(): JSX.Element {
|
||||
const {
|
||||
action,
|
||||
helpUrl,
|
||||
i18n,
|
||||
level,
|
||||
message,
|
||||
messageId,
|
||||
onAction,
|
||||
onClose,
|
||||
} = this.props;
|
||||
return (
|
||||
<Toast
|
||||
onDismiss={(): void => {
|
||||
this.props.onClose();
|
||||
}}
|
||||
onDismiss={(): void => onClose()}
|
||||
timeout={0}
|
||||
intent={mapIntent(this.props.level)}
|
||||
icon={mapIcon(this.props.level)}
|
||||
intent={mapIntent(level)}
|
||||
icon={mapIcon(level)}
|
||||
message={
|
||||
<div>
|
||||
<p>
|
||||
{this.props.messageId
|
||||
? this.props.i18n.translate(this.props.messageId)
|
||||
: this.props.message || 'missing message!'}
|
||||
{messageId
|
||||
? i18n.translate(messageId)
|
||||
: message || 'missing message!'}
|
||||
</p>
|
||||
{this.props.helpUrl && (
|
||||
{helpUrl && (
|
||||
<p>
|
||||
<a
|
||||
href={this.props.helpUrl}
|
||||
href={helpUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -80,15 +90,20 @@ class Notification extends React.Component<NotificationProps> {
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
action={
|
||||
action && {
|
||||
text: i18n.translate(action.titleId),
|
||||
onClick: (): void => onAction(action.action),
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps): DispatchProps => ({
|
||||
onClose: (): void => {
|
||||
dispatch(remove(ownProps.id));
|
||||
},
|
||||
onAction: (a): Action => dispatch(a),
|
||||
onClose: (): Action => dispatch(remove(ownProps.id)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -26,6 +26,7 @@ class NotificationStack extends React.Component<NotificationStackProps> {
|
||||
message={n.message}
|
||||
messageId={n.messageId}
|
||||
helpUrl={n.helpUrl}
|
||||
action={n.action}
|
||||
/>
|
||||
))}
|
||||
</Toaster>
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
"noWebBluetooth": "This web browser does not support Web Bluetooth or it is not enabled.",
|
||||
"connectFailed": "Unexpected error while trying to connect. Check console log and report the error."
|
||||
},
|
||||
"editor": {
|
||||
"programChanged": "The program was changed in another window. Do you want to delete this program and replace it with the new program?",
|
||||
"yesReloadProgram": "Yes"
|
||||
},
|
||||
"serviceWorker": {
|
||||
"success": "Content is cached for offline use.",
|
||||
"update": "New content is available and will be used when all tabs for this page are closed.'"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Reducer } from 'react';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Action } from '../actions';
|
||||
import { EditorActionType, reloadProgram } from '../actions/editor';
|
||||
import {
|
||||
BootloaderConnectionActionType,
|
||||
BootloaderConnectionFailureReason,
|
||||
@@ -18,8 +19,10 @@ export enum MessageId {
|
||||
BleConnectFailed = 'ble.connectFailed',
|
||||
BleGattServiceNotFound = 'ble.gattServiceNotFound',
|
||||
BleNoWebBluetooth = 'ble.noWebBluetooth',
|
||||
ProgramChanged = 'editor.programChanged',
|
||||
ServiceWorkerSuccess = 'serviceWorker.success',
|
||||
ServiceWorkerUpdate = 'serviceWorker.update',
|
||||
YesReloadProgram = 'editor.yesReloadProgram',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,12 +43,18 @@ export enum Level {
|
||||
Info = 'info',
|
||||
}
|
||||
|
||||
export interface MessageAction {
|
||||
titleId: MessageId;
|
||||
action: Action;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
readonly id: number;
|
||||
readonly level: Level;
|
||||
readonly message?: string;
|
||||
readonly messageId?: MessageId;
|
||||
readonly helpUrl?: string;
|
||||
readonly action?: MessageAction;
|
||||
}
|
||||
|
||||
export type NotificationList = Array<Notification>;
|
||||
@@ -57,8 +66,9 @@ function append(
|
||||
level: Level,
|
||||
messageId: MessageId,
|
||||
helpUrl?: string,
|
||||
action?: MessageAction,
|
||||
): NotificationList {
|
||||
return [...state, { id: nextId(), level, messageId, helpUrl }];
|
||||
return [...state, { id: nextId(), level, messageId, helpUrl, action }];
|
||||
}
|
||||
|
||||
const list: Reducer<NotificationList, Action> = (state = [], action) => {
|
||||
@@ -88,6 +98,15 @@ const list: Reducer<NotificationList, Action> = (state = [], action) => {
|
||||
return append(state, Level.Error, MessageId.BleConnectFailed);
|
||||
}
|
||||
return state;
|
||||
case EditorActionType.StorageChanged:
|
||||
if (state.find((x) => x.messageId === MessageId.ProgramChanged)) {
|
||||
// don't show message again if it is already shown
|
||||
return state;
|
||||
}
|
||||
return append(state, Level.Info, MessageId.ProgramChanged, undefined, {
|
||||
titleId: MessageId.YesReloadProgram,
|
||||
action: reloadProgram(),
|
||||
});
|
||||
case MpyActionType.DidFailToCompile:
|
||||
return [
|
||||
...state,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { Ace } from 'ace-builds';
|
||||
import { select, takeEvery } from 'redux-saga/effects';
|
||||
import { EditorActionType, EditorReloadProgramAction } from '../actions/editor';
|
||||
import { RootState } from '../reducers';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function* reloadProgram(_action: EditorReloadProgramAction): Generator {
|
||||
const editor = (yield select(
|
||||
(s: RootState) => s.editor.current,
|
||||
)) as Ace.EditSession;
|
||||
editor.setValue(localStorage.getItem('program') || '');
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(EditorActionType.ReloadProgram, reloadProgram);
|
||||
}
|
||||
+2
-1
@@ -2,10 +2,11 @@
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
|
||||
import { all } from 'redux-saga/effects';
|
||||
import editor from './editor';
|
||||
import bootloader from './lwp3-bootloader';
|
||||
import mpy from './mpy';
|
||||
|
||||
/* istanbul ignore next */
|
||||
export default function* (): Generator {
|
||||
yield all([bootloader(), mpy()]);
|
||||
yield all([bootloader(), editor(), mpy()]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user