From 81c43ecc3d17e0dbf7ad1d949b6d977f2c65bd48 Mon Sep 17 00:00:00 2001 From: muhamedsalih-tw <104364298+muhamedsalih-tw@users.noreply.github.com> Date: Thu, 27 Oct 2022 07:13:47 +0530 Subject: fix: 'failed prop' warning in QuickSwitchModal, SettingsNavigation, SettingsWindow and Recipe component tree (#713) * chore: turn off eslint rule @typescript-eslint/no-useless-constructor to initialize dynamic props & state Co-authored-by: Muhamed <> Co-authored-by: Vijay A --- src/features/quickSwitch/Component.js | 357 --------------------------------- src/features/quickSwitch/Component.tsx | 349 ++++++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+), 357 deletions(-) delete mode 100644 src/features/quickSwitch/Component.js create mode 100644 src/features/quickSwitch/Component.tsx (limited to 'src/features/quickSwitch') diff --git a/src/features/quickSwitch/Component.js b/src/features/quickSwitch/Component.js deleted file mode 100644 index 16da22dce..000000000 --- a/src/features/quickSwitch/Component.js +++ /dev/null @@ -1,357 +0,0 @@ -import { Component, createRef } from 'react'; -import { getCurrentWindow } from '@electron/remote'; -import PropTypes from 'prop-types'; -import { observer, inject } from 'mobx-react'; -import { reaction } from 'mobx'; -import injectSheet from 'react-jss'; -import { defineMessages, injectIntl } from 'react-intl'; -import { compact, invoke } from 'lodash'; - -import Input from '../../components/ui/input/index'; -import { H1 } from '../../components/ui/headline'; -import Modal from '../../components/ui/Modal'; -import { state as ModalState } from './store'; -import ServicesStore from '../../stores/ServicesStore'; - -const messages = defineMessages({ - title: { - id: 'feature.quickSwitch.title', - defaultMessage: 'QuickSwitch', - }, - search: { - id: 'feature.quickSwitch.search', - defaultMessage: 'Search...', - }, - info: { - id: 'feature.quickSwitch.info', - defaultMessage: - 'Select a service with TAB, ↑ and ↓. Open a service with ENTER.', - }, -}); - -const styles = theme => ({ - modal: { - width: '80%', - maxWidth: 600, - background: theme.styleTypes.primary.contrast, - paddingTop: 30, - }, - headline: { - fontSize: 20, - marginBottom: 20, - marginTop: -27, - }, - services: { - width: '100%', - maxHeight: '50vh', - overflowX: 'hidden', - overflowY: 'auto', - }, - service: { - background: theme.styleTypes.primary.contrast, - color: theme.colorText, - borderRadius: 6, - padding: '3px 25px', - marginBottom: 10, - display: 'flex', - alignItems: 'center', - '&:last-child': { - marginBottom: 0, - }, - '&:hover': { - cursor: 'pointer', - }, - }, - activeService: { - background: `${theme.styleTypes.primary.accent} !important`, - color: theme.styleTypes.primary.contrast, - cursor: 'pointer', - }, - serviceIcon: { - width: 50, - height: 50, - paddingRight: 20, - objectFit: 'contain', - }, -}); - -class QuickSwitchModal extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - }; - - state = { - selected: 0, - search: '', - wasPrevVisible: false, - }; - - ARROW_DOWN = 40; - - ARROW_UP = 38; - - ENTER = 13; - - TAB = 9; - - inputRef = createRef(); - - serviceElements = {}; - - constructor(props) { - super(props); - - this._handleKeyDown = this._handleKeyDown.bind(this); - this._handleSearchUpdate = this._handleSearchUpdate.bind(this); - this._handleVisibilityChange = this._handleVisibilityChange.bind(this); - this.openService = this.openService.bind(this); - - reaction( - () => ModalState.isModalVisible, - () => { - this._handleVisibilityChange(); - }, - ); - } - - // Add global keydown listener when component mounts - componentDidMount() { - document.addEventListener('keydown', this._handleKeyDown); - } - - // Remove global keydown listener when component unmounts - componentWillUnmount() { - document.removeEventListener('keydown', this._handleKeyDown); - } - - // Get currently shown services - services() { - let services = []; - if ( - this.state.search && - compact(invoke(this.state.search, 'match', /^[\da-z]/i)).length > 0 - ) { - // Apply simple search algorythm to list of all services - services = this.props.stores.services.allDisplayed; - services = services.filter( - service => - service.name.toLowerCase().search(this.state.search.toLowerCase()) !== - -1, - ); - } else if (this.props.stores.services.allDisplayed.length > 0) { - // Add the currently active service first - const currentService = this.props.stores.services.active; - if (currentService) { - services.push(currentService); - } - - // Add last used services to services array - for (const service of this.props.stores.services.lastUsedServices) { - const tempService = this.props.stores.services.one(service); - if (tempService && !services.includes(tempService)) { - services.push(tempService); - } - } - - // Add all other services in the default order - for (const service of this.props.stores.services.allDisplayed) { - if (!services.includes(service)) { - services.push(service); - } - } - } - - return services; - } - - openService(index) { - // Open service - const service = this.services()[index]; - this.props.actions.service.setActive({ serviceId: service.id }); - - // Reset and close modal - this.setState({ - selected: 0, - search: '', - }); - this.close(); - } - - // Change the selected service - // factor should be -1 or 1 - changeSelected(factor) { - this.setState(state => { - let newSelected = state.selected + factor; - const services = this.services().length; - - // Roll around when on edge of list - if (state.selected < 1 && factor === -1) { - newSelected = services - 1; - } else if (state.selected >= services - 1 && factor === 1) { - newSelected = 0; - } - - // Make sure new selection is visible - const serviceElement = this.serviceElements[newSelected]; - if (serviceElement) { - serviceElement.scrollIntoViewIfNeeded(false); - } - - return { - selected: newSelected, - }; - }); - } - - // Handle global key presses to change the selection - _handleKeyDown(event) { - if (ModalState.isModalVisible) { - switch (event.keyCode) { - case this.ARROW_DOWN: - this.changeSelected(1); - break; - case this.TAB: - if (event.shiftKey) { - this.changeSelected(-1); - } else { - this.changeSelected(1); - } - break; - case this.ARROW_UP: - this.changeSelected(-1); - break; - case this.ENTER: - this.openService(this.state.selected); - break; - default: - break; - } - } - } - - // Handle update of the search query - _handleSearchUpdate(evt) { - this.setState({ - search: evt.target.value, - }); - } - - _handleVisibilityChange() { - const { isModalVisible } = ModalState; - - if (isModalVisible && !this.state.wasPrevVisible) { - // Set focus back on current window if its in a service - // TODO: Find a way to gain back focus - getCurrentWindow().blurWebView(); - getCurrentWindow().webContents.focus(); - - // The input "focus" attribute will only work on first modal open - // Manually add focus to the input element - // Wrapped inside timeout to let the modal render first - setTimeout(() => { - if (this.inputRef.current) { - this.inputRef.current.querySelectorAll('input')[0].focus(); - } - }, 10); - - this.setState({ - wasPrevVisible: true, - }); - } else if (!isModalVisible && this.state.wasPrevVisible) { - // Manually blur focus from the input element to prevent - // search query change when modal not visible - setTimeout(() => { - if (this.inputRef.current) { - this.inputRef.current.querySelectorAll('input')[0].blur(); - } - }, 100); - - this.setState({ - wasPrevVisible: false, - }); - } - } - - // Close this modal - close() { - ModalState.isModalVisible = false; - } - - render() { - const { isModalVisible } = ModalState; - - const { openService } = this; - - const { classes } = this.props; - - const services = this.services(); - - const { intl } = this.props; - - return ( - -

- {intl.formatMessage(messages.title)} -

-
- -
- -
- {services.map((service, index) => ( -
openService(index)} - key={service.id} - ref={el => { - this.serviceElements[index] = el; - }} - > - {service.recipe.name} -
{service.name}
-
- ))} -
- -

-
- {intl.formatMessage(messages.info)} -

-
- ); - } -} - -QuickSwitchModal.propTypes = { - stores: PropTypes.shape({ - services: PropTypes.instanceOf(ServicesStore).isRequired, - }).isRequired, - actions: PropTypes.shape({ - service: PropTypes.instanceOf(ServicesStore).isRequired, - }).isRequired, - classes: PropTypes.object.isRequired, -}; - -export default injectIntl( - injectSheet(styles, { injectTheme: true })( - inject('stores', 'actions')(observer(QuickSwitchModal)), - ), -); diff --git a/src/features/quickSwitch/Component.tsx b/src/features/quickSwitch/Component.tsx new file mode 100644 index 000000000..fb85d61e1 --- /dev/null +++ b/src/features/quickSwitch/Component.tsx @@ -0,0 +1,349 @@ +import { ChangeEvent, Component, createRef, ReactElement } from 'react'; +import { getCurrentWindow } from '@electron/remote'; +import { observer, inject } from 'mobx-react'; +import { reaction } from 'mobx'; +import withStyles, { WithStylesProps } from 'react-jss'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { compact, invoke, noop } from 'lodash'; +import { StoresProps } from '../../@types/ferdium-components.types'; +import Service from '../../models/Service'; +import Input from '../../components/ui/input/index'; +import { H1 } from '../../components/ui/headline'; +import Modal from '../../components/ui/Modal'; +import { state as ModalState } from './store'; + +const messages = defineMessages({ + title: { + id: 'feature.quickSwitch.title', + defaultMessage: 'QuickSwitch', + }, + search: { + id: 'feature.quickSwitch.search', + defaultMessage: 'Search...', + }, + info: { + id: 'feature.quickSwitch.info', + defaultMessage: + 'Select a service with TAB, ↑ and ↓. Open a service with ENTER.', + }, +}); + +const styles = theme => ({ + modal: { + width: '80%', + maxWidth: 600, + background: theme.styleTypes.primary.contrast, + paddingTop: 30, + }, + headline: { + fontSize: 20, + marginBottom: 20, + marginTop: -27, + }, + services: { + width: '100%', + maxHeight: '50vh', + overflowX: 'hidden', + overflowY: 'auto', + }, + service: { + background: theme.styleTypes.primary.contrast, + color: theme.colorText, + borderRadius: 6, + padding: '3px 25px', + marginBottom: 10, + display: 'flex', + alignItems: 'center', + '&:last-child': { + marginBottom: 0, + }, + '&:hover': { + cursor: 'pointer', + }, + }, + activeService: { + background: `${theme.styleTypes.primary.accent} !important`, + color: theme.styleTypes.primary.contrast, + cursor: 'pointer', + }, + serviceIcon: { + width: 50, + height: 50, + paddingRight: 20, + objectFit: 'contain', + }, +}); + +interface IProps + extends WithStylesProps, + Partial, + WrappedComponentProps {} + +interface IState { + selected: number; + search: string; + wasPrevVisible: boolean; +} + +@inject('stores', 'actions') +@observer +class QuickSwitchModal extends Component { + ARROW_DOWN = 40; + + ARROW_UP = 38; + + ENTER = 13; + + TAB = 9; + + inputRef = createRef(); + + serviceElements = {}; + + constructor(props) { + super(props); + + this.state = { + selected: 0, + search: '', + wasPrevVisible: false, + }; + + this._handleKeyDown = this._handleKeyDown.bind(this); + this._handleSearchUpdate = this._handleSearchUpdate.bind(this); + this._handleVisibilityChange = this._handleVisibilityChange.bind(this); + this.openService = this.openService.bind(this); + + reaction( + () => ModalState.isModalVisible, + () => { + this._handleVisibilityChange(); + }, + ); + } + + // Add global keydown listener when component mounts + componentDidMount(): void { + document.addEventListener('keydown', this._handleKeyDown); + } + + // Remove global keydown listener when component unmounts + componentWillUnmount(): void { + document.removeEventListener('keydown', this._handleKeyDown); + } + + // Get currently shown services + services(): Service[] { + let services: Service[] = []; + if ( + this.state.search && + compact(invoke(this.state.search, 'match', /^[\da-z]/i)).length > 0 + ) { + // Apply simple search algorythm to list of all services + services = this.props.stores!.services.allDisplayed; + services = services.filter( + service => + service.name.toLowerCase().search(this.state.search.toLowerCase()) !== + -1, + ); + } else if (this.props.stores!.services.allDisplayed.length > 0) { + // Add the currently active service first + const currentService = this.props.stores!.services.active; + if (currentService) { + services.push(currentService); + } + + // Add last used services to services array + for (const service of this.props.stores!.services.lastUsedServices) { + const tempService = this.props.stores!.services.one(service); + if (tempService && !services.includes(tempService)) { + services.push(tempService); + } + } + + // Add all other services in the default order + for (const service of this.props.stores!.services.allDisplayed) { + if (!services.includes(service)) { + services.push(service); + } + } + } + + return services; + } + + openService(index): void { + // Open service + const service = this.services()[index]; + this.props.actions!.service.setActive({ serviceId: service.id }); + + // Reset and close modal + this.setState({ + selected: 0, + search: '', + }); + this.close(); + } + + // Change the selected service + // factor should be -1 or 1 + changeSelected(factor: number): any { + this.setState(state => { + let newSelected = state.selected + factor; + const services = this.services().length; + + // Roll around when on edge of list + if (state.selected < 1 && factor === -1) { + newSelected = services - 1; + } else if (state.selected >= services - 1 && factor === 1) { + newSelected = 0; + } + + // Make sure new selection is visible + const serviceElement = this.serviceElements[newSelected]; + if (serviceElement) { + serviceElement.scrollIntoViewIfNeeded(false); + } + + return { + selected: newSelected, + }; + }); + } + + // Handle global key presses to change the selection + _handleKeyDown(event: KeyboardEvent): void { + if (ModalState.isModalVisible) { + switch (event.keyCode) { + case this.ARROW_DOWN: + this.changeSelected(1); + break; + case this.TAB: + if (event.shiftKey) { + this.changeSelected(-1); + } else { + this.changeSelected(1); + } + break; + case this.ARROW_UP: + this.changeSelected(-1); + break; + case this.ENTER: + this.openService(this.state.selected); + break; + default: + break; + } + } + } + + // Handle update of the search query + _handleSearchUpdate(event: ChangeEvent): void { + this.setState({ + search: event.target.value, + }); + } + + _handleVisibilityChange(): void { + const { isModalVisible } = ModalState; + + if (isModalVisible && !this.state.wasPrevVisible) { + // Set focus back on current window if its in a service + // TODO: Find a way to gain back focus + getCurrentWindow().blurWebView(); + getCurrentWindow().webContents.focus(); + + // The input "focus" attribute will only work on first modal open + // Manually add focus to the input element + // Wrapped inside timeout to let the modal render first + setTimeout(() => { + if (this.inputRef.current) { + this.inputRef.current.querySelectorAll('input')[0].focus(); + } + }, 10); + + this.setState({ + wasPrevVisible: true, + }); + } else if (!isModalVisible && this.state.wasPrevVisible) { + // Manually blur focus from the input element to prevent + // search query change when modal not visible + setTimeout(() => { + if (this.inputRef.current) { + this.inputRef.current.querySelectorAll('input')[0].blur(); + } + }, 100); + + this.setState({ + wasPrevVisible: false, + }); + } + } + + // Close this modal + close(): void { + ModalState.isModalVisible = false; + } + + render(): ReactElement { + const { isModalVisible } = ModalState; + const { openService } = this; + const { classes, intl } = this.props; + const services = this.services(); + + return ( + +

+ {intl.formatMessage(messages.title)} +

+
+ +
+ +
+ {services.map((service, index) => ( +
openService(index)} + onKeyDown={noop} + key={service.id} + ref={el => { + this.serviceElements[index] = el; + }} + > + {service.recipe.name} +
{service.name}
+
+ ))} +
+ +

+
+ {intl.formatMessage(messages.info)} +

+
+ ); + } +} + +export default injectIntl( + withStyles(styles, { injectTheme: true })(QuickSwitchModal), +); -- cgit v1.2.3-70-g09d2