Add toggling between planner screen and card screen

closes FALCOR-173

Test Plan:
  - Enable student planner feature flag
  - Go to the dashboard
  - The dashboard options dropdown should have Planner as an option
  - Changing to planner should show a "Planner placeholder"
    message
  - Refreshing the page should persist the setting

Change-Id: I9062961327e03e750883246d94acd5da3283fcce
Reviewed-on: https://gerrit.instructure.com/105430
Tested-by: Jenkins
Reviewed-by: Stephen Jensen <sejensen@instructure.com>
QA-Review: Dan Sasaki
Product-Review: Colleen Palmer <colleen@instructure.com>
This commit is contained in:
Clay Diffrient 2017-03-16 15:27:21 -06:00 committed by Stephen Jensen
parent 62db47a269
commit 9bec9774c4
14 changed files with 251 additions and 25 deletions

View File

@ -484,8 +484,10 @@ class UsersController < ApplicationController
:DASHBOARD_SIDEBAR_URL => dashboard_sidebar_url,
:PREFERENCES => {
:recent_activity_dashboard => @current_user.preferences[:recent_activity_dashboard],
:custom_colors => @current_user.custom_colors
}
:custom_colors => @current_user.custom_colors,
:show_planner => show_planner?
},
:STUDENT_PLANNER_ENABLED => planner_enabled?
})
@announcements = AccountNotification.for_user_and_account(@current_user, @domain_root_account)
@ -542,6 +544,24 @@ class UsersController < ApplicationController
render json: {}
end
def dashboard_view
if request.get?
render json: {
dashboard_view: @current_user.preferences[:dashboard_view]
}
elsif request.put?
valid_options = ['activity', 'cards', 'planner']
unless valid_options.include?(params[:dashboard_view])
return render(json: { :message => "Invalid Dashboard View Option" }, status: :bad_request)
end
@current_user.preferences[:dashboard_view] = params[:dashboard_view]
@current_user.save!
render json: {}
end
end
include Api::V1::StreamItem
# @API List the activity stream

View File

@ -953,4 +953,13 @@ module ApplicationHelper
js_env NEW_USER_TUTORIALS: {is_enabled: is_enabled}
end
def planner_enabled?
@domain_root_account&.feature_enabled?(:student_planner)
end
def show_planner?
@current_user.preferences[:dashboard_view] == 'planner'
end
end

View File

@ -21,6 +21,14 @@ module DashboardHelper
@current_user.preferences[:recent_activity_dashboard].present?
end
def show_dashboard_cards?
if planner_enabled?
show_planner?
else
show_recent_activity?
end
end
def show_welcome_message?
@current_user.present? && !@current_user.has_active_enrollment?
end

View File

@ -20,7 +20,11 @@ if (ENV.DASHBOARD_SIDEBAR_URL) {
const dashboardOptionsMenuContainer = document.getElementById('DashboardOptionsMenu_Container')
if (dashboardOptionsMenuContainer) {
ReactDOM.render(
<DashboardOptionsMenu recent_activity_dashboard={ENV.PREFERENCES.recent_activity_dashboard} />,
<DashboardOptionsMenu
recent_activity_dashboard={ENV.PREFERENCES.recent_activity_dashboard}
planner_enabled={ENV.STUDENT_PLANNER_ENABLED}
planner_selected={ENV.PREFERENCES.show_planner}
/>,
dashboardOptionsMenuContainer
)
}

View File

@ -0,0 +1,7 @@
import ReactDOM from 'react-dom';
import React from 'react';
const element = document.getElementById('dashboard-planner');
if (element) {
ReactDOM.render(<div>Planner placeholder</div>, element);
}

View File

@ -9,15 +9,27 @@ import IconSettings2Solid from 'instructure-icons/react/Solid/IconSettings2Solid
export default class DashboardOptionsMenu extends React.Component {
static propTypes = {
recent_activity_dashboard: React.PropTypes.bool.isRequired
recent_activity_dashboard: React.PropTypes.bool.isRequired,
planner_enabled: React.PropTypes.bool,
planner_selected: React.PropTypes.bool
}
static defaultProps = {
planner_enabled: false,
planner_selected: false,
}
constructor (props) {
super(props)
this.state = {
view: props.recent_activity_dashboard ? ['activity'] : ['cards']
let view;
if (props.planner_enabled) {
view = props.planner_selected ? ['planner'] : ['cards']
} else {
view = props.recent_activity_dashboard ? ['activity'] : ['cards']
}
this.state = { view }
}
handleViewOptionSelect = (e, newSelected) => {
@ -31,15 +43,26 @@ export default class DashboardOptionsMenu extends React.Component {
}
toggleDashboardView () {
const dashboardActivity = document.getElementById('dashboard-activity')
const dashboardCards = document.getElementById('DashboardCard_Container')
if (this.props.planner_enabled) {
const dashboardPlanner = document.getElementById('dashboard-planner')
dashboardPlanner.style.display = (dashboardPlanner.style.display === 'none') ? 'block' : 'none'
} else {
const dashboardActivity = document.getElementById('dashboard-activity')
dashboardActivity.style.display = (dashboardActivity.style.display === 'none') ? 'block' : 'none'
}
dashboardActivity.style.display = (dashboardActivity.style.display === 'none') ? 'block' : 'none'
const dashboardCards = document.getElementById('DashboardCard_Container')
dashboardCards.style.display = (dashboardCards.style.display === 'none') ? 'block' : 'none'
}
postDashboardToggle () {
axios.post('/users/toggle_recent_activity_dashboard')
if (this.props.planner_enabled) {
axios.put('/dashboard/view', {
dashboard_view: this.state.view[0]
})
} else {
axios.post('/users/toggle_recent_activity_dashboard')
}
}
render () {
@ -51,15 +74,18 @@ export default class DashboardOptionsMenu extends React.Component {
<IconSettings2Solid />
</Button>
}
contentRef={(el) => { this.menuContentRef = el; }}
>
<MenuItemGroup
label={I18n.t('Dashboard View')}
onSelect={this.handleViewOptionSelect}
selected={this.state.view}
>
<MenuItem value="activity">
{I18n.t('Recent Activity')}
</MenuItem>
{
(this.props.planner_enabled) ?
<MenuItem value="planner">{I18n.t('Planner')}</MenuItem> :
<MenuItem value="activity">{I18n.t('Recent Activity')}</MenuItem>
}
<MenuItem value="cards">
{I18n.t('Course Cards')}
</MenuItem>

View File

@ -9,7 +9,7 @@ js_env({
})
%>
<div id="DashboardCard_Container" style="display: <%= show_recent_activity? ? 'none' : 'block' %>">
<div id="DashboardCard_Container" style="display: <%= show_dashboard_cards? ? 'none' : 'block' %>">
<div class="ic-DashboardCard__box">
<% for i in 0..dashboard_courses.length-1 do %>
<div class="ic-DashboardCard">

View File

@ -26,8 +26,14 @@
</div>
</div>
</div>
<div id="dashboard-activity" class="ic-Dashboard-Activity" style="display: <%= show_recent_activity? ? 'block' : 'none' %>">
<%= render :partial => 'shared/recent_activity' %>
</div>
<% if planner_enabled? %>
<% js_bundle :student_planner %>
<div id="dashboard-planner" class="StudentPlanner__Container" style="display: <%= show_planner? ? 'block' : 'none' %>"></div>
<% else %>
<div id="dashboard-activity" class="ic-Dashboard-Activity" style="display: <%= show_recent_activity? ? 'block' : 'none' %>">
<%= render :partial => 'shared/recent_activity' %>
</div>
<% end %>
<%= render :partial => 'shared/dashboard_card' %>
</div>

View File

@ -802,6 +802,7 @@ CanvasRails::Application.routes.draw do
end
scope '/dashboard' do
put 'view' => 'users#dashboard_view'
delete 'account_notifications/:id' => 'users#close_notification', as: :dashboard_close_notification
get 'eportfolios' => 'eportfolios#user_index', as: :dashboard_eportfolios
post 'comment_session' => 'services_api#start_kaltura_session', as: :dashboard_comment_session

View File

@ -1303,6 +1303,24 @@ describe UsersController do
end
end
describe '#dashboard_view' do
before(:each) do
course_factory
user_factory(active_all: true)
user_session(@user)
end
it 'sets the proper user preference on PUT requests' do
put :dashboard_view, :dashboard_view => 'cards'
expect(@user.preferences[:dashboard_view]).to eql('cards')
end
it 'does not allow arbitrary values to be set' do
put :dashboard_view, :dashboard_view => 'a non-whitelisted value'
assert_status(400)
end
end
describe "#invite_users" do
it 'does not work without ability to manage students or admins on course' do
Account.default.tap{|a| a.settings[:open_registration] = true; a.save!}

View File

@ -780,4 +780,42 @@ describe ApplicationHelper do
end
end
describe "planner_enabled?" do
before(:each) do
@domain_root_account = Account.default
end
context "with student_planner feature flag enabled" do
before(:each) do
@domain_root_account.enable_feature! :student_planner
end
it "return true" do
expect(planner_enabled?).to be true
end
end
context "with student_planner feature flag disabled" do
it "returns false" do
expect(planner_enabled?).to be false
end
end
end
describe "show_planner?" do
before(:each) do
@current_user = User.create!
end
it "returns true when the dashboard view is set to planner" do
@current_user.preferences[:dashboard_view] = 'planner'
expect(show_planner?).to be true
end
it "returns false when the dashboard view is not set to planner" do
@current_user.preferences[:dashboard_view] = 'cards'
expect(show_planner?).to be false
end
end
end

View File

@ -20,7 +20,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe DashboardHelper do
include DashboardHelper
context "show_welcome_message?" do
it "should be true if the user has no current enrollments" do
user_model

View File

@ -3,30 +3,48 @@ import ReactDOM from 'react-dom'
import TestUtils from 'react-addons-test-utils'
import { mount } from 'enzyme'
import DashboardOptionsMenu from 'jsx/dashboard_card/DashboardOptionsMenu'
import moxios from 'moxios';
const container = document.getElementById('fixtures')
const FakeDashboard = function ({menuRef}) {
const FakeDashboard = function (props) {
return (
<div>
<DashboardOptionsMenu
ref={(c) => { menuRef(c) }}
ref={(c) => { props.menuRef(c) }}
recent_activity_dashboard
{...props}
/>
<div
id="dashboard-activity"
style={{ display: 'block' }}
/>
{
(props.planner_enabled) ? (
<div
id="dashboard-planner"
style={{ display: 'block' }}
/>
) :
(
<div
id="dashboard-activity"
style={{ display: 'block' }}
/>
)
}
<div
id="DashboardCard_Container"
style={{ display: 'none' }}
/>
</div>
)
}
FakeDashboard.propTypes = {
menuRef: React.PropTypes.func.isRequired
menuRef: React.PropTypes.func.isRequired,
planner_enabled: React.PropTypes.bool
}
FakeDashboard.defaultProps = {
planner_enabled: false
}
QUnit.module('Dashboard Options Menu', {
@ -73,3 +91,57 @@ test('it should not call toggleDashboardView when correct view is already set',
equal(toggleDashboardView.callCount, 0)
wrapper.unmount()
})
test('it should switch dashboard view appropriately with Student Planner enabled when view option is selected', function () {
this.stub(DashboardOptionsMenu.prototype, 'postDashboardToggle')
let dashboardMenu = null
ReactDOM.render(
<FakeDashboard
menuRef={(c) => { dashboardMenu = c }}
planner_enabled
/>, container)
dashboardMenu.handleViewOptionSelect(null, ['cards'])
ok(document.getElementById('dashboard-planner').style.display === 'none')
ok(document.getElementById('DashboardCard_Container').style.display === 'block')
dashboardMenu.handleViewOptionSelect(null, ['planner'])
ok(document.getElementById('dashboard-planner').style.display === 'block')
ok(document.getElementById('DashboardCard_Container').style.display === 'none')
})
test('it should use the dashboard view endpoint when Student Planner is enabled', function (assert) {
const done = assert.async();
moxios.install();
let dashboardMenu = null
ReactDOM.render(
<FakeDashboard
menuRef={(c) => { dashboardMenu = c }}
planner_enabled
/>, container)
dashboardMenu.handleViewOptionSelect(null, ['cards'])
dashboardMenu.postDashboardToggle();
moxios.wait(() => {
const request = moxios.requests.mostRecent()
equal(request.url, '/dashboard/view')
equal(request.config.data, '{"dashboard_view":"cards"}');
done()
})
moxios.uninstall();
});
test('it should include a planner menu item when Student Planner is enabled', function () {
const wrapper = mount(
<DashboardOptionsMenu
recent_activity_dashboard={false}
planner_enabled
/>
)
wrapper.find('button').simulate('click')
const menuItems = Array.from(document.querySelectorAll('[role="menuitemradio"]'))
ok(menuItems.some(menuItem => menuItem.textContent === 'Planner'))
});

View File

@ -49,4 +49,21 @@ describe "/users/user_dashboard" do
render "users/user_dashboard"
expect(response.body).to match /My Global Announcement/
end
context "with Student Planner enabled" do
it "should render out a div with id of dashboard-planner" do
course_with_student
view_context
@course.account.enable_feature!(:student_planner)
assign(:courses, [@course])
assign(:enrollments, [@enrollment])
assign(:group_memberships, [])
assign(:topics, [])
assign(:upcoming_events, [])
assign(:stream_items, [])
render "users/user_dashboard"
expect(response.body).to match /id="dashboard-planner"/
end
end
end