Merge pull request #940 from xodio/feat-925-ui-snackbar-messages

Brand-new look of messages in the IDE
This commit is contained in:
Kirill Shumilov
2017-12-13 20:21:32 +03:00
committed by GitHub
34 changed files with 614 additions and 237 deletions

View File

@@ -8,6 +8,7 @@ import popupsReducer from './popups/reducer';
import uploadReducer from './upload/reducer';
import debuggerMiddleware from './debugger/middleware';
import autoupdateMiddleware from './view/autoupdateMiddleware';
import installLibMiddleware from './view/installLibMiddleware';
const extraReducers = {
@@ -18,6 +19,7 @@ const extraReducers = {
const extraMiddlewares = [
debuggerMiddleware,
installLibMiddleware,
autoupdateMiddleware,
];
ReactDOM.render(

View File

@@ -0,0 +1,8 @@
// Duplicate of `composeMessage` from `xod-client`
// It's necessary to prevent errors with importing `xod-client`,
// that contains styles and react components.
export default (title, note = null, button = null) => ({
title,
note,
button,
});

View File

@@ -5,42 +5,95 @@ import { ERROR_CODES as XFS_EC } from 'xod-fs';
// that contains async/await functions and non-isomorphic dependencies
import { COMPILATION_ERRORS as XD_EC } from 'xod-deploy/dist/constants';
import composeMessage from './composeMessage';
import * as EC from './errorCodes';
const UNKNOWN_ERROR = err => `Unknown error occurred: ${err.message || JSON.stringify(err)}`;
const UNKNOWN_ERROR = err => composeMessage(
'Unknown error occurred',
err.message || JSON.stringify(err)
);
const ERROR_FORMATTERS = {
// Errors that will showed in the upload popup
[EC.TRANSPILE_ERROR]: err => `Error occurred during transpilation: ${err}`,
[EC.PORT_NOT_FOUND]: err => `Could not find Arduino device on port: ${err.port.comName}. Available ports: ${R.map(R.prop('comName'), err.ports)}`,
[EC.UPLOAD_ERROR]: err => `Error occured during uploading: ${err}`,
[EC.INDEX_LIST_ERROR]: err => `Could not connect to Arduino Packages Index at ${err.request.path}: ${err.error.message}`,
[EC.CANT_INSTALL_ARCHITECTURE]: err => `Could not install tools: ${err}`,
[XFS_EC.INVALID_WORKSPACE_PATH]: err => `Invalid workspace path: "${err.path}" of type "${typeof err.path}"`,
[XFS_EC.WORKSPACE_DIR_NOT_EMPTY]: err => `Workspace directory at ${err.path} is not empty`,
[XFS_EC.WORKSPACE_DIR_NOT_EXIST_OR_EMPTY]: err => `Workspace directory at ${err.path} not exist or empty`,
// Snackbar errors
[XFS_EC.INVALID_WORKSPACE_PATH]: err => composeMessage(
'Invalid workspace path',
`"${err.path}" of type "${typeof err.path}"`
),
[XFS_EC.WORKSPACE_DIR_NOT_EMPTY]: err => composeMessage(
`Workspace directory at ${err.path} is not empty`
),
[XFS_EC.WORKSPACE_DIR_NOT_EXIST_OR_EMPTY]: err => composeMessage(
`Workspace directory at ${err.path} not exist or empty`
),
[XFS_EC.INVALID_FILE_CONTENTS]: err => `Could not open selected project: invalid contents in ${err.path}`,
[XFS_EC.INVALID_FILE_CONTENTS]: err => composeMessage(
'Canʼt open selected project',
`Invalid contents in ${err.path}`
),
[XFS_EC.CANT_CREATE_WORKSPACE_FILE]: err => `Could not create workspace at ${err.path}: ${err.message}`,
[XFS_EC.CANT_COPY_STDLIB]: err => `Could not copy stdlib at ${err.path}: ${err.message}`,
[XFS_EC.CANT_COPY_DEFAULT_PROJECT]: err => `Could not copy default project at ${err.path}: ${err.message}`,
[XFS_EC.CANT_ENUMERATE_PROJECTS]: err => `Could not enumerate projects at ${err.path}: ${err.message}`,
[XFS_EC.CANT_SAVE_PROJECT]: err => `Could not save the project at ${err.path}: ${err.message}`,
[XFS_EC.CANT_SAVE_LIBRARY]: err => `Could not save the library at ${err.path}: ${err.message}`,
[XFS_EC.CANT_CREATE_WORKSPACE_FILE]: err => composeMessage(
`Could not create workspace at ${err.path}`,
err.message
),
[XFS_EC.CANT_COPY_STDLIB]: err => composeMessage(
`Could not copy stdlib at ${err.path}`,
err.message
),
[XFS_EC.CANT_COPY_DEFAULT_PROJECT]: err => composeMessage(
`Could not copy default project at ${err.path}`,
err.message
),
[XFS_EC.CANT_ENUMERATE_PROJECTS]: err => composeMessage(
`Could not enumerate projects at ${err.path}`,
err.message
),
[XFS_EC.CANT_SAVE_PROJECT]: err => composeMessage(
`Could not save the project at ${err.path}`,
err.message
),
[XFS_EC.CANT_SAVE_LIBRARY]: err => composeMessage(
`Could not save the library at ${err.path}`,
err.message
),
[EC.CANT_CREATE_NEW_PROJECT]: err => `Could not create a new project: ${err.message}`,
[EC.CANT_OPEN_SELECTED_PROJECT]: err => `Could not open selected project: ${err.message}`,
[EC.CANT_CREATE_NEW_PROJECT]: err => composeMessage(
'Could not create a new project',
err.message
),
[EC.CANT_OPEN_SELECTED_PROJECT]: err => composeMessage(
'Could not open selected project',
err.message
),
[EC.BOARD_NOT_SUPPORTED]: err => `Board ${err.boardId} is not supported yet`,
[EC.CANT_GET_UPLOAD_CONFIG]: err => `Could not get upload config for board ${err.boardId}. Returned status ${err.status} ${err.statusText}`,
[EC.CLOUD_NETWORK_ERROR]: err => `Could not connect to cloud service. Probably you donʼt have an internet connection. Error: ${err.message}`,
[EC.CLOUD_NETWORK_ERROR]: err => composeMessage(
'Could not connect to cloud service.',
`Probably you donʼt have an internet connection. Error: ${err.message}`
),
[XD_EC.COMPILE_FAILED]: err => `Compilation failed with error: ${err.message}`,
[XD_EC.COMPILE_TIMEDOUT]: err => `Compilation timed out (${err.message}ms expired)`,
[XD_EC.CLOSED]: err => `Connection was closed with reason (${err.closeCode}) ${err.reason}`,
[XD_EC.FAILED]: err => `Canʼt establish connection with compile server: (${err.code}) ${err.message}`,
[XD_EC.COMPILE_FAILED]: err => composeMessage(
'Compilation failed with error',
err.message
),
[XD_EC.COMPILE_TIMEDOUT]: err => composeMessage(
`Compilation timed out (${err.message}ms expired)`
),
[XD_EC.CLOSED]: err => composeMessage(
`Connection was closed with reason (${err.closeCode})`,
err.reason
),
[XD_EC.FAILED]: err => composeMessage(
`Canʼt establish connection with compile server: (${err.code})`,
err.message
),
};
// :: Error -> String

View File

@@ -1,3 +1,4 @@
import composeMessage from './composeMessage';
import * as EVENTS from './events';
export const CODE_TRANSPILED = 'Project was successfully transpiled. Searching for device...';
@@ -18,6 +19,18 @@ export const SAVE_ALL_PROCESSED = '';
export const SAVE_ALL_FAILED = 'Failed to save project.';
export const SAVE_ALL_SUCCEED = 'Saved successfully!';
export const DEBUG_SESSION_STOPPED_ON_CHANGE = 'Debug session was automatically stopped, cause your Project has been changed.';
export const DEBUG_SESSION_STOPPED_ON_TAB_CLOSE = 'Debug session was automatically stopped, cause you closed Debugger tab.';
export const DEBUG_SESSION_STOPPED_ON_CHANGE = composeMessage(
'Debug session stopped',
'Your Project has been changed.'
);
export const DEBUG_SESSION_STOPPED_ON_TAB_CLOSE = composeMessage(
'Debug session stopped',
'You closed Debugger tab.'
);
export const DEBUG_LOST_CONNECTION = 'Lost connection with the device.';
export const updateAvailableMessage = version => composeMessage(
'Update available',
`New version ${version} of XOD\u00A0IDE is available`,
'Download & Install'
);

View File

@@ -10,6 +10,7 @@ import {
addConfirmation,
addError,
SAVE_ALL,
composeMessage,
} from 'xod-client';
import * as EVENTS from '../shared/events';
import * as MESSAGES from '../shared/messages';
@@ -91,7 +92,9 @@ export const createAsyncAction = ({
);
}
if (notify) {
dispatch(addConfirmation(completeMsg));
dispatch(addConfirmation(
composeMessage(completeMsg)
));
}
onComplete(payload, dispatch);
@@ -119,7 +122,7 @@ export const createAsyncAction = ({
);
}
if (notify) {
dispatch(addError(errorMsg));
dispatch(addError(composeMessage(errorMsg)));
}
onError(err, dispatch);

View File

@@ -1,5 +1,8 @@
import R from 'ramda';
import * as EVENTS from '../shared/events';
import { updateAvailableMessage } from '../shared/messages';
export const UPDATE_IDE_MESSAGE_ID = 'updateIde';
// :: ipcRenderer -> AppContainer -> ()
export const subscribeAutoUpdaterEvents = (ipcRenderer, App) => {
@@ -15,15 +18,9 @@ export const subscribeAutoUpdaterEvents = (ipcRenderer, App) => {
(event, info) => {
console.log('Update available: ', info); // eslint-disable-line no-console
App.props.actions.addNotification(
`New version (${info.version}) of XOD\u00A0IDE is available`,
[{
id: 'downloadAndInstall',
text: 'Download & Install',
}, {
id: 'dismiss',
text: 'Skip',
}],
true
updateAvailableMessage(info.version),
true,
UPDATE_IDE_MESSAGE_ID
);
}
);
@@ -42,6 +39,7 @@ export const subscribeAutoUpdaterEvents = (ipcRenderer, App) => {
ipcRenderer.on(
EVENTS.APP_UPDATE_DOWNLOAD_STARTED,
() => {
App.setState(R.assoc('downloadProgressPopup', true));
console.log('Download has been started!'); // eslint-disable-line no-console
}
);

View File

@@ -0,0 +1,12 @@
import client from 'xod-client';
import { ipcRenderer } from 'electron';
import { UPDATE_IDE_MESSAGE_ID, downloadUpdate } from './autoupdate';
export default () => next => (action) => {
if (action.type === client.MESSAGE_BUTTON_CLICKED && action.payload === UPDATE_IDE_MESSAGE_ID) {
downloadUpdate(ipcRenderer);
}
return next(action);
};

View File

@@ -37,7 +37,7 @@ import * as EVENTS from '../../shared/events';
import * as MESSAGES from '../../shared/messages';
import { createSystemMessage } from '../../shared/debuggerMessages';
import { subscribeAutoUpdaterEvents, downloadUpdate } from '../autoupdate';
import { subscribeAutoUpdaterEvents } from '../autoupdate';
import { subscribeToTriggerMainMenuRequests } from '../../testUtils/triggerMainMenu';
const { app, dialog, Menu } = remoteElectron;
@@ -85,8 +85,6 @@ class App extends client.App {
this.getSidebarPaneHeight = this.getSidebarPaneHeight.bind(this);
this.setSidebarPaneHeight = this.setSidebarPaneHeight.bind(this);
this.onClickMessageButton = this.onClickMessageButton.bind(this);
this.onUploadToArduinoClicked = this.onUploadToArduinoClicked.bind(this);
this.onUploadToArduino = this.onUploadToArduino.bind(this);
this.onArduinoTargetBoardChange = this.onArduinoTargetBoardChange.bind(this);
@@ -184,13 +182,6 @@ class App extends client.App {
props.actions.fetchGrant();
}
onClickMessageButton(buttonId, /* messageInfo */) {
if (buttonId === 'downloadAndInstall') {
downloadUpdate(ipcRenderer);
this.setState(R.assoc('downloadProgressPopup', true));
}
}
onResize() {
this.setState(
R.set(
@@ -238,7 +229,9 @@ class App extends client.App {
proc.success(payload.message);
if (debug) {
foldEither(
error => this.props.actions.addError(error.message),
error => this.props.actions.addError(
client.composeMessage(error.message)
),
(nodeIdsMap) => {
this.props.actions.startDebuggerSession(
createSystemMessage('Debug session started'),
@@ -271,7 +264,7 @@ class App extends client.App {
this.props.actions.createProject(projectName);
ipcRenderer.send(EVENTS.CREATE_PROJECT, projectName);
ipcRenderer.once(EVENTS.SAVE_ALL, () => {
this.props.actions.addConfirmation(MESSAGES.PROJECT_SAVE_SUCCEED);
this.props.actions.addConfirmation(MESSAGES.SAVE_ALL_SUCCEED);
});
}
@@ -303,7 +296,9 @@ class App extends client.App {
fs.readFile(filePaths[0], 'utf8', (err, data) => {
if (err) {
this.props.actions.addError(err.message);
this.props.actions.addError(
client.composeMessage(err.message)
);
}
this.onImport(data);
@@ -337,7 +332,10 @@ class App extends client.App {
onImport(jsonString) {
foldEither(
this.props.actions.addError,
R.compose(
this.props.actions.addError,
client.composeMessage
),
(project) => {
if (!this.confirmUnsavedChanges()) return;
this.props.actions.importProject(project);
@@ -680,9 +678,6 @@ class App extends client.App {
setSidebarPaneHeight={this.setSidebarPaneHeight}
stopDebuggerSession={() => debuggerIPC.sendStopDebuggerSession(ipcRenderer)}
/>
<client.SnackBar
onClickMessageButton={this.onClickMessageButton}
/>
{this.renderPopupShowCode()}
{this.renderPopupUploadConfig()}
{this.renderPopupUploadProcess()}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
const CloseButton = ({
as = 'button',
className,
...restProps
}) => React.createElement(
as,
{
...restProps,
className: cn('CloseButton', className),
role: as !== 'button' ? 'button' : restProps.role,
}
);
CloseButton.propTypes = {
as: PropTypes.string,
className: PropTypes.string,
};
export default CloseButton;

View File

@@ -23,6 +23,7 @@ import PopupProjectPreferences from '../../project/components/PopupProjectPrefer
import PopupPublishProject from '../../project/components/PopupPublishProject';
import * as actions from '../actions';
import composeMessage from '../../messages/composeMessage';
export default class App extends React.Component {
componentDidMount() {
@@ -37,7 +38,7 @@ export default class App extends React.Component {
const eitherCode = eitherTProject.map(transpile);
return foldEither(
error => this.props.actions.addError(error.message),
error => this.props.actions.addError(composeMessage(error.message)),
this.props.actions.showCode,
eitherCode
);

View File

@@ -0,0 +1,117 @@
// Monochromes
$black: #000;
$white: #fff;
$coal: #222;
$coal-light: #252525;
$coal-bright: #2b2b2b;
$dark: #363636;
$dark-deep: #303030;
$dark-light: #3e3e3e;
$dark-bright: #404040;
$grey: #444;
$grey-light: #555;
$grey-bright: #666;
$light-grey: #777;
$light-grey-bright: #999;
$chalk: #ccc;
$chalk-light: #aaa;
$chalk-bright: #eee;
// Colored
$blue: #2d83b4;
$magenta: #9f5497;
$cyan: #4ed5ed;
$yellow: #f6dd0d;
$red: #c33d3d;
$red-bright: #fd6161;
$green: #579205;
$green-bright: #86cc23;
$violet: #6965bd;
$violet-light: #7570cb;
$violet-shadow: #5c59a6;
$orange: #c76530;
$orange-light: #d9733c;
$orange-shadow: #af592a;
$blackberry: #4a4a57;
$blackberry-light: #616077;
$blackberry-shadow: #8c4a85;
$olive: #827a22;
$olive-light: #988f34;
$olive-shadow: #726b1e;
$emerald: #2e8f6c;
$emerald-light: #3fa37e;
$emerald-shadow: #287e5f;
$apricot: #d8a53a;
$apricot-light: #e9b445;
$apricot-shadow: #be9133;
$brown: #584b22;
$brown-light: #8e5f35;
$brown-shadow: #69421e;
// Associated colors
$selected: $cyan;
$error: $red;
$chrome-bg: $dark;
$chrome-title: $coal-bright;
$chrome-title-button-hover: $dark;
$chrome-outlines: $coal;
$chrome-light-bg: $chalk;
$menu-bg: $dark-deep;
$menu-hover: $dark-bright;
$menu-active: $coal;
$menu-item-bg: $coal;
$menu-item-hover: $dark-deep;
$menu-separator: $grey;
$tab-bg: $coal;
$tab-item-bg: $dark-deep;
$tab-item-hover: $grey;
$tab-button-hover: $dark;
$canvas: $dark-deep;
$canvas-outlines: $dark-light;
$node-label: $chalk;
$node-outlines: $light-grey-bright;
$node-hover-outlines: $chalk;
$pin-bg: $coal-light;
$pin-label: $light-grey-bright;
$input-bg: $chrome-bg;
$input-outlines: $light-grey;
$input-disabled-outlines: $dark-bright;
$input-disabled-text: $grey-light;
$input-active-bg: $grey-light;
$dark-button-text: $coal;
$dark-button-bg: $chrome-bg;
$dark-button-outlines: $input-outlines;
$dark-button-hover-bg: $tab-item-hover;
$dark-button-hover-outlines: $grey-light;
$dark-button-disabled-text: $grey;
$dark-button-disabled-outlines: $grey;
$light-button-text: $coal;
$light-button-bg: $chrome-light-bg;
$light-button-outlines: $input-outlines;
$light-button-hover-bg: $light-button-bg;
$light-button-hover-outlines: $grey-bright;
$light-button-disabled-text: rgba($light-button-text, .5);
$light-button-disabled-outlines: rgba($light-button-outlines, .5);

View File

@@ -57,7 +57,6 @@ $color-tabs-hover-text: #FFF;
$color-tabs-debugger-background: #3d4931;
$color-tabs-debugger-text: #82b948;
$snackbar-width: 220px;
$sidebar-width: 200px;
$sidebar-color-bg: #3d3d3d;
@@ -93,7 +92,3 @@ $button-fg-color-light: black;
$modal-fg-color: #cccccc;
$modal-fg-color-light: black;
$modal-bg-color-light: #cccccc;
$message-border-color: #818181;
$message-text-color: #e5e5e5;
$message-bg-color: #3d3d3d;

View File

@@ -0,0 +1,52 @@
.CloseButton {
position: absolute;
right: 0;
top: 0;
width: 32px;
height: 32px;
background: transparent;
border: 0;
cursor: pointer;
line-height: 0;
font-size: $font-size-xs;
color: $chalk;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline: none;
&:hover, &:focus {
outline: none;
&:before {
background-color: $black;
}
}
&:before {
content: '';
position: absolute;
top: 6px;
right: 6px;
width: 18px;
height: 18px;
background-color: rgba($black, .5);
border-radius: 100%;
border: 0;
}
&:after {
content: '';
display: inline;
position: absolute;
top: 15px;
width: 18px;
right: 6px;
z-index: 2;
}
}

View File

@@ -1,8 +1,10 @@
.SnackBarList {
position: fixed;
position: absolute;
z-index: 9999;
top: 37px; // Height of a top bar. TODO: variable?
right: 7px;
left: 0;
right: 0;
bottom: 0;
width: 100%;
margin: 0;
width: $snackbar-width;
padding: 0;
}

View File

@@ -1,58 +1,82 @@
.SnackBarMessage {
position: relative;
display: none;
margin-top: 7px;
transition: all 0.3s ease-in;
left: 0;
transition: all 0.1s ease-in;
opacity: 1;
&.hidden {
left: $snackbar-width;
}
font-family: $font-family-normal;
font-weight: 300;
background: $chrome-bg;
color: $chalk;
border-top: 1px solid $chrome-outlines;
min-height: 60px;
box-sizing: border-box;
&.display {
display: block;
}
&.hidden { opacity: 0; }
&.display { display: block; }
&.error {
a {
color: $color-error;
border-color: $color-error;
}
}
&.confirmation {
a {
border-color: $message-border-color;
}
}
&.notification {
a {
border-color: $color-success;
}
}
a {
display: block;
padding: 11px 11px;
color: $message-text-color;
background-color: $message-bg-color;
box-shadow: 0 0 7px rgba(0,0,0,.5);
border: 2px solid $message-border-color;
border-radius: 6px;
.message-content {
display: flex;
width: 70%;
padding: 10px 0;
margin: 0 auto;
cursor: default;
outline: none;
.message-content { margin: 0; }
}
.SnackBar-buttons-container {
margin-top: 7px;
.message-text {
width: 65%;
margin: 0;
color: $white;
font-size: $font-size-m;
line-height: 1.4em;
padding-right: 3em;
opacity: 0.9;
}
.message-buttons {
width: 35%;
display: flex;
flex-direction: row;
align-items: center;
.Button {
margin-right: 5px;
display: block;
width: 100%;
border: none;
}
}
.title {
display: block;
font-family: $font-family-condensed;
font-size: $font-size-l;
font-weight: 400;
margin-bottom: .5em;
}
&.persistent {
color: $white !important;
.title { color: $white !important; }
}
&.error {
.title { color: $red-bright; }
&.persistent { background: $error; }
}
&.confirmation {
.title { color: $green-bright; }
&.persistent {
background: $green;
}
}
&.notification {
.title { color: $cyan; }
&.persistent {
background: $blue;
}
}
}

View File

@@ -1,4 +1,5 @@
@import
'abstracts/colors',
'abstracts/variables',
'abstracts/functions',
'abstracts/mixins';
@@ -18,6 +19,7 @@
'components/BackgroundLayer',
'components/Breadcrumbs',
'components/Button',
'components/CloseButton',
'components/Comment',
'components/CppImplementationEditor',
'components/Debugger',

View File

@@ -7,11 +7,13 @@ import { fetchLibsWithDependencies, stringifyLibQuery, getLibName } from 'xod-pm
import {
SELECTION_ENTITY_TYPE,
CLIPBOARD_DATA_TYPE,
CLIPBOARD_ERRORS as CLIPBOARD_ERROR_CODES,
} from './constants';
import { LINK_ERRORS, CLIPBOARD_ERRORS } from '../messages/constants';
import { libInstalled } from './messages';
import { LINK_ERRORS } from '../project/messages';
import {
libInstalled,
CLIPBOARD_RECURSION_PASTE_ERROR,
clipboardMissingPatchPasteError,
} from './messages';
import * as ActionType from './actionTypes';
@@ -25,6 +27,7 @@ import {
addError,
addConfirmation,
} from '../messages/actions';
import composeMessage from '../messages/composeMessage';
import * as Selectors from './selectors';
import * as ProjectSelectors from '../project/selectors';
@@ -345,7 +348,7 @@ export const pasteEntities = event => (dispatch, getState) => {
const pastedNodeTypes = R.map(XP.getNodeType, copiedEntities.nodes);
if (R.contains(currentPatchPath, pastedNodeTypes)) {
dispatch(addError(CLIPBOARD_ERRORS[CLIPBOARD_ERROR_CODES.RECURSION_DETECTED]));
dispatch(addError(CLIPBOARD_RECURSION_PASTE_ERROR));
return;
}
@@ -358,10 +361,9 @@ export const pasteEntities = event => (dispatch, getState) => {
const missingPatches = R.without(availablePatches, pastedNodeTypes);
if (!R.isEmpty(missingPatches)) {
dispatch(addError(XP.formatString(
CLIPBOARD_ERRORS[CLIPBOARD_ERROR_CODES.NO_REQUIRED_PATCHES],
{ missingPatches: missingPatches.join(', ') }
)));
dispatch(addError(
clipboardMissingPatchPasteError(missingPatches.join(', '))
));
return;
}
@@ -472,6 +474,6 @@ export const installLibraries = libParams => (dispatch, getState) => {
errorCode: err.errorCode,
},
});
dispatch(addError(err.message));
dispatch(addError(composeMessage(err.message)));
});
};

View File

@@ -48,10 +48,6 @@ export const LINK_ERRORS = {
INCOMPATIBLE_TYPES: 'INCOMPATIBLE_TYPES',
};
export const PROPERTY_ERRORS = {
PIN_HAS_LINK: 'PIN_HAS_LINK',
};
export const CLIPBOARD_ERRORS = {
RECURSION_DETECTED: 'RECURSION_DETECTED',
NO_REQUIRED_PATCHES: 'NO_REQUIRED_PATCHES',

View File

@@ -33,6 +33,7 @@ import Debugger from '../../debugger/containers/Debugger';
import Breadcrumbs from '../../debugger/containers/Breadcrumbs';
import Sidebar from '../components/Sidebar';
import Workarea from '../../utils/components/Workarea';
import SnackBar from '../../messages/containers/SnackBar';
import { RenderableSelection } from '../../types';
import sanctuaryPropType from '../../utils/sanctuaryPropType';
@@ -235,6 +236,7 @@ class Editor extends React.Component {
{BreadcrumbsContainer}
{this.renderOpenedImplementationEditorTabs()}
{DebuggerContainer}
<SnackBar />
</Workarea>
</FocusTrap>
<Helpbar />

View File

@@ -1,6 +1,18 @@
import composeMessage from '../messages/composeMessage';
export const PATCH_FOR_NODE_IS_MISSING = 'Patch for this node is missing.';
export const libInstalled = (libName, version) => `${libName} @ ${version} installed successfully`;
export const libInstalled = (libName, version) => composeMessage(
`${libName} @ ${version} installed successfully`
);
export const CLIPBOARD_RECURSION_PASTE_ERROR = composeMessage(
'Canʼt paste a patch into itself'
);
export const clipboardMissingPatchPasteError = missingPatches => composeMessage(
'Canʼt paste',
`Canʼt find following patches: ${missingPatches}`
);
export const LIB_SUGGESTER_TYPE_TO_BEGIN = 'Type owner/libname to find a library';
export const LIB_SUGGESTER_NOTHING_FOUND = 'No library found';

View File

@@ -15,6 +15,7 @@ import * as ProjectBrowserActions from './projectBrowser/actions';
import * as PopupActions from './popups/actions';
import * as DebuggerActions from './debugger/actions';
import { MESSAGE_BUTTON_CLICKED } from './messages/actionTypes';
import { TAB_CLOSE, INSTALL_LIBRARIES_COMPLETE } from './editor/actionTypes';
import { SAVE_ALL } from './project/actionTypes';
@@ -33,6 +34,7 @@ import App from './core/containers/App';
import Root from './core/containers/Root';
import { container as Editor, CreateNodeWidget } from './editor';
import SnackBar from './messages';
import composeMessage from './messages/composeMessage';
import * as MessageConstants from './messages/constants';
import Toolbar from './utils/components/Toolbar';
import PopupShowCode from './utils/components/PopupShowCode';
@@ -53,6 +55,7 @@ export * from './projectBrowser/actions';
export * from './popups/actions';
export * from './debugger/actions';
export { MESSAGE_BUTTON_CLICKED } from './messages/actionTypes';
export { TAB_CLOSE, INSTALL_LIBRARIES_COMPLETE } from './editor/actionTypes';
export { SAVE_ALL } from './project/actionTypes';
@@ -82,6 +85,7 @@ export { default as App } from './core/containers/App';
export { default as Root } from './core/containers/Root';
export { container as Editor, CreateNodeWidget } from './editor';
export { default as SnackBar } from './messages';
export { default as composeMessage } from './messages/composeMessage';
export * from './messages/constants';
export { default as initialState } from './core/state';
@@ -109,9 +113,11 @@ export default Object.assign({
PopupProjectPreferences,
hasUnsavedChanges,
getLastSavedProject,
composeMessage,
TAB_CLOSE,
SAVE_ALL,
INSTALL_LIBRARIES_COMPLETE,
MESSAGE_BUTTON_CLICKED,
},
UserSelectors,
EditorSelectors,

View File

@@ -1,2 +1,3 @@
export const MESSAGE_ADD = 'MESSAGE_ADD';
export const MESSAGE_DELETE = 'MESSAGE_DELETE';
export const MESSAGE_BUTTON_CLICKED = 'MESSAGE_BUTTON_CLICKED';

View File

@@ -4,10 +4,11 @@ import { STATUS } from '../utils/constants';
const getTimestamp = () => new Date().getTime();
export const addMessage = (type, message, buttons = [], persistent = false) => ({
export const addMessage = (type, messageData, persistent = false, id = null) => ({
type: ActionType.MESSAGE_ADD,
payload: { message, buttons },
payload: messageData,
meta: {
id,
type,
persistent,
timestamp: getTimestamp(),
@@ -29,3 +30,8 @@ export const deleteMessage = id => ({
export const addError = (...args) => addMessage(MESSAGE_TYPE.ERROR, ...args);
export const addConfirmation = (...args) => addMessage(MESSAGE_TYPE.CONFIRMATION, ...args);
export const addNotification = (...args) => addMessage(MESSAGE_TYPE.NOTIFICATION, ...args);
export const messageButtonClick = messageId => ({
type: ActionType.MESSAGE_BUTTON_CLICKED,
payload: messageId,
});

View File

@@ -3,8 +3,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { MESSAGE_TYPE } from '../constants';
import Button from '../../core/components/Button';
import CloseButton from '../../core/components/CloseButton';
const ANIMATION_TIMEOUT = 500;
const ANIMATION_TIMEOUT = 100;
class SnackBarMessage extends React.Component {
constructor(props) {
@@ -16,6 +18,7 @@ class SnackBarMessage extends React.Component {
};
this.hide = this.hide.bind(this);
this.onCloseMessage = this.onCloseMessage.bind(this);
}
componentDidMount() {
@@ -24,35 +27,37 @@ class SnackBarMessage extends React.Component {
});
}
onCloseMessage() {
this.props.onCloseMessage(this.props.message.id);
}
getMessageContent() {
const { message, onClickMessageButton } = this.props;
const buttons = R.unless(
R.isEmpty,
R.compose(
btns => React.createElement(
'div',
{ className: 'SnackBar-buttons-container' },
btns
),
R.map(({ id, text }) => (
<button
className="Button Button--small"
key={id}
onClick={() => onClickMessageButton(id, message)}
>
{text}
</button>
))
const button = R.unless(
R.isNil,
text => (
<Button
small
light
onClick={() => onClickMessageButton(message.id)}
>
{text}
</Button>
)
)(message.payload.buttons);
)(message.payload.button);
return (
<div className="message-content">
{message.payload.message}
{buttons}
</div>
);
return [
<div className="message-text" key="text">
<span className="title">
{message.payload.title}
</span>
{message.payload.note}
</div>,
<div className="message-buttons" key="buttons">
{button}
</div>,
];
}
setHidden(val) {
@@ -84,6 +89,7 @@ class SnackBarMessage extends React.Component {
error: message.type === MESSAGE_TYPE.ERROR,
confirmation: message.type === MESSAGE_TYPE.CONFIRMATION,
notification: message.type === MESSAGE_TYPE.NOTIFICATION,
persistent: message.persistent,
});
const messageContent = this.getMessageContent();
@@ -91,7 +97,11 @@ class SnackBarMessage extends React.Component {
<li
className={cls}
>
<a tabIndex={message.id} >
<a
className="message-content"
tabIndex={message.id}
>
<CloseButton onClick={this.onCloseMessage} />
{messageContent}
</a>
</li>
@@ -102,19 +112,21 @@ class SnackBarMessage extends React.Component {
SnackBarMessage.propTypes = {
message: PropTypes.shape({
/* eslint-disable react/no-unused-prop-types */
id: PropTypes.number,
id: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
type: PropTypes.string,
persistent: PropTypes.bool,
payload: PropTypes.shape({
message: PropTypes.string.isRequired,
buttons: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
text: PropTypes.string,
})).isRequired,
title: PropTypes.string.isRequired,
note: PropTypes.string,
button: PropTypes.string,
}),
/* eslint-enable react/no-unused-prop-types */
}),
onClickMessageButton: PropTypes.func,
onCloseMessage: PropTypes.func.isRequired,
};
SnackBarMessage.defaultProps = {

View File

@@ -0,0 +1,5 @@
export default (title, note = null, button = null) => ({
title,
note,
button,
});

View File

@@ -1,51 +1,6 @@
import {
LINK_ERRORS as LE,
NODETYPE_ERROR_TYPES as NTE,
PROPERTY_ERRORS as PE,
CLIPBOARD_ERRORS as CE,
} from '../editor/constants';
// eslint-disable-next-line import/prefer-default-export
export const MESSAGE_TYPE = {
ERROR: 'ERROR',
CONFIRMATION: 'CONFIRMATION',
NOTIFICATION: 'NOTIFICATION',
};
export const LINK_ERRORS = {
[LE.SAME_DIRECTION]: 'Can`t create link between pins of the same direction!',
[LE.SAME_NODE]: 'Can`t create link between pins of the same node!',
[LE.ONE_LINK_FOR_INPUT_PIN]: 'Input pin can have only one link!',
[LE.UNKNOWN_ERROR]: 'Unknown error!',
[LE.INCOMPATIBLE_TYPES]: 'Incompatible pin types!',
};
export const PROJECT_BROWSER_ERRORS = {
CANT_OPEN_LIBPATCH_WITHOUT_XOD_IMPL: 'This patch has only native implementation and can`t be opened',
CANT_DELETE_CURRENT_PATCH: 'Current patch cannot been deleted. Switch to another patch before!',
PATCH_NAME_TAKEN: 'This patch name is already taken!',
INVALID_PATCH_NAME: 'Invalid patch name',
INVALID_PROJECT_NAME: 'Invalid project name',
};
export const NODETYPE_ERRORS = {
[NTE.CANT_DELETE_USED_PATCHNODE]: (
'Current Patch Node is used somewhere. You should remove it first!'
),
[NTE.CANT_DELETE_USED_PIN_OF_PATCHNODE]: [
'Current IO Node is represents a Pin of Patch Node.',
'And it is used somewhere.',
'You should remove a linkage first!',
].join(' '),
};
export const PROPERTY_ERRORS = {
[PE.PIN_HAS_LINK]: [
'Can`t convert a pin into property, because it has a connected links.',
'You should remove links first!',
].join(' '),
};
export const CLIPBOARD_ERRORS = {
[CE.RECURSION_DETECTED]: 'Can`t paste a patch into itself!',
[CE.NO_REQUIRED_PATCHES]: 'Can`t find following patches: {missingPatches}',
};

View File

@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
import SnackBarList from '../components/SnackBarList';
import SnackBarMessage from '../components/SnackBarMessage';
import * as ErrorSelectors from '../selectors';
import { deleteMessage } from '../actions';
import { deleteMessage, messageButtonClick } from '../actions';
const ERROR_TIMEOUT = 3000;
@@ -24,6 +24,7 @@ class SnackBar extends React.Component {
this.onMouseOver = this.onMouseOver.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
this.onButtonClicked = this.onButtonClicked.bind(this);
this.onCloseMessage = this.onCloseMessage.bind(this);
}
componentWillReceiveProps(nextProps) {
@@ -48,9 +49,13 @@ class SnackBar extends React.Component {
)(this.messages);
}
onButtonClicked(buttonId, messageData) {
this.props.onClickMessageButton(buttonId, messageData);
this.hideMessage(messageData.id);
onButtonClicked(messageId) {
this.props.onMessageButtonClick(messageId);
this.hideMessage(messageId);
}
onCloseMessage(messageId) {
this.hideMessage(messageId);
}
setHideTimeout(messageData) {
@@ -69,7 +74,7 @@ class SnackBar extends React.Component {
element
.hide()
.then(() => delete this.messages[id])
.then(() => this.props.deleteMessage(id));
.then(() => this.props.onDeleteMessage(id));
}
addMessages(messages) {
@@ -95,6 +100,7 @@ class SnackBar extends React.Component {
key={messageData.id}
message={messageData}
onClickMessageButton={this.onButtonClicked}
onCloseMessage={this.onCloseMessage}
/>
),
};
@@ -121,8 +127,8 @@ class SnackBar extends React.Component {
SnackBar.propTypes = {
errors: PropTypes.object,
onClickMessageButton: PropTypes.func,
deleteMessage: PropTypes.func,
onDeleteMessage: PropTypes.func,
onMessageButtonClick: PropTypes.func,
};
const mapStateToProps = state => ({
@@ -130,7 +136,8 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => (bindActionCreators({
deleteMessage,
onDeleteMessage: deleteMessage,
onMessageButtonClick: messageButtonClick,
}, dispatch));
export default connect(mapStateToProps, mapDispatchToProps)(SnackBar);

View File

@@ -5,7 +5,7 @@ import { getNewId } from './selectors';
export default (messages = {}, action) => {
switch (action.type) {
case MESSAGE_ADD: {
const newId = getNewId(messages);
const newId = (action.meta.id) ? action.meta.id : getNewId(messages);
return R.assoc(
newId,
{

View File

@@ -6,7 +6,7 @@ import { foldMaybe, rejectWithCode } from 'xod-func-tools';
import { addConfirmation, addError } from '../messages/actions';
import { NODETYPE_ERROR_TYPES } from '../editor/constants';
import { PROJECT_BROWSER_ERRORS, NODETYPE_ERRORS } from '../messages/constants';
import { PROJECT_BROWSER_ERRORS } from '../projectBrowser/messages';
import * as ActionType from './actionTypes';
import { isPatchPathTaken } from './utils';
import { getCurrentPatchPath } from '../editor/selectors';
@@ -14,9 +14,10 @@ import { getGrant } from '../user/selectors';
import { fetchGrant } from '../user/actions';
import { LOG_IN_TO_CONTINUE } from '../user/messages';
import { AUTHORIZATION_NEEDED } from '../user/errorCodes';
import { SUCCESSFULLY_PUBLISHED } from './messages';
import { SUCCESSFULLY_PUBLISHED, NODETYPE_ERRORS } from './messages';
import { getProject } from './selectors';
import { getPmSwaggerUrl } from '../utils/urls';
import composeMessage from '../messages/composeMessage';
//
// Project
@@ -114,7 +115,7 @@ export const publishProject = () => (dispatch, getState) => {
})
.catch((err) => {
dispatch({ type: ActionType.PROJECT_PUBLISH_FAIL });
dispatch(addError(err.message));
dispatch(addError(composeMessage(err.message)));
});
};

View File

@@ -1,2 +1,32 @@
// eslint-disable-next-line import/prefer-default-export
export const SUCCESSFULLY_PUBLISHED = 'Successfully published';
import composeMessage from '../messages/composeMessage';
import {
LINK_ERRORS as LE,
NODETYPE_ERROR_TYPES as NTE,
} from '../editor/constants';
export const SUCCESSFULLY_PUBLISHED = composeMessage('Library published');
export const LINK_ERRORS = {
[LE.SAME_DIRECTION]: composeMessage('Canʼt create link between pins of the same direction!'),
[LE.SAME_NODE]: composeMessage('Canʼt create link between pins of the same node!'),
[LE.ONE_LINK_FOR_INPUT_PIN]: composeMessage('Input pin can have only one link!'),
[LE.UNKNOWN_ERROR]: composeMessage('Canʼt create link', 'Unknown error!'),
[LE.INCOMPATIBLE_TYPES]: composeMessage('Incompatible pin types!'),
};
export const NODETYPE_ERRORS = {
[NTE.CANT_DELETE_USED_PATCHNODE]: (
composeMessage(
'Canʼt delete Patch',
'Current Patch Node is used somewhere. You should remove it first!'
)
),
[NTE.CANT_DELETE_USED_PIN_OF_PATCHNODE]: composeMessage(
'Canʼt delete Pin',
[
'Current IO Node is represents a Pin of Patch Node.',
'And it is used somewhere.',
'You should remove a linkage first!',
].join(' ')
),
};

View File

@@ -0,0 +1,8 @@
import composeMessage from '../messages/composeMessage';
// eslint-disable-next-line import/prefer-default-export
export const PROJECT_BROWSER_ERRORS = {
PATCH_NAME_TAKEN: composeMessage('This patch name is already taken!'),
INVALID_PATCH_NAME: composeMessage('Invalid patch name'),
INVALID_PROJECT_NAME: composeMessage('Invalid project name'),
};

View File

@@ -1,3 +1,5 @@
export const INCORRECT_CREDENTIALS = 'Incorrect username or password';
export const SERVICE_UNAVAILABLE = 'Service unavailable';
export const LOG_IN_TO_CONTINUE = 'Please log in to continue';
import composeMessage from '../messages/composeMessage';
export const INCORRECT_CREDENTIALS = composeMessage('Incorrect username or password');
export const SERVICE_UNAVAILABLE = composeMessage('Service unavailable');
export const LOG_IN_TO_CONTINUE = composeMessage('Please log in to continue');

View File

@@ -6,45 +6,66 @@ import '../src/core/styles/main.scss';
import SnackBarMessage from '../src/messages/components/SnackBarMessage';
import { MESSAGE_TYPE } from '../src/messages/constants';
const err = {
id: 1,
const errMsg = {
id: 0,
payload: {
message: 'Something bad just happened',
buttons: [],
title: 'Error',
note: 'Something bad just happened. And we just want to notify you. Without any actions required.',
},
timestamp: 1234567890,
type: MESSAGE_TYPE.ERROR,
};
const errMsgWithButton = {
id: 'errorWithButton',
persistent: true,
payload: {
title: 'Error occured. What shall we do now?',
note: 'Just showed error message, choose what to do now to fix it up?',
button: 'Retry',
},
timestamp: 1234567890,
type: MESSAGE_TYPE.ERROR,
};
const confirmationMsg = {
id: 2,
id: 1,
payload: {
message: 'Please confirm something',
buttons: [],
title: 'Confirmation',
note: 'Message that confirms something. For example, it tells User that Project was successfully saved.',
},
timestamp: 1234567890,
type: MESSAGE_TYPE.CONFIRMATION,
};
const confirmationMsgWithButton = {
id: 'confirmationWithButton',
persistent: true,
payload: {
title: 'Confirmation with buttons',
note: 'We have a new update of XOD. What do you want to do with it?',
button: 'Download & Install',
},
timestamp: 1234567890,
type: MESSAGE_TYPE.CONFIRMATION,
};
const notificationMsg = {
id: 3,
id: 2,
payload: {
message: 'Something happened. Just wanted you to know.',
buttons: [],
title: 'Some notification',
note: 'Something happened. Just wanted you to know.',
},
timestamp: 1234567890,
type: MESSAGE_TYPE.NOTIFICATION,
};
const notificationWithBtns = {
id: 4,
const notificationWithButton = {
id: 'notificationWithButton',
persistent: true,
payload: {
message: 'Something happened. What should we do with it?',
buttons: [
{ id: 'abort', text: 'Abort' },
{ id: 'retry', text: 'Retry' },
{ id: 'ignore', text: 'Ignore' },
],
title: 'Notification with buttons',
note: 'Something happened. What should we do with it?',
button: 'Learn more',
},
timestamp: 1234567890,
type: MESSAGE_TYPE.CONFIRMATION,
@@ -58,7 +79,7 @@ storiesOf('SnackBarMessage', module)
))
.add('error', () => (
<SnackBarMessage
message={err}
message={errMsg}
/>
))
.add('confirmation', () => (
@@ -73,23 +94,34 @@ storiesOf('SnackBarMessage', module)
))
.add('notification with buttons', () => (
<SnackBarMessage
message={notificationWithBtns}
message={notificationWithButton}
onClickMessageButton={action('OnButton')}
/>
))
.add('multiple messages', () => (
<ul className="SnackBarList">
<SnackBarMessage
message={err}
message={errMsg}
onClickMessageButton={action('OnButton')}
/>
<SnackBarMessage
message={errMsgWithButton}
onClickMessageButton={action('OnButton')}
/>
<SnackBarMessage
message={confirmationMsg}
onClickMessageButton={action('OnButton')}
/>
<SnackBarMessage
message={confirmationMsgWithButton}
onClickMessageButton={action('OnButton')}
/>
<SnackBarMessage
message={notificationMsg}
onClickMessageButton={action('OnButton')}
/>
<SnackBarMessage
message={notificationWithBtns}
message={notificationWithButton}
onClickMessageButton={action('OnButton')}
/>
</ul>

View File

@@ -13,7 +13,7 @@ import generateReducers from '../src/core/reducer';
import { getProject } from '../src/project/selectors';
import { NODETYPE_ERROR_TYPES } from '../src/editor/constants';
import { NODETYPE_ERRORS } from '../src/messages/constants';
import { NODETYPE_ERRORS } from '../src/project/messages';
import { addError } from '../src/messages/actions';
import { switchPatch } from '../src/editor/actions';