Make dashcards reorder on drag and drop
NOTE: This commit does not make it so that the data persists after refreshes. closes CNVS-32548 Test Plan: - Enable dashboard card reordering feature flag - Go to a dashboard with multiple dashcards - You should be able to move them around via drag and drop. Change-Id: I66b91b30fd619516558841a6867ef857a33b890a Reviewed-on: https://gerrit.instructure.com/92616 Reviewed-by: Felix Milea-Ciobanu <fmileaciobanu@instructure.com> Tested-by: Jenkins QA-Review: Benjamin Christian Nelson <bcnelson@instructure.com> Product-Review: Clay Diffrient <cdiffrient@instructure.com>
This commit is contained in:
parent
c49e2560a6
commit
56019ec813
|
@ -4,9 +4,15 @@ require [
|
|||
'react'
|
||||
'react-dom'
|
||||
'jsx/dashboard_card/DashboardCardBox',
|
||||
], ($, _, React, ReactDOM, DashboardCardBox) ->
|
||||
element = React.createElement(DashboardCardBox, {
|
||||
courseCards: ENV.DASHBOARD_COURSES
|
||||
'jsx/dashboard_card/getDroppableDashboardCardBox',
|
||||
], ($, _, React, ReactDOM, DashboardCardBox, getDroppableDashboardCardBox) ->
|
||||
|
||||
component = if ENV.DASHBOARD_REORDERING_ENABLED then getDroppableDashboardCardBox() else DashboardCardBox
|
||||
|
||||
element = React.createElement(component, {
|
||||
courseCards: ENV.DASHBOARD_COURSES,
|
||||
reorderingEnabled: ENV.DASHBOARD_REORDERING_ENABLED
|
||||
})
|
||||
|
||||
dashboardContainer = document.getElementById('DashboardCard_Container')
|
||||
ReactDOM.render(element, dashboardContainer)
|
||||
|
|
|
@ -4,8 +4,11 @@ define([
|
|||
'i18n!dashcards',
|
||||
'./DashboardCardAction',
|
||||
'./DashboardColorPicker',
|
||||
'./CourseActivitySummaryStore'
|
||||
], function(_, React, I18n, DashboardCardAction, DashboardColorPicker, CourseActivitySummaryStore) {
|
||||
'./CourseActivitySummaryStore',
|
||||
'react-dnd',
|
||||
'./Types',
|
||||
'jsx/shared/helpers/compose'
|
||||
], function(_, React, I18n, DashboardCardAction, DashboardColorPicker, CourseActivitySummaryStore, ReactDnD, ItemTypes, compose) {
|
||||
|
||||
var DashboardCard = React.createClass({
|
||||
|
||||
|
@ -23,7 +26,11 @@ define([
|
|||
assetString: React.PropTypes.string,
|
||||
term: React.PropTypes.string,
|
||||
href: React.PropTypes.string,
|
||||
links: React.PropTypes.array
|
||||
links: React.PropTypes.array,
|
||||
reorderingEnabled: React.PropTypes.bool,
|
||||
isDragging: React.PropTypes.bool,
|
||||
connectDragSource: React.PropTypes.func,
|
||||
connectDropTarget: React.PropTypes.func
|
||||
},
|
||||
|
||||
getDefaultProps: function () {
|
||||
|
@ -196,11 +203,21 @@ define([
|
|||
},
|
||||
|
||||
render: function () {
|
||||
return (
|
||||
const cardStyles = {
|
||||
borderBottomColor: this.props.backgroundColor
|
||||
};
|
||||
|
||||
if (this.props.reorderingEnabled) {
|
||||
if (this.props.isDragging) {
|
||||
cardStyles.opacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const dashboardCard = (
|
||||
<div
|
||||
className="ic-DashboardCard"
|
||||
ref="cardDiv"
|
||||
style={{borderBottomColor: this.props.backgroundColor}}
|
||||
ref={(c) => this.cardDiv = c}
|
||||
style={cardStyles}
|
||||
aria-label={this.props.originalName}
|
||||
>
|
||||
<div className="ic-DashboardCard__header">
|
||||
|
@ -209,7 +226,7 @@ define([
|
|||
this.props.imagesEnabled && this.props.image ?
|
||||
I18n.t("Course image for %{course}", {course: this.state.nicknameInfo.nickname})
|
||||
:
|
||||
I18n.t("Course card color region for %{course}", {course: this.state.nicknameInfo.nickname})
|
||||
I18n.t("Course card color region for %{course}", {course: this.state.nicknameInfo.nickname})
|
||||
}
|
||||
</span>
|
||||
{this.renderHeaderHero()}
|
||||
|
@ -251,6 +268,13 @@ define([
|
|||
{ this.colorPickerIfEditing() }
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.props.reorderingEnabled) {
|
||||
const { connectDragSource, connectDropTarget } = this.props;
|
||||
return connectDragSource(connectDropTarget(dashboardCard));
|
||||
}
|
||||
|
||||
return dashboardCard;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -2,14 +2,23 @@ define([
|
|||
'jquery',
|
||||
'react',
|
||||
'./DashboardCard',
|
||||
'./DashboardCardBackgroundStore'
|
||||
], function($, React, DashboardCard, DashboardCardBackgroundStore) {
|
||||
var DashboardCardBox = React.createClass({
|
||||
'./DraggableDashboardCard',
|
||||
'./DashboardCardBackgroundStore',
|
||||
], ($, React, DashboardCard, DraggableDashboardCard, DashboardCardBackgroundStore) => {
|
||||
const DashboardCardBox = React.createClass({
|
||||
|
||||
displayName: 'DashboardCardBox',
|
||||
|
||||
propTypes: {
|
||||
courseCards: React.PropTypes.array
|
||||
courseCards: React.PropTypes.array,
|
||||
reorderingEnabled: React.PropTypes.bool,
|
||||
connectDropTarget: React.PropTypes.func
|
||||
},
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({
|
||||
courseCards: this.props.courseCards
|
||||
});
|
||||
},
|
||||
|
||||
componentDidMount: function(){
|
||||
|
@ -17,8 +26,12 @@ define([
|
|||
DashboardCardBackgroundStore.setDefaultColors(this.allCourseAssetStrings());
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(){
|
||||
componentWillReceiveProps: function (newProps) {
|
||||
DashboardCardBackgroundStore.setDefaultColors(this.allCourseAssetStrings());
|
||||
|
||||
this.setState({
|
||||
courseCards: newProps.courseCards
|
||||
});
|
||||
},
|
||||
|
||||
getDefaultProps: function () {
|
||||
|
@ -27,6 +40,16 @@ define([
|
|||
};
|
||||
},
|
||||
|
||||
moveCard (assetString, atIndex) {
|
||||
const cardIndex = this.state.courseCards.findIndex(card => card.assetString === assetString);
|
||||
const newCards = this.state.courseCards.slice();
|
||||
newCards.splice(atIndex, 0, newCards.splice(cardIndex, 1)[0]);
|
||||
this.setState({
|
||||
courseCards: newCards
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
colorsUpdated: function(){
|
||||
if(this.isMounted()){
|
||||
this.forceUpdate();
|
||||
|
@ -45,10 +68,16 @@ define([
|
|||
DashboardCardBackgroundStore.setColorForCourse(assetString, newColor);
|
||||
},
|
||||
|
||||
getOriginalIndex (assetString) {
|
||||
return this.props.courseCards.findIndex(c => c.assetString === assetString);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var cards = this.props.courseCards.map((card) => {
|
||||
const Component = (this.props.reorderingEnabled) ? DraggableDashboardCard : DashboardCard;
|
||||
const cards = this.state.courseCards.map((card, index) => {
|
||||
const position = (card.position != null) ? card.position : this.getOriginalIndex.bind(this, card.assetString)
|
||||
return (
|
||||
<DashboardCard
|
||||
<Component
|
||||
key={card.id}
|
||||
shortName={card.shortName}
|
||||
originalName={card.originalName}
|
||||
|
@ -62,14 +91,26 @@ define([
|
|||
handleColorChange={this.handleColorChange.bind(this, card.assetString)}
|
||||
image={card.image}
|
||||
imagesEnabled={card.imagesEnabled}
|
||||
reorderingEnabled={this.props.reorderingEnabled}
|
||||
position={position}
|
||||
currentIndex={index}
|
||||
moveCard={this.moveCard}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
|
||||
const dashboardCardBox = (
|
||||
<div className="ic-DashboardCard__box">
|
||||
{cards}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.props.reorderingEnabled) {
|
||||
const { connectDropTarget } = this.props;
|
||||
return connectDropTarget(dashboardCardBox);
|
||||
}
|
||||
|
||||
return dashboardCardBox;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
define([
|
||||
'react-dnd',
|
||||
'jsx/shared/helpers/compose',
|
||||
'./Types',
|
||||
'./DashboardCard'
|
||||
], ({ DropTarget, DragSource }, compose, ItemTypes, DashboardCard) => {
|
||||
const cardSource = {
|
||||
beginDrag(props) {
|
||||
return {
|
||||
assetString: props.assetString,
|
||||
originalIndex: props.currentIndex
|
||||
};
|
||||
},
|
||||
isDragging(props, monitor) {
|
||||
return monitor.getItem().assetString === props.assetString;
|
||||
},
|
||||
endDrag(props, monitor) {
|
||||
const { assetString: draggedAssetString } = monitor.getItem();
|
||||
if (!monitor.didDrop()) {
|
||||
props.moveCard(draggedAssetString, props.position);
|
||||
}
|
||||
// TODO: Call something to actually move things to the right positions on the server
|
||||
}
|
||||
};
|
||||
|
||||
const cardTarget = {
|
||||
canDrop() {
|
||||
return false;
|
||||
},
|
||||
hover(props, monitor) {
|
||||
const { assetString: draggedAssetString } = monitor.getItem();
|
||||
const { assetString: overAssetString } = props;
|
||||
if (draggedAssetString !== overAssetString) {
|
||||
const { currentIndex: overIndex } = props;
|
||||
props.moveCard(draggedAssetString, overIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-disable new-cap */
|
||||
return compose(
|
||||
DropTarget(ItemTypes.CARD, cardTarget, connect => ({
|
||||
connectDropTarget: connect.dropTarget()
|
||||
})),
|
||||
DragSource(ItemTypes.CARD, cardSource, (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
}))
|
||||
)(DashboardCard);
|
||||
/* eslint-enable new-cap */
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
define([], () => {
|
||||
/**
|
||||
* Used for drag and drop stuff
|
||||
*/
|
||||
const TYPES = {
|
||||
CARD: 'card'
|
||||
};
|
||||
|
||||
return TYPES;
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
define([
|
||||
'react-dnd',
|
||||
'react-dnd-html5-backend',
|
||||
'jsx/shared/helpers/compose',
|
||||
'./Types',
|
||||
'./DashboardCardBox'
|
||||
], ({ DragDropContext, DropTarget }, ReactDnDHTML5Backend, compose, ItemTypes, DashboardCardBox) => {
|
||||
const cardTarget = {
|
||||
drop () {}
|
||||
};
|
||||
|
||||
const getDroppableDashboardCardBox = (backend = ReactDnDHTML5Backend) => (
|
||||
/* eslint-disable new-cap */
|
||||
compose(
|
||||
DragDropContext(backend),
|
||||
DropTarget(ItemTypes.CARD, cardTarget, connect => ({
|
||||
connectDropTarget: connect.dropTarget()
|
||||
}))
|
||||
)(DashboardCardBox)
|
||||
/* eslint-enable new-cap */
|
||||
);
|
||||
|
||||
return getDroppableDashboardCardBox;
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
define([], () => {
|
||||
// Adapted from https://github.com/reactjs/redux/blob/master/src/compose.js
|
||||
|
||||
// This is a simple utility to help with composing functions together
|
||||
|
||||
/**
|
||||
* Composes single-argument functions from right to left. The rightmost
|
||||
* function can take multiple arguments as it provides the signature for
|
||||
* the resulting composite function.
|
||||
*
|
||||
* @param {...Function} funcs The functions to compose.
|
||||
* @returns {Function} A function obtained by composing the argument functions
|
||||
* from right to left. For example, compose(f, g, h) is identical to doing
|
||||
* (...args) => f(g(h(...args))).
|
||||
*/
|
||||
|
||||
function compose(...funcs) {
|
||||
if (funcs.length === 0) {
|
||||
return arg => arg
|
||||
}
|
||||
|
||||
if (funcs.length === 1) {
|
||||
return funcs[0]
|
||||
}
|
||||
|
||||
const last = funcs[funcs.length - 1]
|
||||
const rest = funcs.slice(0, -1)
|
||||
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
|
||||
}
|
||||
|
||||
return compose;
|
||||
});
|
|
@ -1,9 +1,13 @@
|
|||
<% js_bundle :dashboard_card %>
|
||||
<% css_bundle :dashboard_card %>
|
||||
|
||||
<%
|
||||
<%
|
||||
dashboard_courses = map_courses_for_menu(@current_user.menu_courses, :include_section_tabs => true)
|
||||
js_env :DASHBOARD_COURSES => dashboard_courses %>
|
||||
js_env({
|
||||
:DASHBOARD_COURSES => dashboard_courses,
|
||||
:DASHBOARD_REORDERING_ENABLED => @domain_root_account.feature_enabled?(:dashcard_reordering)
|
||||
})
|
||||
%>
|
||||
|
||||
<div id="DashboardCard_Container" style="display: <%= show_recent_activity? ? 'none' : 'block' %>">
|
||||
<div class="ic-DashboardCard__box">
|
||||
|
|
|
@ -79,6 +79,8 @@ module Canvas
|
|||
{name: 'qs', location: 'symlink_to_node_modules/qs', main: 'dist/qs'},
|
||||
{name: 'react', location: 'symlink_to_node_modules/react', main: 'dist/react-with-addons'},
|
||||
{name: 'react-dom', location: 'symlink_to_node_modules/react-dom', main: 'dist/react-dom'},
|
||||
{name: 'react-dnd', location: 'symlink_to_node_modules/react-dnd', main: 'dist/ReactDnD.min'},
|
||||
{name: 'react-dnd-html5-backend', location: 'symlink_to_node_modules/react-dnd-html5-backend', main: 'dist/ReactDnDHTML5Backend.min'},
|
||||
{name: 'react-redux', location: 'symlink_to_node_modules/react-redux', main: 'dist/react-redux.min'},
|
||||
{name: 'react-select-box', location: 'symlink_to_node_modules/react-select-box', main: 'dist/react-select-box'},
|
||||
{name: 'react-tokeninput', location: 'symlink_to_node_modules/react-tokeninput', main: 'dist/react-tokeninput'},
|
||||
|
|
|
@ -88,6 +88,8 @@
|
|||
"qs": "https://github.com/hapijs/qs.git#a341cdf2fadba5ede1ce6c95c7051f6f31f37b81",
|
||||
"react": "0.14.8",
|
||||
"react-dom": "0.14.8",
|
||||
"react-dnd": "2.1.4",
|
||||
"react-dnd-html5-backend": "2.1.2",
|
||||
"react-addons-css-transition-group": "0.14.8",
|
||||
"react-addons-pure-render-mixin": "0.14.8",
|
||||
"react-addons-test-utils": "0.14.8",
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,95 @@
|
|||
define([
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-addons-test-utils',
|
||||
'react-dnd',
|
||||
'react-dnd-test-backend',
|
||||
'jsx/dashboard_card/DraggableDashboardCard',
|
||||
'jsx/dashboard_card/getDroppableDashboardCardBox',
|
||||
'jsx/dashboard_card/DashboardCardBox',
|
||||
'helpers/fakeENV'
|
||||
], (React, ReactDOM, TestUtils, { DragDropContext }, ReactDndTestBackend, DraggableDashboardCard, getDroppableDashboardCardBox, DashboardCardBox, fakeENV) => {
|
||||
let cards;
|
||||
let fakeServer;
|
||||
|
||||
module('DashboardCard Reordering', {
|
||||
setup () {
|
||||
fakeENV.setup({
|
||||
DASHBOARD_REORDERING_ENABLED: true
|
||||
});
|
||||
|
||||
cards = [{
|
||||
id: 1,
|
||||
assetString: 'course_1',
|
||||
position: 0,
|
||||
originalName: 'Intro to Dashcards 1',
|
||||
shortName: 'Dash 101'
|
||||
}, {
|
||||
id: 2,
|
||||
assetString: 'course_2',
|
||||
position: 1,
|
||||
originalName: 'Intermediate Dashcarding',
|
||||
shortName: 'Dash 201'
|
||||
}, {
|
||||
id: 3,
|
||||
assetString: 'course_3',
|
||||
originalName: 'Advanced Dashcards',
|
||||
shortName: 'Dash 301'
|
||||
}];
|
||||
|
||||
fakeServer = sinon.fakeServer.create();
|
||||
},
|
||||
teardown () {
|
||||
fakeENV.teardown();
|
||||
cards = null;
|
||||
fakeServer.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('it renders', () => {
|
||||
const Box = getDroppableDashboardCardBox()
|
||||
const root = TestUtils.renderIntoDocument(
|
||||
<Box reorderingEnabled courseCards={cards} />
|
||||
);
|
||||
ok(root);
|
||||
});
|
||||
|
||||
test('cards have opacity of 0 while moving', () => {
|
||||
const Card = DraggableDashboardCard.DecoratedComponent.DecoratedComponent;
|
||||
const card = TestUtils.renderIntoDocument(
|
||||
<Card
|
||||
{...cards[0]}
|
||||
connectDragSource={el => el}
|
||||
connectDropTarget={el => el}
|
||||
isDragging
|
||||
reorderingEnabled
|
||||
/>
|
||||
);
|
||||
const div = TestUtils.findRenderedDOMComponentWithClass(card, 'ic-DashboardCard')
|
||||
equal(div.style.opacity, 0);
|
||||
});
|
||||
|
||||
test('moving a card adjusts the position property', () => {
|
||||
const Box = getDroppableDashboardCardBox(ReactDndTestBackend);
|
||||
const root = TestUtils.renderIntoDocument(
|
||||
<Box
|
||||
reorderingEnabled
|
||||
courseCards={cards}
|
||||
connectDropTarget={el => el}
|
||||
/>
|
||||
);
|
||||
|
||||
const backend = root.getManager().getBackend();
|
||||
const renderedCardComponents = TestUtils.scryRenderedComponentsWithType(root, DraggableDashboardCard);
|
||||
const sourceHandlerId = renderedCardComponents[0].getDecoratedComponentInstance().getHandlerId();
|
||||
const targetHandlerId = renderedCardComponents[1].getHandlerId()
|
||||
|
||||
backend.simulateBeginDrag([sourceHandlerId]);
|
||||
backend.simulateHover([targetHandlerId]);
|
||||
backend.simulateDrop();
|
||||
|
||||
const renderedAfterDragNDrop = TestUtils.scryRenderedDOMComponentsWithClass(root, 'ic-DashboardCard')
|
||||
equal(renderedAfterDragNDrop[0].getAttribute('aria-label'), 'Intermediate Dashcarding')
|
||||
equal(renderedAfterDragNDrop[1].getAttribute('aria-label'), 'Intro to Dashcards 1')
|
||||
})
|
||||
});
|
|
@ -30,4 +30,4 @@ var require = {
|
|||
location: '../../spec'
|
||||
},
|
||||
].concat(<%= packages %>)
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue