packages/issue-dashboard-widgets/widgets/youtrack-issues-list/app/issues-list-widget.js (384 lines of code) (raw):

import React from 'react'; import PropTypes from 'prop-types'; import {i18n} from 'hub-dashboard-addons/dist/localization'; import ConfigurableWidget from '@jetbrains/hub-widget-ui/dist/configurable-widget'; import ServiceResources from '@jetbrains/hub-widget-ui/dist/service-resources'; import { loadDateFormats, loadIssues, loadTotalIssuesCount, ISSUES_PACK_SIZE } from './resources'; import IssuesListEditForm from './issues-list-edit-form'; import './style/issues-list-widget.css'; import Content from './content'; class IssuesListWidget extends React.Component { static COUNTER_POLLING_PERIOD_SEC = 240; // eslint-disable-line no-magic-numbers static COUNTER_POLLING_PERIOD_MLS = 60000; // eslint-disable-line no-magic-numbers static DEFAULT_REFRESH_PERIOD_SEC = 600; // eslint-disable-line no-magic-numbers static digitToUnicodeSuperScriptDigit = digitSymbol => { const unicodeSuperscriptDigits = [ 0x2070, 0x00B9, 0x00B2, 0x00B3, 0x2074, // eslint-disable-line no-magic-numbers 0x2075, 0x2076, 0x2077, 0x2078, 0x2079 // eslint-disable-line no-magic-numbers ]; return String.fromCharCode(unicodeSuperscriptDigits[Number(digitSymbol)]); }; static getIssueListLink = (homeUrl, context, search) => { // eslint-disable-line complexity let link = homeUrl.charAt(homeUrl.length - 1) === '/' ? homeUrl : `${homeUrl}/`; if (context && context.shortName) { link += `issues/${context.shortName.toLowerCase()}`; } else if (context && context.$type) { if (context.$type.toLowerCase().indexOf('tag') > -1) { link += `tag/${context.name.toLowerCase()}-${context.id.split('-').pop()}`; } else { link += `search/${context.name.toLowerCase()}-${context.id.split('-').pop()}`; } } else { link += 'issues'; } if (search) { link += `?q=${encodeURIComponent(search)}`; } return link; }; static getFullSearchPresentation = (context, search) => [ context && context.name && `#{${context.name}}`, search ].filter(str => !!str).join(' ') || `#${i18n('issues')}`; static getDefaultYouTrackService = async (dashboardApi, predefinedYouTrack) => { if (dashboardApi.loadServices) { return (await dashboardApi.loadServices('YouTrack')). filter( it => (predefinedYouTrack ? it.id === predefinedYouTrack.id : true) )[0]; } try { return await ServiceResources.getYouTrackService( dashboardApi, predefinedYouTrack && predefinedYouTrack.id ); } catch (err) { return null; } }; static youTrackServiceNeedsUpdate = service => !service.name; static getDefaultWidgetTitle = () => i18n('Issues List'); static getWidgetTitle = (search, context, title, issuesCount, youTrack) => { let displayedTitle = title || IssuesListWidget.getFullSearchPresentation(context, search); if (issuesCount > 0) { const superScriptIssuesCount = `${issuesCount}`.split(''). map(IssuesListWidget.digitToUnicodeSuperScriptDigit).join(''); displayedTitle += ` ${superScriptIssuesCount}`; } return { text: displayedTitle, href: youTrack && IssuesListWidget.getIssueListLink( youTrack.homeUrl, context, search ) }; }; static propTypes = { dashboardApi: PropTypes.object, configWrapper: PropTypes.object, registerWidgetApi: PropTypes.func, editable: PropTypes.bool }; constructor(props) { super(props); const {registerWidgetApi} = props; this.state = { isConfiguring: false, isLoading: true, refreshPeriod: IssuesListWidget.DEFAULT_REFRESH_PERIOD_SEC }; this.missedUpdate = false; registerWidgetApi({ onConfigure: () => this.setState({ isConfiguring: true, isLoading: false, isLoadDataError: false }), onRefresh: () => this.loadIssues(), getExternalWidgetOptions: () => ({ authClientId: (this.props.configWrapper.getFieldValue('youTrack') || {}).id }) }); } componentDidMount() { this.initialize(this.props.dashboardApi); } componentWillUnmount = () => { document.removeEventListener('visibilitychange', this.onVisibilityChange); } initialize = async dashboardApi => { document.addEventListener('visibilitychange', this.onVisibilityChange); this.setState({isLoading: true}); await this.props.configWrapper.init(); const youTrackService = await IssuesListWidget.getDefaultYouTrackService( dashboardApi, this.props.configWrapper.getFieldValue('youTrack') ); if (this.props.configWrapper.isNewConfig()) { this.initializeNewWidget(youTrackService); } else { await this.initializeExistingWidget(youTrackService); } }; initializeNewWidget(youTrackService) { if (youTrackService && youTrackService.id) { this.setState({ isConfiguring: true, youTrack: youTrackService, isLoading: false }); } this.setState({isLoadDataError: true, isLoading: false}); } async initializeExistingWidget(youTrackService) { const search = this.props.configWrapper.getFieldValue('search'); const context = this.props.configWrapper.getFieldValue('context'); const refreshPeriod = this.props.configWrapper.getFieldValue('refreshPeriod'); const title = this.props.configWrapper.getFieldValue('title'); this.setState({ title, search: search || '', context, refreshPeriod: refreshPeriod || IssuesListWidget.COUNTER_POLLING_PERIOD_SEC }); await this.showListFromCache(search, context); if (youTrackService && youTrackService.id) { const onYouTrackSpecified = async () => { await this.loadIssues(search, context); const dateFormats = await loadDateFormats( this.fetchYouTrack ); this.setState({dateFormats, isLoading: false}); }; this.setYouTrack(youTrackService, onYouTrackSpecified); } } async showListFromCache(search, context) { const {dashboardApi} = this.props; const cache = (await dashboardApi.readCache() || {}).result; if (cache && cache.search === search && (cache.context || {}).id === (context || {}).id) { this.setState({issues: cache.issues, fromCache: true}); } } setYouTrack(youTrackService, onAfterYouTrackSetFunction) { const {homeUrl} = youTrackService; this.setState({ youTrack: { id: youTrackService.id, homeUrl } }, async () => await onAfterYouTrackSetFunction()); if (IssuesListWidget.youTrackServiceNeedsUpdate(youTrackService)) { const {dashboardApi} = this.props; ServiceResources.getYouTrackService( dashboardApi, youTrackService.id ).then( updatedYouTrackService => { const shouldReSetYouTrack = updatedYouTrackService && !IssuesListWidget.youTrackServiceNeedsUpdate( updatedYouTrackService ) && updatedYouTrackService.homeUrl !== homeUrl; if (shouldReSetYouTrack) { this.setYouTrack( updatedYouTrackService, onAfterYouTrackSetFunction ); if (!this.state.isConfiguring && this.props.editable) { this.props.configWrapper.update({ youTrack: { id: updatedYouTrackService.id, homeUrl: updatedYouTrackService.homeUrl } }); } } } ); } } submitConfiguration = async formParameters => { const { search, title, context, refreshPeriod, selectedYouTrack } = formParameters; this.setYouTrack( selectedYouTrack, async () => { this.setState( {search: search || '', context, title, refreshPeriod}, async () => { await this.loadIssues(); await this.props.configWrapper.replace({ search, context, title, refreshPeriod, youTrack: { id: selectedYouTrack.id, homeUrl: selectedYouTrack.homeUrl } }); this.setState( {isConfiguring: false, fromCache: false} ); } ); } ); }; cancelConfiguration = async () => { if (this.props.configWrapper.isNewConfig()) { await this.props.dashboardApi.removeWidget(); } else { this.setState({isConfiguring: false}); await this.props.dashboardApi.exitConfigMode(); this.initialize(this.props.dashboardApi); } }; fetchYouTrack = async (url, params) => { const {dashboardApi} = this.props; const {youTrack} = this.state; return await dashboardApi.fetch(youTrack.id, url, params); }; editSearchQuery = () => this.setState({isConfiguring: true}); renderConfiguration = () => ( <div className="issues-list-widget"> <IssuesListEditForm search={this.state.search} context={this.state.context} title={this.state.title} refreshPeriod={this.state.refreshPeriod} onSubmit={this.submitConfiguration} onCancel={this.cancelConfiguration} dashboardApi={this.props.dashboardApi} youTrackId={this.state.youTrack.id} /> </div> ); loadIssues = async (search, context) => { if (document.hidden) { this.missedUpdate = true; return; } this.missedUpdate = false; try { await this.loadIssuesUnsafe(search, context); } catch (error) { this.setState({isLoadDataError: true}); } }; onVisibilityChange = () => { if (!document.hidden && this.missedUpdate) { this.loadIssues(); } } loadIssuesUnsafe = async (search, context) => { const currentSearch = search || this.state.search; const currentContext = context || this.state.context; const issues = await loadIssues( this.fetchYouTrack, currentSearch, currentContext ); if (Array.isArray(issues)) { this.setState({issues, fromCache: false, isLoadDataError: false}); this.props.dashboardApi.storeCache({ search: currentSearch, context: currentContext, issues }); this.loadIssuesCount(issues, currentSearch, currentContext); } }; loadNextPageOfIssues = async () => { const {issues, search, context} = this.state; const loadMoreCount = this.getLoadMoreCount(); if (loadMoreCount > 0) { this.setState({isNextPageLoading: true}); const newIssues = await loadIssues( this.fetchYouTrack, search, context, issues.length ); this.setState({ isNextPageLoading: false, issues: issues.concat(newIssues || []) }); } }; loadIssuesCount = async (issues, search, context) => { const issuesCount = (issues.length && issues.length >= ISSUES_PACK_SIZE) ? await loadTotalIssuesCount( this.fetchYouTrack, issues[0], search, context ) : (issues.length || 0); this.setState({issuesCount}); if (issuesCount === -1) { setTimeout( () => this.loadIssuesCount(issues, search, context), IssuesListWidget.COUNTER_POLLING_PERIOD_MLS ); } }; getLoadMoreCount() { const {issuesCount, issues} = this.state; return (issues && issuesCount && issuesCount > issues.length) ? issuesCount - issues.length : 0; } renderContent = () => { const { issues, isLoading, fromCache, isLoadDataError, dateFormats, issuesCount, isNextPageLoading, refreshPeriod, youTrack } = this.state; const millisInSec = 1000; return ( <Content youTrack={youTrack} issues={issues} issuesCount={issuesCount} isLoading={isLoading} fromCache={fromCache} isLoadDataError={isLoadDataError} isNextPageLoading={isNextPageLoading} onLoadMore={this.loadNextPageOfIssues} onEdit={this.editSearchQuery} dateFormats={dateFormats} tickPeriod={refreshPeriod * millisInSec} onTick={this.loadIssues} editable={this.props.editable} /> ); }; // eslint-disable-next-line complexity render() { const { isConfiguring, search, context, title, issuesCount, youTrack } = this.state; const widgetTitle = isConfiguring ? IssuesListWidget.getDefaultWidgetTitle() : IssuesListWidget.getWidgetTitle( search, context, title, issuesCount, youTrack ); return ( <ConfigurableWidget isConfiguring={isConfiguring} dashboardApi={this.props.dashboardApi} widgetTitle={widgetTitle} widgetLoader={this.state.isLoading} Configuration={this.renderConfiguration} Content={this.renderContent} /> ); } } export default IssuesListWidget;