cncjs
721 строка · 23.4 Кб
1import _ from 'lodash';2import classNames from 'classnames';3import Dropzone from 'react-dropzone';4import pubsub from 'pubsub-js';5import React, { PureComponent } from 'react';6import ReactDOM from 'react-dom';7import { withRouter } from 'react-router-dom';8import { Button, ButtonGroup, ButtonToolbar } from 'app/components/Buttons';9import api from 'app/api';10import {11WORKFLOW_STATE_IDLE12} from 'app/constants';13import controller from 'app/lib/controller';14import i18n from 'app/lib/i18n';15import log from 'app/lib/log';16import store from 'app/store';17import * as widgetManager from './WidgetManager';18import DefaultWidgets from './DefaultWidgets';19import PrimaryWidgets from './PrimaryWidgets';20import SecondaryWidgets from './SecondaryWidgets';21import FeederPaused from './modals/FeederPaused';22import FeederWait from './modals/FeederWait';23import ServerDisconnected from './modals/ServerDisconnected';24import styles from './index.styl';25import {26MODAL_NONE,27MODAL_FEEDER_PAUSED,28MODAL_FEEDER_WAIT,29MODAL_SERVER_DISCONNECTED30} from './constants';31
32const WAIT = '%wait';33
34const startWaiting = () => {35// Adds the 'wait' class to <html>36const root = document.documentElement;37root.classList.add('wait');38};39const stopWaiting = () => {40// Adds the 'wait' class to <html>41const root = document.documentElement;42root.classList.remove('wait');43};44
45class Workspace extends PureComponent {46static propTypes = {47...withRouter.propTypes48};49
50state = {51mounted: false,52port: '',53modal: {54name: MODAL_NONE,55params: {}56},57isDraggingFile: false,58isDraggingWidget: false,59isUploading: false,60showPrimaryContainer: store.get('workspace.container.primary.show'),61showSecondaryContainer: store.get('workspace.container.secondary.show'),62inactiveCount: _.size(widgetManager.getInactiveWidgets())63};64
65action = {66openModal: (name = MODAL_NONE, params = {}) => {67this.setState(state => ({68modal: {69name: name,70params: params71}72}));73},74closeModal: () => {75this.setState(state => ({76modal: {77name: MODAL_NONE,78params: {}79}80}));81},82updateModalParams: (params = {}) => {83this.setState(state => ({84modal: {85...state.modal,86params: {87...state.modal.params,88...params89}90}91}));92}93};94
95sortableGroup = {96primary: null,97secondary: null98};99
100primaryContainer = null;101
102secondaryContainer = null;103
104primaryToggler = null;105
106secondaryToggler = null;107
108primaryWidgets = null;109
110secondaryWidgets = null;111
112defaultContainer = null;113
114controllerEvents = {115'connect': () => {116if (controller.connected) {117this.action.closeModal();118} else {119this.action.openModal(MODAL_SERVER_DISCONNECTED);120}121},122'connect_error': () => {123if (controller.connected) {124this.action.closeModal();125} else {126this.action.openModal(MODAL_SERVER_DISCONNECTED);127}128},129'disconnect': () => {130if (controller.connected) {131this.action.closeModal();132} else {133this.action.openModal(MODAL_SERVER_DISCONNECTED);134}135},136'serialport:open': (options) => {137const { port } = options;138this.setState({ port: port });139},140'serialport:close': (options) => {141this.setState({ port: '' });142},143'feeder:status': (status) => {144const { modal } = this.state;145const { hold, holdReason } = { ...status };146
147if (!hold) {148if (_.includes([MODAL_FEEDER_PAUSED, MODAL_FEEDER_WAIT], modal.name)) {149this.action.closeModal();150}151return;152}153
154const { err, data, msg } = { ...holdReason };155
156if (err) {157this.action.openModal(MODAL_FEEDER_PAUSED, {158title: i18n._('Error'),159message: msg,160});161return;162}163
164if (data === WAIT) {165this.action.openModal(MODAL_FEEDER_WAIT, {166title: '%wait',167message: msg,168});169return;170}171
172const title = {173'M0': i18n._('M0 Program Pause'),174'M1': i18n._('M1 Program Pause'),175'M2': i18n._('M2 Program End'),176'M30': i18n._('M30 Program End'),177'M6': i18n._('M6 Tool Change'),178'M109': i18n._('M109 Set Extruder Temperature'),179'M190': i18n._('M190 Set Heated Bed Temperature')180}[data] || data;181
182this.action.openModal(MODAL_FEEDER_PAUSED, {183title: title,184message: msg,185});186}187};188
189widgetEventHandler = {190onForkWidget: (widgetId) => {191// TODO192},193onRemoveWidget: (widgetId) => {194const inactiveWidgets = widgetManager.getInactiveWidgets();195this.setState({ inactiveCount: inactiveWidgets.length });196},197onDragStart: () => {198const { isDraggingWidget } = this.state;199if (!isDraggingWidget) {200this.setState({ isDraggingWidget: true });201}202},203onDragEnd: () => {204const { isDraggingWidget } = this.state;205if (isDraggingWidget) {206this.setState({ isDraggingWidget: false });207}208}209};210
211togglePrimaryContainer = () => {212const { showPrimaryContainer } = this.state;213this.setState({ showPrimaryContainer: !showPrimaryContainer });214
215// Publish a 'resize' event216pubsub.publish('resize'); // Also see "widgets/Visualizer"217};218
219toggleSecondaryContainer = () => {220const { showSecondaryContainer } = this.state;221this.setState({ showSecondaryContainer: !showSecondaryContainer });222
223// Publish a 'resize' event224pubsub.publish('resize'); // Also see "widgets/Visualizer"225};226
227resizeDefaultContainer = () => {228const sidebar = document.querySelector('#sidebar');229const primaryContainer = ReactDOM.findDOMNode(this.primaryContainer);230const secondaryContainer = ReactDOM.findDOMNode(this.secondaryContainer);231const primaryToggler = ReactDOM.findDOMNode(this.primaryToggler);232const secondaryToggler = ReactDOM.findDOMNode(this.secondaryToggler);233const defaultContainer = ReactDOM.findDOMNode(this.defaultContainer);234const { showPrimaryContainer, showSecondaryContainer } = this.state;235
236{ // Mobile-Friendly View237const { location } = this.props;238const disableHorizontalScroll = !(showPrimaryContainer && showSecondaryContainer);239
240if (location.pathname === '/workspace' && disableHorizontalScroll) {241// Disable horizontal scroll242document.body.scrollLeft = 0;243document.body.style.overflowX = 'hidden';244} else {245// Enable horizontal scroll246document.body.style.overflowX = '';247}248}249
250if (showPrimaryContainer) {251defaultContainer.style.left = primaryContainer.offsetWidth + sidebar.offsetWidth + 'px';252} else {253defaultContainer.style.left = primaryToggler.offsetWidth + sidebar.offsetWidth + 'px';254}255
256if (showSecondaryContainer) {257defaultContainer.style.right = secondaryContainer.offsetWidth + 'px';258} else {259defaultContainer.style.right = secondaryToggler.offsetWidth + 'px';260}261
262// Publish a 'resize' event263pubsub.publish('resize'); // Also see "widgets/Visualizer"264};265
266onDrop = (files) => {267const { port } = this.state;268
269if (!port) {270return;271}272
273let file = files[0];274let reader = new FileReader();275
276reader.onloadend = (event) => {277const { result, error } = event.target;278
279if (error) {280log.error(error);281return;282}283
284log.debug('FileReader:', _.pick(file, [285'lastModified',286'lastModifiedDate',287'meta',288'name',289'size',290'type'291]));292
293startWaiting();294this.setState({ isUploading: true });295
296const name = file.name;297const gcode = result;298
299api.loadGCode({ port, name, gcode })300.then((res) => {301const { name = '', gcode = '' } = { ...res.body };302pubsub.publish('gcode:load', { name, gcode });303})304.catch((res) => {305log.error('Failed to upload G-code file');306})307.then(() => {308stopWaiting();309this.setState({ isUploading: false });310});311};312
313try {314reader.readAsText(file);315} catch (err) {316// Ignore error317}318};319
320updateWidgetsForPrimaryContainer = () => {321widgetManager.show((activeWidgets, inactiveWidgets) => {322const widgets = Object.keys(store.get('widgets', {}))323.filter(widgetId => {324// e.g. "webcam" or "webcam:d8e6352f-80a9-475f-a4f5-3e9197a48a23"325const name = widgetId.split(':')[0];326return _.includes(activeWidgets, name);327});328
329const defaultWidgets = store.get('workspace.container.default.widgets');330const sortableWidgets = _.difference(widgets, defaultWidgets);331let primaryWidgets = store.get('workspace.container.primary.widgets');332let secondaryWidgets = store.get('workspace.container.secondary.widgets');333
334primaryWidgets = sortableWidgets.slice();335_.pullAll(primaryWidgets, secondaryWidgets);336pubsub.publish('updatePrimaryWidgets', primaryWidgets);337
338secondaryWidgets = sortableWidgets.slice();339_.pullAll(secondaryWidgets, primaryWidgets);340pubsub.publish('updateSecondaryWidgets', secondaryWidgets);341
342// Update inactive count343this.setState({ inactiveCount: _.size(inactiveWidgets) });344});345};346
347updateWidgetsForSecondaryContainer = () => {348widgetManager.show((activeWidgets, inactiveWidgets) => {349const widgets = Object.keys(store.get('widgets', {}))350.filter(widgetId => {351// e.g. "webcam" or "webcam:d8e6352f-80a9-475f-a4f5-3e9197a48a23"352const name = widgetId.split(':')[0];353return _.includes(activeWidgets, name);354});355
356const defaultWidgets = store.get('workspace.container.default.widgets');357const sortableWidgets = _.difference(widgets, defaultWidgets);358let primaryWidgets = store.get('workspace.container.primary.widgets');359let secondaryWidgets = store.get('workspace.container.secondary.widgets');360
361secondaryWidgets = sortableWidgets.slice();362_.pullAll(secondaryWidgets, primaryWidgets);363pubsub.publish('updateSecondaryWidgets', secondaryWidgets);364
365primaryWidgets = sortableWidgets.slice();366_.pullAll(primaryWidgets, secondaryWidgets);367pubsub.publish('updatePrimaryWidgets', primaryWidgets);368
369// Update inactive count370this.setState({ inactiveCount: _.size(inactiveWidgets) });371});372};373
374componentDidMount() {375this.addControllerEvents();376this.addResizeEventListener();377
378setTimeout(() => {379// A workaround solution to trigger componentDidUpdate on initial render380this.setState({ mounted: true });381}, 0);382}383
384componentWillUnmount() {385this.removeControllerEvents();386this.removeResizeEventListener();387}388
389componentDidUpdate() {390store.set('workspace.container.primary.show', this.state.showPrimaryContainer);391store.set('workspace.container.secondary.show', this.state.showSecondaryContainer);392
393this.resizeDefaultContainer();394}395
396addControllerEvents() {397Object.keys(this.controllerEvents).forEach(eventName => {398const callback = this.controllerEvents[eventName];399controller.addListener(eventName, callback);400});401}402
403removeControllerEvents() {404Object.keys(this.controllerEvents).forEach(eventName => {405const callback = this.controllerEvents[eventName];406controller.removeListener(eventName, callback);407});408}409
410addResizeEventListener() {411this.onResizeThrottled = _.throttle(this.resizeDefaultContainer, 50);412window.addEventListener('resize', this.onResizeThrottled);413}414
415removeResizeEventListener() {416window.removeEventListener('resize', this.onResizeThrottled);417this.onResizeThrottled = null;418}419
420render() {421const { style, className } = this.props;422const {423port,424modal,425isDraggingFile,426isDraggingWidget,427showPrimaryContainer,428showSecondaryContainer,429inactiveCount
430} = this.state;431const hidePrimaryContainer = !showPrimaryContainer;432const hideSecondaryContainer = !showSecondaryContainer;433
434return (435<div style={style} className={classNames(className, styles.workspace)}>436{modal.name === MODAL_FEEDER_PAUSED && (437<FeederPaused438title={modal.params.title}439message={modal.params.message}440onClose={this.action.closeModal}441/>442)}443{modal.name === MODAL_FEEDER_WAIT && (444<FeederWait445title={modal.params.title}446message={modal.params.message}447onClose={this.action.closeModal}448/>449)}450{modal.name === MODAL_SERVER_DISCONNECTED &&451<ServerDisconnected />452}453<div454className={classNames(455styles.dropzoneOverlay,456{ [styles.hidden]: !(port && isDraggingFile) }457)}458>459<div className={styles.textBlock}>460{i18n._('Drop G-code file here')}461</div>462</div>463<Dropzone464className={styles.dropzone}465disabled={controller.workflow.state !== WORKFLOW_STATE_IDLE}466disableClick={true}467disablePreview={true}468multiple={false}469onDragStart={(event) => {470}}471onDragEnter={(event) => {472if (controller.workflow.state !== WORKFLOW_STATE_IDLE) {473return;474}475if (isDraggingWidget) {476return;477}478if (!isDraggingFile) {479this.setState({ isDraggingFile: true });480}481}}482onDragLeave={(event) => {483if (controller.workflow.state !== WORKFLOW_STATE_IDLE) {484return;485}486if (isDraggingWidget) {487return;488}489if (isDraggingFile) {490this.setState({ isDraggingFile: false });491}492}}493onDrop={(acceptedFiles, rejectedFiles) => {494if (controller.workflow.state !== WORKFLOW_STATE_IDLE) {495return;496}497if (isDraggingWidget) {498return;499}500if (isDraggingFile) {501this.setState({ isDraggingFile: false });502}503this.onDrop(acceptedFiles);504}}505>506<div className={styles.workspaceTable}>507<div className={styles.workspaceTableRow}>508<div509ref={node => {510this.primaryContainer = node;511}}512className={classNames(513styles.primaryContainer,514{ [styles.hidden]: hidePrimaryContainer }515)}516>517<ButtonToolbar style={{ margin: '5px 0' }}>518<ButtonGroup519style={{ marginLeft: 0, marginRight: 10 }}520btnSize="sm"521btnStyle="flat"522>523<Button524style={{ minWidth: 30 }}525compact
526onClick={this.togglePrimaryContainer}527>528<i className="fa fa-chevron-left" />529</Button>530</ButtonGroup>531<ButtonGroup532style={{ marginLeft: 0, marginRight: 10 }}533btnSize="sm"534btnStyle="flat"535>536<Button537style={{ width: 230 }}538onClick={this.updateWidgetsForPrimaryContainer}539>540<i className="fa fa-list-alt" />541{i18n._('Manage Widgets ({{inactiveCount}})', {542inactiveCount: inactiveCount543})}544</Button>545</ButtonGroup>546<ButtonGroup547style={{ marginLeft: 0, marginRight: 0 }}548btnSize="sm"549btnStyle="flat"550>551<Button552style={{ minWidth: 30 }}553compact
554title={i18n._('Collapse All')}555onClick={event => {556this.primaryWidgets.collapseAll();557}}558>559<i className="fa fa-chevron-up" style={{ fontSize: 14 }} />560</Button>561<Button562style={{ minWidth: 30 }}563compact
564title={i18n._('Expand All')}565onClick={event => {566this.primaryWidgets.expandAll();567}}568>569<i className="fa fa-chevron-down" style={{ fontSize: 14 }} />570</Button>571</ButtonGroup>572</ButtonToolbar>573<PrimaryWidgets574ref={node => {575this.primaryWidgets = node;576}}577onForkWidget={this.widgetEventHandler.onForkWidget}578onRemoveWidget={this.widgetEventHandler.onRemoveWidget}579onDragStart={this.widgetEventHandler.onDragStart}580onDragEnd={this.widgetEventHandler.onDragEnd}581/>582</div>583{hidePrimaryContainer && (584<div585ref={node => {586this.primaryToggler = node;587}}588className={styles.primaryToggler}589>590<ButtonGroup591btnSize="sm"592btnStyle="flat"593>594<Button595style={{ minWidth: 30 }}596compact
597onClick={this.togglePrimaryContainer}598>599<i className="fa fa-chevron-right" />600</Button>601</ButtonGroup>602</div>603)}604<div605ref={node => {606this.defaultContainer = node;607}}608className={classNames(609styles.defaultContainer,610styles.fixed611)}612>613<DefaultWidgets />614</div>615{hideSecondaryContainer && (616<div617ref={node => {618this.secondaryToggler = node;619}}620className={styles.secondaryToggler}621>622<ButtonGroup623btnSize="sm"624btnStyle="flat"625>626<Button627style={{ minWidth: 30 }}628compact
629onClick={this.toggleSecondaryContainer}630>631<i className="fa fa-chevron-left" />632</Button>633</ButtonGroup>634</div>635)}636<div637ref={node => {638this.secondaryContainer = node;639}}640className={classNames(641styles.secondaryContainer,642{ [styles.hidden]: hideSecondaryContainer }643)}644>645<ButtonToolbar style={{ margin: '5px 0' }}>646<div className="pull-left">647<ButtonGroup648style={{ marginLeft: 0, marginRight: 10 }}649btnSize="sm"650btnStyle="flat"651>652<Button653style={{ minWidth: 30 }}654compact
655title={i18n._('Collapse All')}656onClick={event => {657this.secondaryWidgets.collapseAll();658}}659>660<i className="fa fa-chevron-up" style={{ fontSize: 14 }} />661</Button>662<Button663style={{ minWidth: 30 }}664compact
665title={i18n._('Expand All')}666onClick={event => {667this.secondaryWidgets.expandAll();668}}669>670<i className="fa fa-chevron-down" style={{ fontSize: 14 }} />671</Button>672</ButtonGroup>673<ButtonGroup674style={{ marginLeft: 0, marginRight: 10 }}675btnSize="sm"676btnStyle="flat"677>678<Button679style={{ width: 230 }}680onClick={this.updateWidgetsForSecondaryContainer}681>682<i className="fa fa-list-alt" />683{i18n._('Manage Widgets ({{inactiveCount}})', {684inactiveCount: inactiveCount685})}686</Button>687</ButtonGroup>688<ButtonGroup689style={{ marginLeft: 0, marginRight: 0 }}690btnSize="sm"691btnStyle="flat"692>693<Button694style={{ minWidth: 30 }}695compact
696onClick={this.toggleSecondaryContainer}697>698<i className="fa fa-chevron-right" />699</Button>700</ButtonGroup>701</div>702</ButtonToolbar>703<SecondaryWidgets704ref={node => {705this.secondaryWidgets = node;706}}707onForkWidget={this.widgetEventHandler.onForkWidget}708onRemoveWidget={this.widgetEventHandler.onRemoveWidget}709onDragStart={this.widgetEventHandler.onDragStart}710onDragEnd={this.widgetEventHandler.onDragEnd}711/>712</div>713</div>714</div>715</Dropzone>716</div>717);718}719}
720
721export default withRouter(Workspace);722