diff --git a/app/scripts/modules/core/src/cluster/filter/FilterSection.tsx b/app/scripts/modules/core/src/cluster/filter/FilterSection.tsx new file mode 100644 index 00000000000..6a8963372c0 --- /dev/null +++ b/app/scripts/modules/core/src/cluster/filter/FilterSection.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { BindAll } from 'lodash-decorators'; + +import { HelpField } from 'core/help/HelpField'; + +export interface IFilterSectionProps { + heading: string; + expanded?: boolean; + helpKey?: string; +} + +export interface IFilterSectionState { + expanded: boolean; +} + +@BindAll() +export class FilterSection extends React.Component { + constructor(props: IFilterSectionProps) { + super(props); + this.state = { expanded: props.expanded }; + } + + public getIcon() { + return this.state.expanded ? 'down' : 'right'; + } + + public toggle() { + this.setState({ expanded: !this.state.expanded }); + } + + public render() { + return ( +
+
+

+ + {` ${this.props.heading}`} + {this.props.helpKey && ( )} +

+
+ { this.state.expanded && ( +
+ {this.props.children} +
+ )} +
+ ); + } +} diff --git a/app/scripts/modules/core/src/cluster/filter/clusterFilter.component.ts b/app/scripts/modules/core/src/cluster/filter/clusterFilter.component.ts index 07b2b4e9e5a..1804dce26f2 100644 --- a/app/scripts/modules/core/src/cluster/filter/clusterFilter.component.ts +++ b/app/scripts/modules/core/src/cluster/filter/clusterFilter.component.ts @@ -1,11 +1,12 @@ -import {compact, uniq, map} from 'lodash'; -import {IScope, module} from 'angular'; +import { IScope, module } from 'angular'; +import { compact, uniq, map } from 'lodash'; +import { Subscription } from 'rxjs'; -import {CLUSTER_FILTER_SERVICE, ClusterFilterService} from 'core/cluster/filter/clusterFilter.service'; -import {Application} from 'core/application/application.model'; +import { Application } from 'core/application/application.model'; import { CLUSTER_FILTER_MODEL, ClusterFilterModel } from './clusterFilter.model'; -import { Subscription } from 'rxjs'; +import { CLUSTER_FILTER_SERVICE, ClusterFilterService } from 'core/cluster/filter/clusterFilter.service'; import { IFilterTag } from 'core/filterModel/FilterTags'; + export const CLUSTER_FILTER = 'spinnaker.core.cluster.filter.component'; const ngmodule = module(CLUSTER_FILTER, [ diff --git a/app/scripts/modules/core/src/cluster/filter/collapsibleFilterSection.directive.js b/app/scripts/modules/core/src/cluster/filter/collapsibleFilterSection.directive.js index 1c7eb4c2ced..a7512a29bb3 100644 --- a/app/scripts/modules/core/src/cluster/filter/collapsibleFilterSection.directive.js +++ b/app/scripts/modules/core/src/cluster/filter/collapsibleFilterSection.directive.js @@ -4,7 +4,7 @@ const angular = require('angular'); module.exports = angular .module('cluster.filter.collapse', []) - .directive('filterSection', function ($timeout) { + .directive('filterSection', function () { return { restrict: 'E', transclude: true, @@ -14,7 +14,7 @@ module.exports = angular helpKey: '@?' }, templateUrl: require('./collapsibleFilterSection.html'), - link: function (scope, elem) { + link: function (scope) { var expanded = (scope.expanded === 'true'); scope.state = {expanded: expanded}; scope.getIcon = function () { @@ -24,50 +24,6 @@ module.exports = angular scope.toggle = function () { scope.state.expanded = !scope.state.expanded; }; - - scope.$on('parent::clearAll', function() { - $timeout(function() { // This is needed b/c we don't want to trigger another digest cycle while one is currently in flight. - elem.find(':checked').trigger('click'); - elem.find('input').val('').trigger('change'); - }, 0, false); - }); }, - - controller: function($scope) { - $scope.$on('parent::toggle', function(event, isShow) { - $scope.state = {expanded: isShow }; - }); - } - }; - }) - .directive('filterToggleAll', function () { - return { - restrict: 'E', - transclude: true, - template: [ - '
', - '
', - '', - '', - '
', - '
', - '
' - ].join(''), - controller: function ($scope) { - function toggle() { - $scope.show = !$scope.show; - $scope.$broadcast('parent::toggle', $scope.show); - $scope.buttonName = ($scope.show ? 'Hide All' : 'Show All'); - } - - function clearAll() { - $scope.$broadcast('parent::clearAll'); - } - - $scope.show = false; - $scope.buttonName = ($scope.show ? 'Hide All' : 'Show All'); - $scope.toggle = toggle; - $scope.clearAll = clearAll; - } }; }); diff --git a/app/scripts/modules/core/src/delivery/delivery.module.js b/app/scripts/modules/core/src/delivery/delivery.module.js index b82df758514..d5c4362370b 100644 --- a/app/scripts/modules/core/src/delivery/delivery.module.js +++ b/app/scripts/modules/core/src/delivery/delivery.module.js @@ -8,6 +8,7 @@ import { EXECUTION_DETAILS_CONTROLLER } from './details/executionDetails.control import { BUILD_DISPLAY_NAME_FILTER } from './executionBuild/buildDisplayName.filter'; import { EXECUTION_BUILD_NUMBER_COMPONENT } from './executionBuild/executionBuildNumber.component'; import { EXECUTION_COMPONENT } from './executionGroup/execution/execution.component'; +import { EXECUTION_FILTERS_COMPONENT } from './filter/executionFilters.component'; import { EXECUTION_GROUPS_COMPONENT } from './executionGroup/executionGroups.component'; import { EXECUTIONS_COMPONENT } from './executions/executions.component'; import { STAGE_FAILURE_MESSAGE_COMPONENT } from './stageFailureMessage/stageFailureMessage.component'; @@ -27,7 +28,7 @@ module.exports = angular.module('spinnaker.delivery', [ BUILD_DISPLAY_NAME_FILTER, EXECUTION_BUILD_NUMBER_COMPONENT, - require('./filter/executionFilters.directive.js').name, + EXECUTION_FILTERS_COMPONENT, require('./manualExecution/manualPipelineExecution.controller.js').name, diff --git a/app/scripts/modules/core/src/delivery/filter/ExecutionFilters.tsx b/app/scripts/modules/core/src/delivery/filter/ExecutionFilters.tsx new file mode 100644 index 00000000000..ac431d7898d --- /dev/null +++ b/app/scripts/modules/core/src/delivery/filter/ExecutionFilters.tsx @@ -0,0 +1,285 @@ +import * as React from 'react'; +import * as ReactGA from 'react-ga'; +import { get, orderBy, uniq } from 'lodash'; +import { BindAll, Debounce } from 'lodash-decorators'; +import { $q } from 'ngimport'; +import { SortableContainer, SortableElement, SortableHandle, arrayMove, SortEnd } from 'react-sortable-hoc'; +import { Subscription } from 'rxjs'; + +import { Application } from 'core/application'; +import { FilterSection } from 'core/cluster/filter/FilterSection'; +import { IPipeline } from 'core/domain'; +import { ReactInjector } from 'core/reactShims'; + +import './executionFilters.less'; + +export interface IExecutionFiltersProps { + application: Application; +} + +export interface IExecutionFiltersState { + pipelineNames: string[]; + pipelineReorderEnabled: boolean; + tags: any[]; +} + +const DragHandle = SortableHandle(() => ( + +)); + +@BindAll() +export class ExecutionFilters extends React.Component { + private executionsRefreshUnsubscribe: () => void; + private groupsUpdatedSubscription: Subscription; + private locationChangeUnsubscribe: Function; + private pipelineConfigsRefreshUnsubscribe: () => void; + + constructor(props: IExecutionFiltersProps) { + const { executionFilterModel } = ReactInjector; + super(props); + + this.state = { + pipelineNames: this.getPipelineNames(), + pipelineReorderEnabled: false, + tags: executionFilterModel.asFilterModel.tags, + } + } + + public componentDidMount(): void { + const { application } = this.props; + const { executionFilterModel, executionFilterService } = ReactInjector; + + this.executionsRefreshUnsubscribe = application.executions.onRefresh(null, () => { this.refreshPipelines(); }); + this.groupsUpdatedSubscription = executionFilterService.groupsUpdatedStream.subscribe(() => this.setState({ tags: executionFilterModel.asFilterModel.tags })); + this.pipelineConfigsRefreshUnsubscribe = application.pipelineConfigs.onRefresh(null, () => { this.refreshPipelines(); }); + + this.initialize(); + this.locationChangeUnsubscribe = ReactInjector.$uiRouter.transitionService.onSuccess({}, () => { + executionFilterModel.asFilterModel.activate(); + executionFilterService.updateExecutionGroups(application); + }); + } + + private enablePipelineReorder(): void { + this.setState({ pipelineReorderEnabled: true }); + ReactGA.event({category: 'Pipelines', action: 'Filter: reorder'}); + }; + + private disablePipelineReorder(): void { + this.setState({ pipelineReorderEnabled: false }); + ReactGA.event({category: 'Pipelines', action: 'Filter: stop reorder'}); + }; + + private updateExecutionGroups(): void { + ReactInjector.executionFilterModel.asFilterModel.applyParamsToUrl(); + ReactInjector.executionFilterService.updateExecutionGroups(this.props.application); + } + + private refreshExecutions(): void { + ReactInjector.executionFilterModel.asFilterModel.applyParamsToUrl(); + this.props.application.executions.reloadingForFilters = true; + this.props.application.executions.refresh(); + } + + private clearFilters(): void { + ReactGA.event({category: 'Pipelines', action: `Filter: clear all (side nav)`}); + ReactInjector.executionFilterService.clearFilters(); + ReactInjector.executionFilterService.updateExecutionGroups(this.props.application); + } + + private getPipelineNames(): string[] { + const { application } = this.props; + if (application.pipelineConfigs.loadFailure) { + return []; + } + const configs = (application.pipelineConfigs.data || []).concat(application.strategyConfigs.data || []); + const allOptions = orderBy(configs, ['strategy', 'index'], ['desc', 'asc']) + .concat(application.executions.data) + .filter((option: any) => option && option.name) + .map((option: any) => option.name); + return uniq(allOptions); + } + + private refreshPipelines(): void { + this.setState({ pipelineNames: this.getPipelineNames() }); + this.initialize(); + } + + private initialize(): void { + const { application } = this.props; + if (application.pipelineConfigs.loadFailure) { + return; + } + this.updateExecutionGroups(); + application.executions.reloadingForFilters = false; + } + + public componentWillUnmount(): void { + this.groupsUpdatedSubscription.unsubscribe(); + this.locationChangeUnsubscribe(); + this.executionsRefreshUnsubscribe(); + this.pipelineConfigsRefreshUnsubscribe(); + } + + @Debounce(300) + private updateFilterSearch(searchString: string): void { + const sortFilter = ReactInjector.executionFilterModel.asFilterModel.sortFilter; + sortFilter.filter = searchString; + ReactGA.event({category: 'Pipelines', action: 'Filter: search', label: sortFilter.filter}); + this.updateExecutionGroups(); + } + + private searchFieldUpdated(event: React.FormEvent): void { + this.updateFilterSearch(event.currentTarget.value); + } + + private updatePipelines(pipelines: IPipeline[]): void { + $q.all(pipelines.map((pipeline) => ReactInjector.pipelineConfigService.savePipeline(pipeline))); + }; + + private handleSortEnd(sortEnd: SortEnd): void { + const pipelineNames = arrayMove(this.state.pipelineNames, sortEnd.oldIndex, sortEnd.newIndex); + const { application } = this.props; + ReactGA.event({category: 'Pipelines', action: 'Reordered pipeline'}); + const dirty: IPipeline[] = []; + application.pipelineConfigs.data.concat(application.strategyConfigs.data).forEach((pipeline: IPipeline) => { + const newIndex = pipelineNames.indexOf(pipeline.name); + if (pipeline.index !== newIndex) { + pipeline.index = newIndex; + dirty.push(pipeline); + } + }); + this.updatePipelines(dirty); + this.refreshPipelines(); + } + + public render() { + const { pipelineNames, pipelineReorderEnabled, tags } = this.state; + + return ( +
+
+
+ 0 ? 'visible' : 'hidden'}} + > + Clear All + + + +
+
+ +
+
+
+
+
+ +
+ + { pipelineNames.length && ( +
+ { !pipelineReorderEnabled && ( + Reorder Pipelines + + )} + { pipelineReorderEnabled && ( + Done + + )} +
+ )} +
+
+ + +
+ + + + + + +
+
+ +
+
+
+ ); + } +} + +const FilterCheckbox = (props: { pipeline: string, visible: boolean, update: () => void }): JSX.Element => { + const { pipeline, visible, update } = props; + const sortFilter = ReactInjector.executionFilterModel.asFilterModel.sortFilter; + const changeHandler = () => { + ReactGA.event({category: 'Pipelines', action: 'Filter: pipeline', label: pipeline}); + sortFilter.pipeline[pipeline] = !sortFilter.pipeline[pipeline]; + update(); + }; + return ( + + ); +} + +const Pipeline = SortableElement((props: { pipeline: string, dragEnabled: boolean, update: () => void }) => ( +
+
+ +
+
+)); + +const Pipelines = SortableContainer((props: { names: string[], dragEnabled: boolean, update: () => void }) => ( +
+ {props.names.map((pipeline, index) => )} +
+)); + +const FilterStatus = (props: { status: string, label: string, refresh: () => void }): JSX.Element => { + const sortFilter = ReactInjector.executionFilterModel.asFilterModel.sortFilter; + const changed = () => { + ReactGA.event({category: 'Pipelines', action: 'Filter: status', label: props.label.toUpperCase()}); + sortFilter.status[props.status] = !sortFilter.status[props.status]; + props.refresh(); + } + return ( +
+ +
+ ); +} diff --git a/app/scripts/modules/core/src/delivery/filter/executionFilter.controller.js b/app/scripts/modules/core/src/delivery/filter/executionFilter.controller.js deleted file mode 100644 index 0bad43e7fd6..00000000000 --- a/app/scripts/modules/core/src/delivery/filter/executionFilter.controller.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict'; - -import _ from 'lodash'; - -const angular = require('angular'); - -import {EXECUTION_FILTER_MODEL} from 'core/delivery/filter/executionFilter.model'; -import {EXECUTION_FILTER_SERVICE} from 'core/delivery/filter/executionFilter.service'; -import {PIPELINE_CONFIG_SERVICE} from 'core/pipeline/config/services/pipelineConfig.service'; - -module.exports = angular.module('spinnaker.core.delivery.filter.executionFilter.controller', [ - EXECUTION_FILTER_SERVICE, - EXECUTION_FILTER_MODEL, - require('angulartics'), - PIPELINE_CONFIG_SERVICE -]) - .controller('ExecutionFilterCtrl', function ($scope, $rootScope, $q, pipelineConfigService, - executionFilterService, executionFilterModel, $analytics) { - this.tags = executionFilterModel.tags; - $scope.sortFilter = executionFilterModel.sortFilter; - - this.viewState = { - pipelineReorderDisabled: true, - }; - - this.enablePipelineReorder = () => { - this.viewState.pipelineReorderDisabled = false; - this.pipelineSortOptions.disabled = false; - }; - - this.disablePipelineReorder = () => { - this.viewState.pipelineReorderDisabled = true; - this.pipelineSortOptions.disabled = true; - }; - - this.updateExecutionGroups = (reload) => { - executionFilterModel.applyParamsToUrl(); - if (reload) { - this.application.executions.reloadingForFilters = true; - this.application.executions.refresh(); - } else { - executionFilterService.updateExecutionGroups(this.application); - } - }; - - this.clearFilters = () => { - executionFilterService.clearFilters(); - executionFilterService.updateExecutionGroups(this.application); - }; - - this.initialize = () => { - if (this.application.pipelineConfigs.loadFailure) { - return; - } - let configs = (this.application.pipelineConfigs.data || []).concat(this.application.strategyConfigs.data || []); - let allOptions = _.orderBy(configs, ['strategy', 'index'], ['desc', 'asc']) - .concat(this.application.executions.data) - .filter((option) => option && option.name) - .map((option) => option.name); - this.pipelineNames = _.uniq(allOptions); - this.updateExecutionGroups(); - this.application.executions.reloadingForFilters = false; - this.groupsUpdatedSubscription = executionFilterService.groupsUpdatedStream.subscribe(() => this.tags = executionFilterModel.tags); - }; - - this.application.executions.onRefresh($scope, this.initialize); - this.application.pipelineConfigs.onRefresh($scope, this.initialize); - - this.initialize(); - - this.locationChangeUnsubscribe = $rootScope.$on('$locationChangeSuccess', () => { - executionFilterModel.activate(); - executionFilterService.updateExecutionGroups(this.application); - }); - - $scope.$on('$destroy', () => { - if (this.groupsUpdatedSubscription) { - this.groupsUpdatedSubscription.unsubscribe(); - } - this.locationChangeUnsubscribe(); - }); - - let updatePipelines = (pipelines) => { - $q.all(pipelines.map(function(pipeline) { - return pipelineConfigService.savePipeline(pipeline); - })); - }; - - this.pipelineSortOptions = { - axis: 'y', - delay: 150, - disabled: true, - stop: () => { - $analytics.eventTrack('Reordered pipeline', {category: 'Pipelines'}); - const dirty = []; - this.application.pipelineConfigs.data.concat(this.application.strategyConfigs.data).forEach((pipeline) => { - const newIndex = this.pipelineNames.indexOf(pipeline.name); - if (pipeline.index !== newIndex) { - pipeline.index = newIndex; - dirty.push(pipeline); - } - }); - updatePipelines(dirty); - this.initialize(); - } - }; - - } -); diff --git a/app/scripts/modules/core/src/delivery/filter/executionFilter.model.ts b/app/scripts/modules/core/src/delivery/filter/executionFilter.model.ts index 73ece6f11de..f04abfaa7f2 100644 --- a/app/scripts/modules/core/src/delivery/filter/executionFilter.model.ts +++ b/app/scripts/modules/core/src/delivery/filter/executionFilter.model.ts @@ -11,8 +11,8 @@ import { UrlParser } from 'core/navigation/urlParser'; export const filterModelConfig: IFilterConfig[] = [ { model: 'filter', param: 'q', clearValue: '', type: 'string', filterLabel: 'search', }, - { model: 'pipeline', param: 'pipeline', type: 'trueKeyObject', }, - { model: 'status', type: 'trueKeyObject', }, + { model: 'pipeline', param: 'pipeline', type: 'trueKeyObject', clearValue: {}}, + { model: 'status', type: 'trueKeyObject', clearValue: {}, }, ]; export interface IExecutionFilterModel extends IFilterModel { diff --git a/app/scripts/modules/core/src/delivery/filter/executionFilters.component.ts b/app/scripts/modules/core/src/delivery/filter/executionFilters.component.ts new file mode 100644 index 00000000000..91fb7715930 --- /dev/null +++ b/app/scripts/modules/core/src/delivery/filter/executionFilters.component.ts @@ -0,0 +1,8 @@ +import { module } from 'angular'; +import { react2angular } from 'react2angular'; + +import { ExecutionFilters } from './ExecutionFilters'; + +export const EXECUTION_FILTERS_COMPONENT = 'spinnaker.core.delivery.filter.executionFilters.component'; +module(EXECUTION_FILTERS_COMPONENT, []) + .component('executionFilters', react2angular(ExecutionFilters, ['application'])); diff --git a/app/scripts/modules/core/src/delivery/filter/executionFilters.directive.js b/app/scripts/modules/core/src/delivery/filter/executionFilters.directive.js deleted file mode 100644 index 2484d4cfbf9..00000000000 --- a/app/scripts/modules/core/src/delivery/filter/executionFilters.directive.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const angular = require('angular'); - -import { EXECUTION_FILTER_MODEL } from 'core/delivery/filter/executionFilter.model'; -import { EXECUTION_FILTER_SERVICE } from 'core/delivery/filter/executionFilter.service'; - -import './executionFilter.less'; - -module.exports = angular - .module('spinnaker.core.delivery.filter.executionFilters.directive', [ - require('./executionFilter.controller.js').name, - EXECUTION_FILTER_SERVICE, - EXECUTION_FILTER_MODEL, - ]) - .directive('executionFilters', function() { - return { - restrict: 'E', - templateUrl: require('./filterNav.html'), - scope: {}, - bindToController: { - application: '=', - }, - controller: 'ExecutionFilterCtrl', - controllerAs: 'vm', - }; - }); diff --git a/app/scripts/modules/core/src/delivery/filter/executionFilter.less b/app/scripts/modules/core/src/delivery/filter/executionFilters.less similarity index 62% rename from app/scripts/modules/core/src/delivery/filter/executionFilter.less rename to app/scripts/modules/core/src/delivery/filter/executionFilters.less index db5e49a59a3..80a745812bf 100644 --- a/app/scripts/modules/core/src/delivery/filter/executionFilter.less +++ b/app/scripts/modules/core/src/delivery/filter/executionFilters.less @@ -1,10 +1,11 @@ -execution-filters { +execution-filters, .execution-filters { width: 100%; display: flex; .sortable { - .glyphicon-resize-vertical { + .pipeline-drag-handle { display: inline-block; margin-left: -14px; + margin-right: 3px; opacity: 0.7; font-size: 80%; } diff --git a/app/scripts/modules/core/src/delivery/filter/filterNav.html b/app/scripts/modules/core/src/delivery/filter/filterNav.html deleted file mode 100644 index 8b6795ee7f8..00000000000 --- a/app/scripts/modules/core/src/delivery/filter/filterNav.html +++ /dev/null @@ -1,144 +0,0 @@ -
-
- Clear All - - -
- -
-
-
-
- - -
-
-
- -
-
- -
-
- - -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- -
-
diff --git a/app/scripts/modules/core/src/filterModel/IFilterModel.ts b/app/scripts/modules/core/src/filterModel/IFilterModel.ts index 6172257bc37..c241246c693 100644 --- a/app/scripts/modules/core/src/filterModel/IFilterModel.ts +++ b/app/scripts/modules/core/src/filterModel/IFilterModel.ts @@ -3,7 +3,7 @@ import { Ng1StateDeclaration, StateParams } from '@uirouter/angularjs'; export interface IFilterConfig { model: string; param?: string; - clearValue?: string; + clearValue?: any; type?: string; filterLabel?: string; filterTranslator?: {[key: string]: string} diff --git a/package.json b/package.json index ecd84464aa0..2f4f472922b 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "react-dom": "15.6.2", "react-ga": "^2.1.2", "react-select": "^1.0.0-rc.5", + "react-sortable-hoc": "^0.6.8", "react-virtualized-select": "^3.1.0", "react2angular": "^2.0.0", "reflect-metadata": "^0.1.9", @@ -100,6 +101,7 @@ "@types/react-dom": "^15.5.4", "@types/react-ga": "^2.1.1", "@types/react-select": "^1.0.54", + "@types/react-sortable-hoc": "^0.6.0", "@types/webpack": "^2.2.4", "@types/webpack-env": "^1.13.0", "angular-mocks": "1.6.4", diff --git a/yarn.lock b/yarn.lock index cf7bb32627b..e541593ed20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -315,6 +315,12 @@ dependencies: "@types/react" "*" +"@types/react-sortable-hoc@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@types/react-sortable-hoc/-/react-sortable-hoc-0.6.0.tgz#0c568c14dd71a28c9b9e7ec81ecca766499c2c0b" + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^16.0.0", "@types/react@^16.0.5": version "16.0.5" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.5.tgz#d713cf67cc211dea20463d2a0b66005c22070c4b" @@ -4294,7 +4300,7 @@ lodash@^3.8.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.14.0, lodash@^4.16.1, lodash@^4.16.6, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.0.0, lodash@^4.12.0, lodash@^4.14.0, lodash@^4.16.1, lodash@^4.16.6, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -5420,7 +5426,7 @@ promise@^7.0.3, promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@15.6.0: +prop-types@15.6.0, prop-types@^15.5.7: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: @@ -5630,6 +5636,15 @@ react-select@^1.0.0-rc.2, react-select@^1.0.0-rc.5: prop-types "^15.5.8" react-input-autosize "^1.1.3" +react-sortable-hoc@^0.6.8: + version "0.6.8" + resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-0.6.8.tgz#b08562f570d7c41f6e393fca52879d2ebb9118e9" + dependencies: + babel-runtime "^6.11.6" + invariant "^2.2.1" + lodash "^4.12.0" + prop-types "^15.5.7" + react-test-renderer@15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.6.2.tgz#d0333434fc2c438092696ca770da5ed48037efa8"