Show observee data on the student planner
the planner used to rely on ENV.STUDENT_PLANNER_COURSES it now makes a request to get dashboard_cards to acquire the info it needs about the user's list of courses. closes LS-2919 flag=observer_picker test plan: AS A NOT K5 STUDENT > expect the planner to load assignments - click the + and add a note to the user - click the + and add a note to a course > expect both notes to be in the planner, even after you refresh - click the grades button > expect grades to be loaded in the tray - have a missing assignment, and click the opportunities button > expect the missing assignment(s) to show up - dismiss one > expect it to move to the dismissed tab AS AN OBSERVER OF THIS STUDENT - and another student > expect the planner to load assignments > expect the planner to show grades > expect the planner to show opportunities - Don't bother adding notes or dismissing opportunities, those will eventually get disabled. - switch observed students > expect the planner to update with new data - switch to the card dashboar and expect it to be correct - refresh so the cards are the default, then switch to the planner - do ^that, but switch observees before switching dashboard views AS A K5 STUDENT > expect the Homeroom to show the right data - e.g. stuff due today and stuff missing > expect the Schedule tab to show the right data - e.g. assignments and missing items AS AN OBSERVER OF A K5 STUDENT - and another k5 student > expect it to work as it always has - switch observed student > expect the dashboard to update with new data - start from different default dashboards and switch dashboard views or switch observees then try the other way around. AS AN ENROLLED STUDENT OBSERVING A STUDENT > expect the dashboards to default to observing the current user - switch to observing your student > expect the dashboards to reflect the new observee - refresh > expect to be observing the student by default - switch to observing the observer > expect the right stuff to happen TURN OFF THE observer_picker FEATURE > expect all dashboards to still work correctly *** BONUS ***** > expect the initial queries for dashboard_cards, missing_submissions, and planner/items to be the result of the prefetch. On loading the planner page, you should see only 1 copy of the various requests until you start interacting with the page Change-Id: I5a2b1eb2fbc812bbf6ee0bb9c037f21ab99cb007 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/284754 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Jackson Howe <jackson.howe@instructure.com> QA-Review: Jackson Howe <jackson.howe@instructure.com> Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
parent
6a08fb0158
commit
c6df32c76f
|
@ -350,6 +350,9 @@ class PlannerController < ApplicationController
|
|||
# needed for all_ungraded_todo_items, but otherwise we don't need to load the actual
|
||||
# objects
|
||||
@contexts = Context.find_all_by_asset_string(context_ids) if public_access?
|
||||
|
||||
# so we get user notes too if a superobserver
|
||||
@user_ids = [params[:observed_user_id]] if params.key?(:observed_user_id) && @user.grants_right?(@current_user, session, :read_as_parent)
|
||||
end
|
||||
|
||||
# make IDs relative to the user's shard
|
||||
|
|
|
@ -532,7 +532,7 @@ class UsersController < ApplicationController
|
|||
|
||||
# prefetch dashboard cards with the right observer url param
|
||||
if @current_user.roles(@domain_root_account).include?("observer") && (Account.site_admin.feature_enabled?(:k5_parent_support) || Account.site_admin.feature_enabled?(:observer_picker))
|
||||
@cards_prefetch_observer_param = @selected_observed_user&.id
|
||||
@cards_prefetch_observed_param = @selected_observed_user&.id
|
||||
end
|
||||
|
||||
if k5_user?
|
||||
|
|
|
@ -2399,7 +2399,12 @@ class User < ActiveRecord::Base
|
|||
|
||||
def cached_course_ids_for_observed_user(observed_user)
|
||||
Rails.cache.fetch_with_batched_keys(["course_ids_for_observed_user", self, observed_user].cache_key, batch_object: self, batched_keys: :enrollments, expires_in: 1.day) do
|
||||
enrollments.shard(in_region_associated_shards).active_or_pending.of_observer_type.where(associated_user_id: observed_user).pluck(:course_id)
|
||||
enrollments
|
||||
.shard(in_region_associated_shards)
|
||||
.active_by_date
|
||||
.of_observer_type
|
||||
.where(associated_user_id: observed_user)
|
||||
.pluck(:course_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -35,9 +35,9 @@ render_on_pageload = user_dashboard_view == 'cards'
|
|||
%>
|
||||
<% if render_on_pageload %>
|
||||
<%# fire off these `fetch` requests now so they are ready sooner %>
|
||||
<% if Account.site_admin.feature_enabled?(:observer_picker) && @current_user.roles(@domain_root_account).include?("observer") && @cards_prefetch_observer_param.present? %>
|
||||
<%= prefetch_xhr("/api/v1/dashboard/dashboard_cards?observed_user=#{@cards_prefetch_observer_param}") %>
|
||||
<%= prefetch_xhr("/dashboard-sidebar?observed_user=#{@cards_prefetch_observer_param}") %>
|
||||
<% if Account.site_admin.feature_enabled?(:observer_picker) && @current_user.roles(@domain_root_account).include?("observer") && @cards_prefetch_observed_param.present? %>
|
||||
<%= prefetch_xhr("/api/v1/dashboard/dashboard_cards?observed_user=#{@cards_prefetch_observed_param}") %>
|
||||
<%= prefetch_xhr("/dashboard-sidebar?observed_user=#{@cards_prefetch_observed_param}") %>
|
||||
<% else %>
|
||||
<%= prefetch_xhr("/api/v1/dashboard/dashboard_cards") %>
|
||||
<%= prefetch_xhr("/dashboard-sidebar") %>
|
||||
|
|
|
@ -27,31 +27,52 @@
|
|||
<% provide :page_title do %><%= dashboard_title %><% end %>
|
||||
<%
|
||||
is_observer = Account.site_admin.feature_enabled?(:k5_parent_support) && @current_user&.roles(@domain_root_account)&.include?('observer')
|
||||
if show_planner? && !is_observer
|
||||
if show_planner?
|
||||
# fire off these `fetch` requests now so they are ready sooner
|
||||
six_months_ago = Time.now.utc.at_beginning_of_day.months_ago(6).iso8601(3)
|
||||
beginning_of_day = Time.zone.now.at_beginning_of_day.utc.iso8601(3)
|
||||
beginning_of_week = Time.zone.now.beginning_of_week(:sunday).utc.iso8601(3)
|
||||
end_of_week = Time.zone.now.end_of_week(:sunday).utc.iso8601(3)
|
||||
next_year = Time.zone.now.utc.at_beginning_of_day.years_since(1).iso8601
|
||||
last_year = Time.zone.now.utc.at_beginning_of_day.years_ago(1).iso8601
|
||||
if k5_user?
|
||||
six_months_ago = CGI.escape Time.now.utc.at_beginning_of_day.months_ago(6).iso8601(3)
|
||||
beginning_of_day = CGI.escape Time.zone.now.at_beginning_of_day.utc.iso8601(3)
|
||||
beginning_of_week = CGI.escape Time.zone.now.beginning_of_week(:sunday).utc.iso8601(3)
|
||||
end_of_week = CGI.escape Time.zone.now.end_of_week(:sunday).utc.iso8601(3)
|
||||
next_year = CGI.escape Time.zone.now.utc.at_beginning_of_day.years_since(1).iso8601(3)
|
||||
last_year = CGI.escape Time.zone.now.utc.at_beginning_of_day.years_ago(1).iso8601(3)
|
||||
if !is_observer || @cards_prefetch_observed_param == @current_user.id
|
||||
if k5_user?
|
||||
%>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{last_year}&order=asc&per_page=1") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?end_date=#{next_year}&order=desc&per_page=1") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{beginning_of_week}&end_date=#{end_of_week}&per_page=100") %>
|
||||
<%= prefetch_xhr('/api/v1/users/self/missing_submissions?include[]=planner_overrides&filter[]=submittable&filter[]=current_grading_period&per_page=100') %>
|
||||
<% else %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{six_months_ago}&filter=new_activity&order=asc") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?end_date=#{beginning_of_day}&order=desc&per_page=1") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{beginning_of_day}") %>
|
||||
<%= prefetch_xhr('/api/v1/users/self/missing_submissions?include[]=planner_overrides&filter[]=submittable') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= prefetch_xhr("/api/v1/dashboard/dashboard_cards") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{last_year}&order=asc&per_page=1") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?end_date=#{next_year}&order=desc&per_page=1") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{beginning_of_week}&end_date=#{end_of_week}&per_page=100") %>
|
||||
<%= prefetch_xhr('/api/v1/users/self/missing_submissions?include%5B%5D=planner_overrides&filter%5B%5D=current_grading_period&filter%5B%5D=submittable&per_page=100') %>
|
||||
<% else %>
|
||||
<%= prefetch_xhr("/api/v1/dashboard/dashboard_cards") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{six_months_ago}&filter=new_activity&order=asc") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?end_date=#{beginning_of_day}&order=desc&per_page=1") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{beginning_of_day}") %>
|
||||
<%= prefetch_xhr('/api/v1/users/self/missing_submissions?include%5B%5D=planner_overrides&filter%5B%5D=current_grading_period&filter%5B%5D=submittable') %>
|
||||
<% end %>
|
||||
<% else #is_observer %>
|
||||
<% observed_course_ids = @current_user.cached_course_ids_for_observed_user(@selected_observed_user)
|
||||
.sort {|a, b| a <=> b }
|
||||
.map {|cid| "course_ids%5B%5D=#{cid}" }
|
||||
.join('&')
|
||||
observed_context_codes = observed_course_ids.gsub('course_ids%5B%5D=', 'context_codes%5B%5D=course_')
|
||||
|
||||
<% if show_planner? %>
|
||||
<%= prefetch_xhr("/api/v1/dashboard/dashboard_cards#{@cards_prefetch_observer_param.present? ? "?observed_user=#{@cards_prefetch_observer_param}" : ""}") %>
|
||||
<% end %>
|
||||
if k5_user?
|
||||
%>
|
||||
<%= prefetch_xhr("/api/v1/dashboard/dashboard_cards?observed_user=#{@cards_prefetch_observed_param}") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{last_year}&order=asc&per_page=1&observed_user_id=#{@cards_prefetch_observed_param}&#{observed_context_codes}") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?end_date=#{next_year}&order=desc&per_page=1&observed_user_id=#{@cards_prefetch_observed_param}&#{observed_context_codes}") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{beginning_of_week}&end_date=#{end_of_week}&per_page=100&observed_user_id=#{@cards_prefetch_observed_param}&#{observed_context_codes}") %>
|
||||
<%= prefetch_xhr("/api/v1/users/self/missing_submissions?include%5B%5D=planner_overrides&filter%5B%5D=current_grading_period&filter%5B%5D=submittable&per_page=100&observed_user_id=#{@cards_prefetch_observed_param}&#{observed_course_ids}") %>
|
||||
<% else %>
|
||||
<%= prefetch_xhr("/api/v1/dashboard/dashboard_cards?observed_user=#{@cards_prefetch_observed_param}") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{six_months_ago}&filter=new_activity&order=asc&observed_user_id=#{@cards_prefetch_observed_param}&#{observed_context_codes}") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?end_date=#{beginning_of_day}&order=desc&per_page=1&observed_user_id=#{@cards_prefetch_observed_param}&#{observed_context_codes}") %>
|
||||
<%= prefetch_xhr("/api/v1/planner/items?start_date=#{beginning_of_day}&observed_user_id=#{@cards_prefetch_observed_param}&#{observed_context_codes}") %>
|
||||
<%= prefetch_xhr("/api/v1/users/self/missing_submissions?include%5B%5D=planner_overrides&filter%5B%5D=current_grading_period&filter%5B%5D=submittable&observed_user_id=#{@cards_prefetch_observed_param}&#{observed_course_ids}") %>
|
||||
<% end %>
|
||||
<% end%>
|
||||
<% end %>
|
||||
|
||||
<% provide :right_side do %>
|
||||
<% if !show_planner? and !k5_user? %>
|
||||
|
|
|
@ -19,11 +19,13 @@ import moxios from 'moxios'
|
|||
import {findByTestId, render} from '@testing-library/react'
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
import {
|
||||
store,
|
||||
initializePlanner,
|
||||
loadPlannerDashboard,
|
||||
resetPlanner,
|
||||
renderToDoSidebar,
|
||||
renderWeeklyPlannerHeader
|
||||
renderWeeklyPlannerHeader,
|
||||
reloadPlannerForObserver
|
||||
} from '../index'
|
||||
import {initialize as alertInitialize} from '../utilities/alertUtils'
|
||||
|
||||
|
@ -50,6 +52,11 @@ function defaultPlannerOptions() {
|
|||
}
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
courses: [],
|
||||
currentUser: {id: 13}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
resetPlanner()
|
||||
})
|
||||
|
@ -153,4 +160,64 @@ describe('with mock api', () => {
|
|||
expect(wph).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('reloadPlannerForObserver', () => {
|
||||
beforeEach(() => {
|
||||
window.ENV ||= {}
|
||||
ENV.FEATURES = {observer_picker: false}
|
||||
store.dispatch = jest.fn()
|
||||
store.getState = () => defaultState
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('throws an exception unless the planner is initialized', () => {
|
||||
expect(() => reloadPlannerForObserver('1')).toThrow()
|
||||
})
|
||||
|
||||
it('does nothing if not passed an observee id', () => {
|
||||
return initializePlanner(defaultPlannerOptions()).then(() => {
|
||||
store.dispatch.mockClear()
|
||||
reloadPlannerForObserver(null)
|
||||
expect(store.dispatch).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('does nothing if given the existing selectedObservee id', () => {
|
||||
store.getState = () => ({
|
||||
...defaultState,
|
||||
selectedObservee: '17'
|
||||
})
|
||||
|
||||
return initializePlanner(defaultPlannerOptions()).then(() => {
|
||||
store.dispatch.mockClear()
|
||||
reloadPlannerForObserver('17')
|
||||
expect(store.dispatch).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('does nothing if not the weekly planner', () => {
|
||||
return initializePlanner(defaultPlannerOptions()).then(() => {
|
||||
store.dispatch.mockClear()
|
||||
reloadPlannerForObserver('17')
|
||||
expect(store.dispatch).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('dispatches reloadWithObservee when all conditions are met', () => {
|
||||
store.getState = () => ({
|
||||
...defaultState,
|
||||
selectedObservee: '1',
|
||||
weeklyDashboard: {}
|
||||
})
|
||||
|
||||
return initializePlanner(defaultPlannerOptions()).then(() => {
|
||||
store.dispatch.mockClear()
|
||||
reloadPlannerForObserver('17')
|
||||
expect(store.dispatch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -23,6 +23,7 @@ import * as Actions from '../index'
|
|||
import {initialize as alertInitialize} from '../../utilities/alertUtils'
|
||||
|
||||
jest.mock('../../utilities/apiUtils', () => ({
|
||||
...jest.requireActual('../../utilities/apiUtils'),
|
||||
transformApiToInternalItem: jest.fn(response => ({...response, transformedToInternal: true})),
|
||||
transformInternalToApiItem: jest.fn(internal => ({...internal, transformedToApi: true})),
|
||||
transformInternalToApiOverride: jest.fn(internal => ({
|
||||
|
@ -598,54 +599,60 @@ describe('api actions', () => {
|
|||
describe('clearItems', () => {
|
||||
it('dispatches clearWeeklyItems and clearOpportunities actions', () => {
|
||||
const mockDispatch = jest.fn()
|
||||
Actions.clearItems()(mockDispatch, getBasicState)
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(3)
|
||||
Actions.clearItems()(mockDispatch, () => ({
|
||||
weeklyDashboard: {}
|
||||
}))
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(4)
|
||||
expect(mockDispatch).toHaveBeenCalledWith({type: 'CLEAR_WEEKLY_ITEMS'})
|
||||
expect(mockDispatch).toHaveBeenCalledWith({type: 'CLEAR_OPPORTUNITIES'})
|
||||
expect(mockDispatch).toHaveBeenCalledWith({type: 'CLEAR_DAYS'})
|
||||
expect(mockDispatch).toHaveBeenCalledWith({type: 'CLEAR_COURSES'})
|
||||
})
|
||||
|
||||
it('does not dispatch clearWeeklyItems if not a weekly dashboard', () => {
|
||||
const mockDispatch = jest.fn()
|
||||
Actions.clearItems()(mockDispatch, () => ({}))
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(3)
|
||||
expect(mockDispatch).toHaveBeenCalledWith({type: 'CLEAR_OPPORTUNITIES'})
|
||||
expect(mockDispatch).toHaveBeenCalledWith({type: 'CLEAR_DAYS'})
|
||||
expect(mockDispatch).toHaveBeenCalledWith({type: 'CLEAR_COURSES'})
|
||||
})
|
||||
})
|
||||
|
||||
describe('reloadWithObservee', () => {
|
||||
it('dispatches actions to reload planner if observeeId/contextCodes are present and have changed', () => {
|
||||
const mockDispatch = jest.fn()
|
||||
const getState = () => ({
|
||||
...getBasicState(),
|
||||
selectedObservee: {id: '5', contextCodes: undefined}
|
||||
})
|
||||
Actions.reloadWithObservee('5', ['course_1'])(mockDispatch, getState)
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(5)
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: {id: '5', contextCodes: ['course_1']},
|
||||
type: 'SELECTED_OBSERVEE'
|
||||
})
|
||||
let mockDispatch, store, getState
|
||||
beforeEach(() => {
|
||||
mockDispatch = jest.fn(() => Promise.resolve({data: []}))
|
||||
store = {...getBasicState()}
|
||||
getState = () => store
|
||||
})
|
||||
|
||||
it('dispatches startLoadingItems if contextCodes are not present but observee id changed', () => {
|
||||
const mockDispatch = jest.fn()
|
||||
const getState = () => ({
|
||||
...getBasicState(),
|
||||
selectedObservee: {id: '5', contextCodes: undefined}
|
||||
})
|
||||
Actions.reloadWithObservee('6', undefined)(mockDispatch, getState)
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(3)
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: {id: '6', contextCodes: undefined},
|
||||
type: 'SELECTED_OBSERVEE'
|
||||
})
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
type: 'START_LOADING_ITEMS'
|
||||
})
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('does not dispatch anything if contextCodes and id have not changed', () => {
|
||||
const mockDispatch = jest.fn()
|
||||
const getState = () => ({
|
||||
...getBasicState(),
|
||||
selectedObservee: {id: '5', contextCodes: ['course_5']}
|
||||
it('does nothing if no observee id', () => {
|
||||
Actions.reloadWithObservee(undefined)(mockDispatch, getState)
|
||||
expect(mockDispatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing if the observee id did not change', () => {
|
||||
store.selectedObservee = '5'
|
||||
|
||||
Actions.reloadWithObservee('5')(mockDispatch, getState)
|
||||
expect(mockDispatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('dispatches startLoadingItems if contextCodes are not present but observee id changed', async () => {
|
||||
store.selectedObservee = '5'
|
||||
|
||||
await Actions.reloadWithObservee('6')(mockDispatch, getState)
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(4)
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: '6',
|
||||
type: 'SELECTED_OBSERVEE'
|
||||
})
|
||||
Actions.reloadWithObservee('5', ['course_5'])(mockDispatch, getState)
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(0)
|
||||
expect(mockDispatch).toHaveBeenCalledWith({type: 'START_LOADING_ALL_OPPORTUNITIES'})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -24,6 +24,7 @@ import {initialize as alertInitialize} from '../../utilities/alertUtils'
|
|||
import configureStore from '../../store/configureStore'
|
||||
|
||||
jest.mock('../../utilities/apiUtils', () => ({
|
||||
...jest.requireActual('../../utilities/apiUtils'),
|
||||
getContextCodesFromState: jest.requireActual('../../utilities/apiUtils').getContextCodesFromState,
|
||||
findNextLink: jest.fn(),
|
||||
transformApiToInternalItem: jest.fn(response => ({
|
||||
|
@ -82,7 +83,7 @@ describe('api actions', () => {
|
|||
})
|
||||
return moxiosWait(request => {
|
||||
expect(request.config.url).toBe(
|
||||
`/api/v1/planner/items?start_date=${fromMoment.toISOString()}`
|
||||
`/api/v1/planner/items?start_date=${encodeURIComponent(fromMoment.toISOString())}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -91,10 +92,10 @@ describe('api actions', () => {
|
|||
const fromMoment = moment.tz('Asia/Tokyo')
|
||||
Actions.sendFetchRequest({
|
||||
fromMoment,
|
||||
getState: () => ({loading: {futureNextUrl: 'next url'}})
|
||||
getState: () => ({loading: {futureNextUrl: '/next/url'}})
|
||||
})
|
||||
return moxiosWait(request => {
|
||||
expect(request.config.url).toBe('next url')
|
||||
expect(request.config.url).toBe('/next/url')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -107,7 +108,9 @@ describe('api actions', () => {
|
|||
})
|
||||
return moxiosWait(request => {
|
||||
expect(request.config.url).toBe(
|
||||
`/api/v1/planner/items?end_date=${fromMoment.toISOString()}&order=desc`
|
||||
`/api/v1/planner/items?end_date=${encodeURIComponent(
|
||||
fromMoment.toISOString()
|
||||
)}&order=desc`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -117,10 +120,10 @@ describe('api actions', () => {
|
|||
Actions.sendFetchRequest({
|
||||
fromMoment,
|
||||
mode: 'past',
|
||||
getState: () => ({loading: {pastNextUrl: 'past next url'}})
|
||||
getState: () => ({loading: {pastNextUrl: '/past/next/url'}})
|
||||
})
|
||||
return moxiosWait(request => {
|
||||
expect(request.config.url).toBe('past next url')
|
||||
expect(request.config.url).toBe('/past/next/url')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -137,16 +140,19 @@ describe('api actions', () => {
|
|||
})
|
||||
|
||||
describe('getPlannerItems', () => {
|
||||
it('dispatches START_LOADING_ITEMS, getFirstNewActivityDate, and starts the saga', () => {
|
||||
const mockDispatch = jest.fn()
|
||||
Actions.getPlannerItems(moment('2017-12-18'))(mockDispatch, getBasicState)
|
||||
it('dispatches START_LOADING_ITEMS, getFirstNewActivityDate, and starts the saga', async () => {
|
||||
const mockDispatch = jest.fn(() => Promise.resolve({data: []}))
|
||||
const mockMoment = moment()
|
||||
moxios.stubRequest(/.*/, {status: 200, response: [{dateBucketMoment: mockMoment}]})
|
||||
|
||||
await Actions.getPlannerItems(moment('2017-12-18'))(mockDispatch, getBasicState)
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.startLoadingItems())
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.continueLoadingInitialItems())
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.peekIntoPastSaga())
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.startLoadingFutureSaga())
|
||||
const getFirstNewActivityDateThunk = mockDispatch.mock.calls[2][0]
|
||||
const getFirstNewActivityDateThunk = mockDispatch.mock.calls[4][0]
|
||||
expect(typeof getFirstNewActivityDateThunk).toBe('function')
|
||||
const mockMoment = moment()
|
||||
|
||||
const newActivityPromise = getFirstNewActivityDateThunk(mockDispatch, getBasicState)
|
||||
return moxiosRespond([{dateBucketMoment: mockMoment}], newActivityPromise).then(() => {
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
|
@ -159,17 +165,18 @@ describe('api actions', () => {
|
|||
})
|
||||
|
||||
describe('getFirstNewActivityDate', () => {
|
||||
it('sends deep past, filter, and order parameters', () => {
|
||||
const mockDispatch = jest.fn()
|
||||
it('sends deep past, filter, and order parameters', async () => {
|
||||
const mockDispatch = jest.fn(() => Promise.resolve({data: []}))
|
||||
const mockMoment = moment.tz('Asia/Tokyo').startOf('day')
|
||||
Actions.getFirstNewActivityDate(mockMoment)(mockDispatch, getBasicState)
|
||||
return moxiosWait(request => {
|
||||
expect(request.url).toBe(
|
||||
`/api/v1/planner/items?start_date=${mockMoment
|
||||
.subtract(6, 'months')
|
||||
.toISOString()}&filter=new_activity&order=asc`
|
||||
)
|
||||
})
|
||||
moxios.stubRequest(/\/planner\/items/, {response: {data: []}})
|
||||
const p = Actions.getFirstNewActivityDate(mockMoment)(mockDispatch, getBasicState)
|
||||
await p
|
||||
const request = moxios.requests.mostRecent()
|
||||
expect(request.url).toBe(
|
||||
`/api/v1/planner/items?start_date=${encodeURIComponent(
|
||||
mockMoment.subtract(6, 'months').toISOString()
|
||||
)}&filter=new_activity&order=asc`
|
||||
)
|
||||
})
|
||||
|
||||
it('calls the alert method when it fails to get new activity', () => {
|
||||
|
@ -259,7 +266,7 @@ describe('api actions', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
moxios.install()
|
||||
mockDispatch = jest.fn()
|
||||
mockDispatch = jest.fn(() => Promise.resolve({data: []}))
|
||||
weeklyState = getBasicState().weeklyDashboard
|
||||
})
|
||||
|
||||
|
@ -281,9 +288,9 @@ describe('api actions', () => {
|
|||
status: 200,
|
||||
response: [{plannable_date: '2017-01-01T:00:00:00Z'}]
|
||||
})
|
||||
moxios.stubRequest(/dashboard_cards/, {response: {data: []}})
|
||||
|
||||
Actions.getWeeklyPlannerItems(today)(mockDispatch, getBasicState)
|
||||
|
||||
await Actions.getWeeklyPlannerItems(today)(mockDispatch, getBasicState)
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.startLoadingItems())
|
||||
expect(mockDispatch).toHaveBeenCalledWith(Actions.gettingInitWeekItems(weeklyState))
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
|
@ -293,15 +300,16 @@ describe('api actions', () => {
|
|||
isPreload: false
|
||||
})
|
||||
)
|
||||
const getWayFutureItemThunk = mockDispatch.mock.calls[2][0] // the function returned by getWayFutureItem()
|
||||
const getWayFutureItemThunk = mockDispatch.mock.calls[4][0] // the function returned by getWayFutureItem()
|
||||
expect(typeof getWayFutureItemThunk).toBe('function')
|
||||
/* eslint-disable jest/valid-expect-in-promise */
|
||||
const futurePromise = getWayFutureItemThunk(mockDispatch, getBasicState).then(() => {
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
type: 'GOT_WAY_FUTURE_ITEM_DATE',
|
||||
payload: '2017-05-01T:00:00:00Z'
|
||||
})
|
||||
})
|
||||
const getWayPastItemThunk = mockDispatch.mock.calls[3][0]
|
||||
const getWayPastItemThunk = mockDispatch.mock.calls[5][0]
|
||||
expect(typeof getWayPastItemThunk).toBe('function')
|
||||
const pastPromise = getWayPastItemThunk(mockDispatch, getBasicState).then(() => {
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
|
@ -309,6 +317,7 @@ describe('api actions', () => {
|
|||
payload: '2017-01-01T:00:00:00Z'
|
||||
})
|
||||
})
|
||||
/* eslint-enable jest/valid-expect-in-promise */
|
||||
return Promise.all([futurePromise, pastPromise])
|
||||
})
|
||||
})
|
||||
|
@ -488,7 +497,6 @@ describe('api actions', () => {
|
|||
response: [{plannable_date: '2017-05-01T:00:00:00Z'}]
|
||||
})
|
||||
|
||||
const mockCourses = [{id: 7}]
|
||||
const mockUiManager = {
|
||||
setStore: jest.fn(),
|
||||
handleAction: jest.fn(),
|
||||
|
@ -497,20 +505,18 @@ describe('api actions', () => {
|
|||
|
||||
const store = configureStore(mockUiManager, {
|
||||
...getBasicState(),
|
||||
courses: mockCourses,
|
||||
courses: [{id: '7', assetString: 'course_7'}],
|
||||
singleCourse: true
|
||||
})
|
||||
store.dispatch(Actions.getWeeklyPlannerItems(today))
|
||||
await store.dispatch(Actions.getWeeklyPlannerItems(today))
|
||||
|
||||
const expectedContextCodes = /context_codes\[]=course_7/
|
||||
moxios.wait(() => {
|
||||
// Fetching current week, far future date, and far past date should all be filtered by context_codes
|
||||
expect(moxios.requests.count()).toBe(3)
|
||||
expect(moxios.requests.at(0).url).toMatch(expectedContextCodes)
|
||||
expect(moxios.requests.at(1).url).toMatch(expectedContextCodes)
|
||||
expect(moxios.requests.at(2).url).toMatch(expectedContextCodes)
|
||||
done()
|
||||
})
|
||||
const expectedContextCodes = /context_codes%5B%5D=course_7/
|
||||
// Fetching current week, far future date, and far past date should all be filtered by context_codes
|
||||
expect(moxios.requests.count()).toBe(3)
|
||||
expect(moxios.requests.at(0).url).toMatch(expectedContextCodes)
|
||||
expect(moxios.requests.at(1).url).toMatch(expectedContextCodes)
|
||||
expect(moxios.requests.at(2).url).toMatch(expectedContextCodes)
|
||||
done()
|
||||
})
|
||||
|
||||
it('adds observee id and context codes to request if state contains selected observee', async done => {
|
||||
|
@ -519,8 +525,13 @@ describe('api actions', () => {
|
|||
status: 200,
|
||||
response: [{plannable_date: '2017-05-01T:00:00:00Z'}]
|
||||
})
|
||||
moxios.stubRequest(/dashboard_cards/, {
|
||||
response: [
|
||||
{id: '11', assetString: 'course_11'},
|
||||
{id: '12', assetString: 'course_12'}
|
||||
]
|
||||
})
|
||||
|
||||
const mockCourses = [{id: 7}]
|
||||
const mockUiManager = {
|
||||
setStore: jest.fn(),
|
||||
handleAction: jest.fn(),
|
||||
|
@ -529,21 +540,20 @@ describe('api actions', () => {
|
|||
|
||||
const store = configureStore(mockUiManager, {
|
||||
...getBasicState(),
|
||||
courses: mockCourses,
|
||||
selectedObservee: {id: '35', contextCodes: ['course_11', 'course_12']}
|
||||
selectedObservee: '35'
|
||||
})
|
||||
store.dispatch(Actions.getWeeklyPlannerItems(today))
|
||||
|
||||
await store.dispatch(Actions.getWeeklyPlannerItems(today))
|
||||
|
||||
const expectedParams =
|
||||
/observed_user_id=35&context_codes\[]=course_11&context_codes\[]=course_12/
|
||||
moxios.wait(() => {
|
||||
// Fetching current week, far future date, and far past date should all have observee id and context codes
|
||||
expect(moxios.requests.count()).toBe(3)
|
||||
expect(moxios.requests.at(0).url).toMatch(expectedParams)
|
||||
expect(moxios.requests.at(1).url).toMatch(expectedParams)
|
||||
expect(moxios.requests.at(2).url).toMatch(expectedParams)
|
||||
done()
|
||||
})
|
||||
/observed_user_id=35&context_codes%5B%5D=course_11&context_codes%5B%5D=course_12/
|
||||
// Fetching current week, far future date, and far past date should all have observee id and context codes
|
||||
expect(moxios.requests.count()).toBe(4)
|
||||
expect(moxios.requests.at(0).url).toMatch(/dashboard_cards/)
|
||||
expect(moxios.requests.at(1).url).toMatch(expectedParams)
|
||||
expect(moxios.requests.at(2).url).toMatch(expectedParams)
|
||||
expect(moxios.requests.at(3).url).toMatch(expectedParams)
|
||||
done()
|
||||
})
|
||||
|
||||
it('does not add observee id if observee id is the current user id', async done => {
|
||||
|
@ -552,8 +562,10 @@ describe('api actions', () => {
|
|||
status: 200,
|
||||
response: [{plannable_date: '2017-05-01T:00:00:00Z'}]
|
||||
})
|
||||
moxios.stubRequest(/dashboard_cards/, {
|
||||
response: [{id: '7', assetString: 'course_7'}]
|
||||
})
|
||||
|
||||
const mockCourses = [{id: 7}]
|
||||
const mockUiManager = {
|
||||
setStore: jest.fn(),
|
||||
handleAction: jest.fn(),
|
||||
|
@ -562,18 +574,18 @@ describe('api actions', () => {
|
|||
|
||||
const store = configureStore(mockUiManager, {
|
||||
...getBasicState(),
|
||||
courses: mockCourses,
|
||||
selectedObservee: {id: '1', contextCodes: ['course_11', 'course_12']}
|
||||
selectedObservee: '1'
|
||||
})
|
||||
store.dispatch(Actions.getWeeklyPlannerItems(today))
|
||||
|
||||
moxios.wait(() => {
|
||||
expect(moxios.requests.count()).toBe(3)
|
||||
expect(moxios.requests.at(0).url).not.toContain('observed_user_id')
|
||||
expect(moxios.requests.at(1).url).not.toContain('observed_user_id')
|
||||
expect(moxios.requests.at(2).url).not.toContain('observed_user_id')
|
||||
done()
|
||||
})
|
||||
await store.dispatch(Actions.getWeeklyPlannerItems(today))
|
||||
|
||||
expect(moxios.requests.count()).toBe(4)
|
||||
expect(moxios.requests.at(0).url).toMatch(/dashboard_cards/)
|
||||
expect(moxios.requests.at(0).url).not.toContain('observed_user')
|
||||
expect(moxios.requests.at(1).url).not.toContain('observed_user_id')
|
||||
expect(moxios.requests.at(2).url).not.toContain('observed_user_id')
|
||||
expect(moxios.requests.at(3).url).not.toContain('observed_user_id')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -278,7 +278,8 @@ describe('loadAllOpportunitiesSaga', () => {
|
|||
course_ids: undefined,
|
||||
include: ['planner_overrides'],
|
||||
filter: ['submittable', 'current_grading_period'],
|
||||
per_page: 100
|
||||
per_page: 100,
|
||||
observed_user_id: null
|
||||
})
|
||||
)
|
||||
})
|
||||
|
@ -341,13 +342,14 @@ describe('loadAllOpportunitiesSaga', () => {
|
|||
|
||||
it('passes observed_user_id and course_ids to API call if observing', () => {
|
||||
const overrides = {
|
||||
courses: [
|
||||
{id: '1', assetString: 'course_1'},
|
||||
{id: '569', assetString: 'course_569'}
|
||||
],
|
||||
currentUser: {
|
||||
id: '3'
|
||||
},
|
||||
selectedObservee: {
|
||||
id: '12',
|
||||
contextCodes: ['course_1', 'course_569']
|
||||
}
|
||||
selectedObservee: '12'
|
||||
}
|
||||
const generator = loadAllOpportunitiesSaga()
|
||||
generator.next() // select state
|
||||
|
|
|
@ -17,20 +17,19 @@
|
|||
*/
|
||||
import {createActions, createAction} from 'redux-actions'
|
||||
import axios from 'axios'
|
||||
import moment from 'moment-timezone'
|
||||
import {asAxios, getPrefetchedXHR} from '@instructure/js-utils'
|
||||
import {deepEqual} from '@instructure/ui-utils'
|
||||
import parseLinkHeader from 'parse-link-header'
|
||||
import configureAxios from '../utilities/configureAxios'
|
||||
import {alert} from '../utilities/alertUtils'
|
||||
import formatMessage from '../format-message'
|
||||
import {maybeUpdateTodoSidebar} from './sidebar-actions'
|
||||
import {getWeeklyPlannerItems, preloadSurroundingWeeks, startLoadingItems} from './loading-actions'
|
||||
|
||||
import {getPlannerItems, getWeeklyPlannerItems} from './loading-actions'
|
||||
import {
|
||||
transformInternalToApiItem,
|
||||
transformInternalToApiOverride,
|
||||
transformPlannerNoteApiToInternalItem
|
||||
transformPlannerNoteApiToInternalItem,
|
||||
getResponseHeader,
|
||||
buildURL
|
||||
} from '../utilities/apiUtils'
|
||||
|
||||
configureAxios(axios)
|
||||
|
@ -57,7 +56,8 @@ export const {
|
|||
selectedObservee,
|
||||
clearWeeklyItems,
|
||||
clearOpportunities,
|
||||
clearDays
|
||||
clearDays,
|
||||
clearCourses
|
||||
} = createActions(
|
||||
'INITIAL_OPTIONS',
|
||||
'ADD_OPPORTUNITIES',
|
||||
|
@ -80,7 +80,8 @@ export const {
|
|||
'SELECTED_OBSERVEE',
|
||||
'CLEAR_WEEKLY_ITEMS',
|
||||
'CLEAR_OPPORTUNITIES',
|
||||
'CLEAR_DAYS'
|
||||
'CLEAR_DAYS',
|
||||
'CLEAR_COURSES'
|
||||
)
|
||||
|
||||
export * from './loading-actions'
|
||||
|
@ -111,18 +112,20 @@ export const getNextOpportunities = () => {
|
|||
url: getState().opportunities.nextUrl
|
||||
})
|
||||
.then(response => {
|
||||
if (parseLinkHeader(response.headers.link).next) {
|
||||
if (parseLinkHeader(getResponseHeader(response, 'link')).next) {
|
||||
dispatch(
|
||||
addOpportunities({
|
||||
items: response.data,
|
||||
nextUrl: parseLinkHeader(response.headers.link).next.url
|
||||
nextUrl: parseLinkHeader(getResponseHeader(response, 'link')).next.url
|
||||
})
|
||||
)
|
||||
} else {
|
||||
dispatch(addOpportunities({items: response.data, nextUrl: null}))
|
||||
}
|
||||
})
|
||||
.catch(() => alert(formatMessage('Failed to load opportunities'), true))
|
||||
.catch(_ex => {
|
||||
alert(formatMessage('Failed to load opportunities'), true)
|
||||
})
|
||||
} else {
|
||||
dispatch(allOpportunitiesLoaded())
|
||||
}
|
||||
|
@ -133,17 +136,28 @@ export const getInitialOpportunities = () => {
|
|||
return (dispatch, getState) => {
|
||||
dispatch(startLoadingOpportunities())
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const {courses, selectedObservee} = getState()
|
||||
const url =
|
||||
getState().opportunities.nextUrl ||
|
||||
'/api/v1/users/self/missing_submissions?include[]=planner_overrides&filter[]=submittable'
|
||||
buildURL('/api/v1/users/self/missing_submissions', {
|
||||
include: ['planner_overrides'],
|
||||
filter: ['submittable', 'current_grading_period'],
|
||||
observed_user_id: selectedObservee,
|
||||
course_ids: selectedObservee
|
||||
? courses.map(c => c.id).sort((a, b) => a.localeCompare(b, 'en', {numeric: true}))
|
||||
: undefined
|
||||
})
|
||||
const request = asAxios(getPrefetchedXHR(url)) || axios({method: 'get', url})
|
||||
|
||||
request
|
||||
.then(response => {
|
||||
const next = parseLinkHeader(response.headers.link).next
|
||||
const next = parseLinkHeader(getResponseHeader(response, 'link')).next
|
||||
dispatch(addOpportunities({items: response.data, nextUrl: next ? next.url : null}))
|
||||
})
|
||||
.catch(() => alert(formatMessage('Failed to load opportunities'), true))
|
||||
.catch(_ex => {
|
||||
alert(formatMessage('Failed to load opportunities'), true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,27 +300,29 @@ function getOverrideDataOnItem(plannerItem) {
|
|||
}
|
||||
|
||||
export const clearItems = () => {
|
||||
return dispatch => {
|
||||
dispatch(clearWeeklyItems())
|
||||
return (dispatch, getState) => {
|
||||
if (getState().weeklyDashboard) {
|
||||
dispatch(clearWeeklyItems())
|
||||
}
|
||||
dispatch(clearCourses(getState().singleCourse))
|
||||
dispatch(clearOpportunities())
|
||||
dispatch(clearDays())
|
||||
}
|
||||
}
|
||||
|
||||
export const reloadWithObservee = (observeeId, contextCodes) => {
|
||||
export const reloadWithObservee = observeeId => {
|
||||
return (dispatch, getState) => {
|
||||
if (
|
||||
getState().selectedObservee?.id !== observeeId ||
|
||||
(observeeId && !deepEqual(getState().selectedObservee?.contextCodes, contextCodes))
|
||||
) {
|
||||
dispatch(selectedObservee({id: observeeId, contextCodes}))
|
||||
if (getState().selectedObservee !== observeeId) {
|
||||
dispatch(selectedObservee(observeeId))
|
||||
dispatch(clearItems())
|
||||
if (observeeId && !contextCodes) {
|
||||
dispatch(startLoadingItems())
|
||||
if (getState().weeklyDashboard) {
|
||||
return dispatch(getWeeklyPlannerItems(getState().today)).then(() => {
|
||||
dispatch(startLoadingAllOpportunities())
|
||||
})
|
||||
} else {
|
||||
dispatch(getWeeklyPlannerItems(moment.tz(getState().timeZone).startOf('day')))
|
||||
dispatch(preloadSurroundingWeeks())
|
||||
dispatch(startLoadingAllOpportunities())
|
||||
return dispatch(getPlannerItems(getState().today)).then(() => {
|
||||
dispatch(startLoadingAllOpportunities())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,12 +18,13 @@
|
|||
|
||||
import {createActions, createAction} from 'redux-actions'
|
||||
import axios from 'axios'
|
||||
import buildURL from 'axios/lib/helpers/buildURL'
|
||||
import {asAxios, getPrefetchedXHR} from '@instructure/js-utils'
|
||||
import {
|
||||
getContextCodesFromState,
|
||||
transformApiToInternalItem,
|
||||
observedUserId
|
||||
observedUserId,
|
||||
observedUserContextCodes,
|
||||
buildURL
|
||||
} from '../utilities/apiUtils'
|
||||
import {alert} from '../utilities/alertUtils'
|
||||
import formatMessage from '../format-message'
|
||||
|
@ -54,7 +55,8 @@ export const {
|
|||
jumpToWeek,
|
||||
jumpToThisWeek,
|
||||
gotWayPastItemDate,
|
||||
gotWayFutureItemDate
|
||||
gotWayFutureItemDate,
|
||||
gotCourseList
|
||||
} = createActions(
|
||||
'START_LOADING_ITEMS',
|
||||
'CONTINUE_LOADING_INITIAL_ITEMS',
|
||||
|
@ -80,7 +82,8 @@ export const {
|
|||
'JUMP_TO_WEEK',
|
||||
'JUMP_TO_THIS_WEEK',
|
||||
'GOT_WAY_FUTURE_ITEM_DATE',
|
||||
'GOT_WAY_PAST_ITEM_DATE'
|
||||
'GOT_WAY_PAST_ITEM_DATE',
|
||||
'GOT_COURSE_LIST'
|
||||
)
|
||||
|
||||
export const gettingPastItems = createAction(
|
||||
|
@ -111,8 +114,17 @@ export function getFirstNewActivityDate(fromMoment) {
|
|||
// specifically so we know what the very oldest new activity is
|
||||
return (dispatch, getState) => {
|
||||
fromMoment = fromMoment.clone().subtract(6, 'months')
|
||||
const observed_user_id = observedUserId(getState())
|
||||
const context_codes = observedUserContextCodes(getState())
|
||||
|
||||
const url = buildURL('/api/v1/planner/items', {
|
||||
start_date: fromMoment.toISOString(),
|
||||
filter: 'new_activity',
|
||||
order: 'asc',
|
||||
observed_user_id,
|
||||
context_codes
|
||||
})
|
||||
|
||||
const url = `/api/v1/planner/items?start_date=${fromMoment.toISOString()}&filter=new_activity&order=asc`
|
||||
const request = asAxios(getPrefetchedXHR(url)) || axios.get(url)
|
||||
|
||||
return request
|
||||
|
@ -127,7 +139,7 @@ export function getFirstNewActivityDate(fromMoment) {
|
|||
dispatch(foundFirstNewActivityDate(first.dateBucketMoment))
|
||||
}
|
||||
})
|
||||
.catch(() => alert(formatMessage('Failed to get new activity'), true))
|
||||
.catch(_ex => alert(formatMessage('Failed to get new activity'), true))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,10 +147,30 @@ export function getFirstNewActivityDate(fromMoment) {
|
|||
export function getPlannerItems(fromMoment) {
|
||||
return dispatch => {
|
||||
dispatch(startLoadingItems())
|
||||
dispatch(continueLoadingInitialItems()) // a start counts as a continue for the ContinueInitialLoad animation
|
||||
dispatch(getFirstNewActivityDate(fromMoment))
|
||||
dispatch(peekIntoPastSaga())
|
||||
dispatch(startLoadingFutureSaga())
|
||||
return dispatch(getCourseList()) // classic planner never does singleCourse
|
||||
.then(res => {
|
||||
dispatch(gotCourseList(res.data))
|
||||
dispatch(continueLoadingInitialItems()) // a start counts as a continue for the ContinueInitialLoad animation
|
||||
dispatch(getFirstNewActivityDate(fromMoment))
|
||||
dispatch(peekIntoPastSaga())
|
||||
dispatch(startLoadingFutureSaga())
|
||||
})
|
||||
.catch(_ex => {
|
||||
alert(formatMessage('Failed getting course list'))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function getCourseList() {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().singleCourse) {
|
||||
return Promise.resolve({data: getState().courses}) // set via INITIAL_OPTIONS
|
||||
}
|
||||
const observeeId = observedUserId(getState())
|
||||
const ovserveeParam = observeeId ? `?observed_user=${observeeId}` : ''
|
||||
const url = `/api/v1/dashboard/dashboard_cards${ovserveeParam}`
|
||||
const request = asAxios(getPrefetchedXHR(url)) || axios.get(url)
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,11 +230,21 @@ export const loadPastUntilToday = () => dispatch => {
|
|||
export function getWeeklyPlannerItems(fromMoment) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(startLoadingItems())
|
||||
const weeklyState = getState().weeklyDashboard
|
||||
dispatch(gettingInitWeekItems(weeklyState))
|
||||
dispatch(getWayFutureItem(fromMoment))
|
||||
dispatch(getWayPastItem(fromMoment))
|
||||
loadWeekItems(dispatch, getState)
|
||||
const coursesPromise = getState().singleCourse
|
||||
? Promise.resolve({data: getState().courses}) // set in INITIAL_OPTIONS for the singleCourse case
|
||||
: dispatch(getCourseList())
|
||||
return coursesPromise
|
||||
.then(res => {
|
||||
dispatch(gotCourseList(res.data))
|
||||
const weeklyState = getState().weeklyDashboard
|
||||
dispatch(gettingInitWeekItems(weeklyState))
|
||||
dispatch(getWayFutureItem(fromMoment))
|
||||
dispatch(getWayPastItem(fromMoment))
|
||||
loadWeekItems(dispatch, getState)
|
||||
})
|
||||
.catch(_ex => {
|
||||
alert(formatMessage('Failed getting course list'))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -285,15 +327,15 @@ function getWayFutureItem(fromMoment) {
|
|||
const observed_user_id = observedUserId(state)
|
||||
let context_codes
|
||||
if (observed_user_id) {
|
||||
context_codes = state.selectedObservee.contextCodes
|
||||
context_codes = getContextCodesFromState(state)
|
||||
} else {
|
||||
context_codes = state.singleCourse ? getContextCodesFromState(state) : undefined
|
||||
}
|
||||
const futureMoment = fromMoment.clone().add(1, 'year')
|
||||
const futureMoment = fromMoment.clone().tz('UTC').startOf('day').add(1, 'year')
|
||||
const url = buildURL('/api/v1/planner/items', {
|
||||
observed_user_id,
|
||||
context_codes,
|
||||
end_date: futureMoment.format(),
|
||||
end_date: futureMoment.toISOString(),
|
||||
order: 'desc',
|
||||
per_page: 1
|
||||
})
|
||||
|
@ -306,7 +348,7 @@ function getWayFutureItem(fromMoment) {
|
|||
dispatch(gotWayFutureItemDate(wayFutureItemDate))
|
||||
}
|
||||
})
|
||||
.catch(() => alert(formatMessage('Failed peeking into your future'), true))
|
||||
.catch(_ex => alert(formatMessage('Failed peeking into your future'), true))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,15 +358,15 @@ function getWayPastItem(fromMoment) {
|
|||
const observed_user_id = observedUserId(state)
|
||||
let context_codes
|
||||
if (observed_user_id) {
|
||||
context_codes = state.selectedObservee.contextCodes
|
||||
context_codes = getContextCodesFromState(state)
|
||||
} else {
|
||||
context_codes = state.singleCourse ? getContextCodesFromState(state) : undefined
|
||||
}
|
||||
const pastMoment = fromMoment.clone().add(-1, 'year')
|
||||
const pastMoment = fromMoment.clone().tz('UTC').startOf('day').add(-1, 'year')
|
||||
const url = buildURL('/api/v1/planner/items', {
|
||||
observed_user_id,
|
||||
context_codes,
|
||||
start_date: pastMoment.format(),
|
||||
start_date: pastMoment.toISOString(),
|
||||
order: 'asc',
|
||||
per_page: 1
|
||||
})
|
||||
|
@ -337,7 +379,7 @@ function getWayPastItem(fromMoment) {
|
|||
dispatch(gotWayPastItemDate(wayPastItemDate))
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(_ex => {
|
||||
alert(formatMessage('Failed peeking into your past'), true)
|
||||
})
|
||||
}
|
||||
|
@ -385,7 +427,7 @@ function fetchParams(loadingOptions) {
|
|||
const observeeId = observedUserId(loadingOptions.getState())
|
||||
if (observeeId) {
|
||||
params.observed_user_id = observeeId
|
||||
params.context_codes = loadingOptions.getState().selectedObservee.contextCodes
|
||||
params.context_codes = observedUserContextCodes(loadingOptions.getState())
|
||||
}
|
||||
|
||||
return ['/api/v1/planner/items', {params}]
|
||||
|
|
|
@ -23,7 +23,8 @@ import {getFirstLoadedMoment, getLastLoadedMoment} from '../utilities/dateUtils'
|
|||
import {
|
||||
getContextCodesFromState,
|
||||
transformApiToInternalGrade,
|
||||
observedUserId
|
||||
observedUserId,
|
||||
getResponseHeader
|
||||
} from '../utilities/apiUtils'
|
||||
import {alert} from '../utilities/alertUtils'
|
||||
import formatMessage from '../format-message'
|
||||
|
@ -142,7 +143,7 @@ export function* loadGradesSaga() {
|
|||
gradesData[internalGrade.courseId] = internalGrade
|
||||
})
|
||||
|
||||
const links = parseLinkHeader(response.headers.link)
|
||||
const links = parseLinkHeader(getResponseHeader(response, 'link'))
|
||||
loadingUrl = links && links.next ? links.next.url : null
|
||||
}
|
||||
yield put(gotGradesSuccess(gradesData))
|
||||
|
@ -160,9 +161,9 @@ export function* loadAllOpportunitiesSaga() {
|
|||
const observed_user_id = observedUserId({selectedObservee, currentUser})
|
||||
let course_ids
|
||||
if (observed_user_id) {
|
||||
course_ids = selectedObservee.contextCodes?.map(c => c.split('_')[1])
|
||||
course_ids = courses.map(c => c.id)
|
||||
} else {
|
||||
course_ids = singleCourse ? courses.map(({id}) => id) : undefined
|
||||
course_ids = singleCourse ? courses.map(c => c.id) : undefined
|
||||
}
|
||||
while (loadingUrl != null) {
|
||||
const filter = ['submittable']
|
||||
|
@ -178,7 +179,7 @@ export function* loadAllOpportunitiesSaga() {
|
|||
})
|
||||
items.push(...response.data)
|
||||
|
||||
const links = parseLinkHeader(response.headers.link)
|
||||
const links = parseLinkHeader(getResponseHeader(response, 'link'))
|
||||
loadingUrl = links?.next ? links.next.url : null
|
||||
}
|
||||
yield put(addOpportunities({items, nextUrl: null}))
|
||||
|
|
|
@ -37,14 +37,15 @@ import {
|
|||
loadPastButtonClicked,
|
||||
togglePlannerItemCompletion,
|
||||
updateTodo,
|
||||
scrollToToday
|
||||
scrollToToday,
|
||||
reloadWithObservee
|
||||
} from '../../actions'
|
||||
import {notifier} from '../../dynamic-ui'
|
||||
import {daysToDaysHash} from '../../utilities/daysUtils'
|
||||
import {formatDayKey, isThisWeek} from '../../utilities/dateUtils'
|
||||
import {Animator} from '../../dynamic-ui/animator'
|
||||
import responsiviser from '../responsiviser'
|
||||
import {observedUserId} from '../../utilities/apiUtils'
|
||||
import {observedUserId, observedUserContextCodes} from '../../utilities/apiUtils'
|
||||
|
||||
export class PlannerApp extends Component {
|
||||
static propTypes = {
|
||||
|
@ -84,7 +85,9 @@ export class PlannerApp extends Component {
|
|||
loadingOpportunities: bool,
|
||||
opportunityCount: number,
|
||||
singleCourseView: bool,
|
||||
isObserving: bool
|
||||
isObserving: bool,
|
||||
observedUserId: string,
|
||||
observedUserContextCodes: arrayOf(string)
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -125,6 +128,11 @@ export class PlannerApp extends Component {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.observedUserId !== this.props.observedUserId) {
|
||||
reloadWithObservee(this.props.observedUserId, this.props.observedUserContextCodes)
|
||||
return
|
||||
}
|
||||
|
||||
this.props.triggerDynamicUiUpdates()
|
||||
if (this.props.responsiveSize !== prevProps.responsiveSize) {
|
||||
this.afterLayoutChange()
|
||||
|
@ -523,7 +531,9 @@ export const mapStateToProps = state => {
|
|||
loadingOpportunities: !!state.loading.loadingOpportunities,
|
||||
opportunityCount: state.opportunities?.items?.length || 0,
|
||||
singleCourseView: state.singleCourse,
|
||||
isObserving: !!observedUserId(state)
|
||||
isObserving: !!observedUserId(state),
|
||||
observeduserId: observedUserId(state),
|
||||
observedUserContextCodes: observedUserContextCodes(state)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -249,22 +249,23 @@ function loading() {
|
|||
export function createPlannerApp() {
|
||||
if (!store.getState().weeklyDashboard) {
|
||||
// disable load on scroll for weekly dashboard
|
||||
registerScrollEvents({
|
||||
scrollIntoPast: handleScrollIntoPastAttempt,
|
||||
scrollIntoFuture: handleScrollIntoFutureAttempt,
|
||||
scrollPositionChange: pos => dynamicUiManager.handleScrollPositionChange(pos)
|
||||
})
|
||||
if (!createPlannerApp.scrollEventsRegistered) {
|
||||
// register events only once
|
||||
registerScrollEvents({
|
||||
scrollIntoPast: handleScrollIntoPastAttempt,
|
||||
scrollIntoFuture: handleScrollIntoFutureAttempt,
|
||||
scrollPositionChange: pos => dynamicUiManager.handleScrollPositionChange(pos)
|
||||
})
|
||||
createPlannerApp.scrollEventsRegistered = true
|
||||
}
|
||||
|
||||
store.dispatch(getPlannerItems(moment.tz(initializedOptions.env.timeZone).startOf('day')))
|
||||
} else {
|
||||
const waitingOnObserveeContextCodes =
|
||||
store.getState().selectedObservee?.id && !store.getState().selectedObservee?.contextCodes
|
||||
if (!waitingOnObserveeContextCodes) {
|
||||
store.dispatch(
|
||||
getWeeklyPlannerItems(moment.tz(initializedOptions.env.timeZone).startOf('day'))
|
||||
)
|
||||
store.dispatch(startLoadingAllOpportunities())
|
||||
}
|
||||
store
|
||||
.dispatch(getWeeklyPlannerItems(moment.tz(initializedOptions.env.timeZone).startOf('day')))
|
||||
.then(() => {
|
||||
store.dispatch(startLoadingAllOpportunities())
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -285,6 +286,7 @@ export function createPlannerApp() {
|
|||
</DynamicUiProvider>
|
||||
)
|
||||
}
|
||||
createPlannerApp.scrollEventsRegistered = false
|
||||
|
||||
function renderApp(element) {
|
||||
ReactDOM.render(createPlannerApp(), element)
|
||||
|
@ -325,7 +327,7 @@ export function renderToDoSidebar(element) {
|
|||
<Provider store={store}>
|
||||
<Suspense fallback={loading()}>
|
||||
<ToDoSidebar
|
||||
courses={env.STUDENT_PLANNER_COURSES}
|
||||
courses={store.getState().courses}
|
||||
timeZone={env.TIMEZONE}
|
||||
locale={env.MOMENT_LOCALE}
|
||||
changeDashboardView={initializedOptions.changeDashboardView}
|
||||
|
@ -377,13 +379,20 @@ export function preloadInitialItems() {
|
|||
}
|
||||
}
|
||||
|
||||
// Call with student id and student's context codes to load planner scoped to
|
||||
// Call with student id to load planner scoped to
|
||||
// one of an observer's students
|
||||
export function reloadPlannerForObserver(observeeId, contextCodes) {
|
||||
export function reloadPlannerForObserver(newObserveeId) {
|
||||
if (!initializedOptions)
|
||||
throw new Error('initializePlanner must be called before reloadPlannerForObserver')
|
||||
if (!store.getState().weeklyDashboard)
|
||||
throw new Error('reloadPlannerForObserver is only supported in weekly dashboard mode')
|
||||
|
||||
store.dispatch(reloadWithObservee(observeeId, contextCodes))
|
||||
// if observer is observing themselves, then we're not really observing
|
||||
const observeeId =
|
||||
!newObserveeId || newObserveeId === store.getState().currentUser.id ? null : newObserveeId
|
||||
|
||||
if (
|
||||
observeeId !== store.getState().selectedObservee &&
|
||||
(store.getState().weeklyDashboard || ENV.FEATURES.observer_picker)
|
||||
) {
|
||||
store.dispatch(reloadWithObservee(observeeId))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import reducer from '../courses-reducer'
|
||||
import {gotGradesSuccess} from '../../actions'
|
||||
import {gotGradesSuccess, clearCourses} from '../../actions'
|
||||
|
||||
it('merges grades into courses', () => {
|
||||
const courses = [
|
||||
|
@ -25,10 +25,31 @@ it('merges grades into courses', () => {
|
|||
{id: '2', otherData: 'second-other-fields'}
|
||||
]
|
||||
const grades = {
|
||||
'1': {courseId: '1', hasGradingPeriods: true, grade: '34.42%'},
|
||||
'2': {courseId: '2', hasGradingPeriods: false, grade: '42.34%'}
|
||||
1: {courseId: '1', hasGradingPeriods: true, grade: '34.42%'},
|
||||
2: {courseId: '2', hasGradingPeriods: false, grade: '42.34%'}
|
||||
}
|
||||
const action = gotGradesSuccess(grades)
|
||||
const nextState = reducer(courses, action)
|
||||
expect(nextState).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('CLEAR_COURSES', () => {
|
||||
it('clears courses', () => {
|
||||
const courses = [
|
||||
{id: '1', otherData: 'first-other-fields'},
|
||||
{id: '2', otherData: 'second-other-fields'}
|
||||
]
|
||||
|
||||
const action = clearCourses()
|
||||
const nextState = reducer(courses, action)
|
||||
expect(nextState).toEqual([])
|
||||
})
|
||||
|
||||
it('does not clear courses in singleCourse mode', () => {
|
||||
const courses = [{id: '1', otherData: 'just-one-course'}]
|
||||
|
||||
const action = clearCourses(true)
|
||||
const nextState = reducer(courses, action)
|
||||
expect(nextState).toBe(courses)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
*/
|
||||
import {handleActions} from 'redux-actions'
|
||||
|
||||
const defaultState = []
|
||||
|
||||
function mergeGradesIntoCourses(courses, action) {
|
||||
const grades = action.payload
|
||||
return courses.map(course => {
|
||||
|
@ -26,21 +28,22 @@ function mergeGradesIntoCourses(courses, action) {
|
|||
})
|
||||
}
|
||||
|
||||
const getPlannerCourses = ({
|
||||
payload: {
|
||||
env: {COURSE, STUDENT_PLANNER_COURSES}
|
||||
}
|
||||
}) => {
|
||||
if (!STUDENT_PLANNER_COURSES.length && COURSE) {
|
||||
return [COURSE]
|
||||
}
|
||||
return STUDENT_PLANNER_COURSES
|
||||
}
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
INITIAL_OPTIONS: (state, action) => getPlannerCourses(action),
|
||||
GOT_GRADES_SUCCESS: mergeGradesIntoCourses
|
||||
INITIAL_OPTIONS: (state, action) => {
|
||||
if (action.payload.singleCourse) {
|
||||
return [action.payload.env.COURSE]
|
||||
}
|
||||
return []
|
||||
},
|
||||
GOT_COURSE_LIST: (state, action) => {
|
||||
return action.payload
|
||||
},
|
||||
GOT_GRADES_SUCCESS: mergeGradesIntoCourses,
|
||||
CLEAR_COURSES: (state, action) => {
|
||||
if (action.payload) return state
|
||||
return defaultState
|
||||
}
|
||||
},
|
||||
[]
|
||||
defaultState
|
||||
)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import moment from 'moment-timezone'
|
||||
import {combineReducers} from 'redux'
|
||||
import {handleAction, handleActions} from 'redux-actions'
|
||||
import {handleAction} from 'redux-actions'
|
||||
import days from './days-reducer'
|
||||
import loading from './loading-reducer'
|
||||
import courses from './courses-reducer'
|
||||
|
@ -29,6 +29,7 @@ import ui from './ui-reducer'
|
|||
import savePlannerItem from './save-item-reducer'
|
||||
import sidebar from './sidebar-reducer'
|
||||
import weeklyDashboard from './weekly-reducer'
|
||||
import selectedObservee from './selected-observee-reducer'
|
||||
|
||||
const locale = handleAction(
|
||||
'INITIAL_OPTIONS',
|
||||
|
@ -87,19 +88,6 @@ const firstNewActivityDate = handleAction(
|
|||
null
|
||||
)
|
||||
|
||||
const selectedObservee = handleActions(
|
||||
{
|
||||
SELECTED_OBSERVEE: (state, action) => ({
|
||||
id: action.payload?.id,
|
||||
contextCodes: action.payload?.contextCodes
|
||||
}),
|
||||
INITIAL_OPTIONS: (state, action) => ({
|
||||
id: action.payload.observedUserId
|
||||
})
|
||||
},
|
||||
null
|
||||
)
|
||||
|
||||
const combinedReducers = combineReducers({
|
||||
courses,
|
||||
groups,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (C) 2022 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {handleActions} from 'redux-actions'
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
INITIAL_OPTIONS: (state, action) => {
|
||||
return action.payload.observedUserId || action.payload.observedUser?.id || null
|
||||
},
|
||||
SELECTED_OBSERVEE: (state, action) => {
|
||||
return action.payload || null
|
||||
}
|
||||
},
|
||||
null
|
||||
)
|
|
@ -22,7 +22,10 @@ import {
|
|||
transformInternalToApiOverride,
|
||||
transformPlannerNoteApiToInternalItem,
|
||||
transformApiToInternalGrade,
|
||||
observedUserId
|
||||
observedUserId,
|
||||
observedUserContextCodes,
|
||||
buildURL,
|
||||
getContextCodesFromState
|
||||
} from '../apiUtils'
|
||||
|
||||
const courses = [
|
||||
|
@ -646,17 +649,95 @@ describe('observedUserId', () => {
|
|||
selectedObservee: null
|
||||
}
|
||||
|
||||
it('returns undefined if selectedObservee.id does not exist', () => {
|
||||
expect(observedUserId(defaultState)).toBeUndefined()
|
||||
it('returns null if selectedObservee does not exist', () => {
|
||||
expect(observedUserId(defaultState)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns undefined if the selectedObservee is the same as the current user', () => {
|
||||
const state = {...defaultState, selectedObservee: {id: '3'}}
|
||||
expect(observedUserId(state)).toBeUndefined()
|
||||
it('returns null if the selectedObservee is the same as the current user', () => {
|
||||
const state = {...defaultState, selectedObservee: '3'}
|
||||
expect(observedUserId(state)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns the observee id if present and not the current user', () => {
|
||||
const state = {...defaultState, selectedObservee: {id: '2'}}
|
||||
const state = {...defaultState, selectedObservee: '2'}
|
||||
expect(observedUserId(state)).toBe('2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('observedUserContextCodes', () => {
|
||||
it('returns undefined if selectedObservee is the current user', () => {
|
||||
expect(
|
||||
observedUserContextCodes({
|
||||
currentUser: {id: '3'},
|
||||
selectedObservee: '3'
|
||||
})
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns the course context codes for an observee', () => {
|
||||
expect(
|
||||
observedUserContextCodes({
|
||||
currentUser: {id: '3'},
|
||||
selectedObservee: '17',
|
||||
courses: [{id: '20'}, {id: '4'}]
|
||||
})
|
||||
).toStrictEqual(['course_4', 'course_20'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildURL', () => {
|
||||
it('returns a url with params in the expected order', () => {
|
||||
const url = buildURL('/here/there', {
|
||||
course_ids: ['50', '7'],
|
||||
context_codes: ['g_30', 'g_5'],
|
||||
observed_user_id: 'f',
|
||||
per_page: 'e',
|
||||
order: 'd',
|
||||
filter: 'c',
|
||||
include: 'i',
|
||||
end_date: 'b',
|
||||
start_date: 'a'
|
||||
})
|
||||
expect(url).toStrictEqual(
|
||||
'/here/there?start_date=a&end_date=b&include=i&filter=c&order=d&per_page=e&observed_user_id=f&context_codes%5B%5D=g_5&context_codes%5B%5D=g_30&course_ids%5B%5D=7&course_ids%5B%5D=50'
|
||||
)
|
||||
})
|
||||
|
||||
it('appends unordered params to the end', () => {
|
||||
const url1 = buildURL('/here/there', {
|
||||
filter: 'c',
|
||||
start_date: 'a',
|
||||
foo: 'bar'
|
||||
})
|
||||
expect(url1).toStrictEqual('/here/there?start_date=a&filter=c&foo=bar')
|
||||
const url2 = buildURL('/here/there', {
|
||||
filter: 'c',
|
||||
start_date: 'a',
|
||||
foo: ['bar', 'baz']
|
||||
})
|
||||
expect(url2).toStrictEqual('/here/there?start_date=a&filter=c&foo%5B%5D=bar&foo%5B%5D=baz')
|
||||
})
|
||||
|
||||
it('omits undefined and null params', () => {
|
||||
const url1 = buildURL('/here/there', {
|
||||
filter: undefined,
|
||||
start_date: 'a'
|
||||
})
|
||||
expect(url1).toStrictEqual('/here/there?start_date=a')
|
||||
const url2 = buildURL('/here/there', {
|
||||
foo: null,
|
||||
start_date: 'a'
|
||||
})
|
||||
expect(url2).toStrictEqual('/here/there?start_date=a')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getContextCodesFromState', () => {
|
||||
it('returns context codes in sorted order', () => {
|
||||
expect(
|
||||
getContextCodesFromState({
|
||||
courses: [{id: '20'}, {id: '4'}]
|
||||
})
|
||||
).toStrictEqual(['course_4', 'course_20'])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -80,7 +80,7 @@ const getApiItemType = overrideType => {
|
|||
}
|
||||
|
||||
export function findNextLink(response) {
|
||||
const linkHeader = response.headers.link
|
||||
const linkHeader = getResponseHeader(response, 'link')
|
||||
if (linkHeader == null) return null
|
||||
|
||||
const parsedLinks = parseLinkHeader(linkHeader)
|
||||
|
@ -214,7 +214,11 @@ export function transformApiToInternalGrade(apiResult) {
|
|||
}
|
||||
|
||||
export function getContextCodesFromState({courses = []}) {
|
||||
return courses?.length ? courses.map(({id}) => `course_${id}`) : undefined
|
||||
return courses?.length
|
||||
? courses
|
||||
.map(({id}) => `course_${id}`)
|
||||
.sort((a, b) => a.localeCompare(b, 'en', {numeric: true}))
|
||||
: undefined
|
||||
}
|
||||
|
||||
function getCourseContext(course) {
|
||||
|
@ -260,7 +264,68 @@ function isComplete(apiResponse) {
|
|||
}
|
||||
|
||||
export function observedUserId(state) {
|
||||
if (state.selectedObservee?.id && state.selectedObservee.id !== state.currentUser.id) {
|
||||
return state.selectedObservee.id
|
||||
if (state.selectedObservee && state.selectedObservee !== state.currentUser.id) {
|
||||
return state.selectedObservee
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function observedUserContextCodes(state) {
|
||||
if (state.selectedObservee && state.selectedObservee !== state.currentUser.id) {
|
||||
return getContextCodesFromState(state)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getResponseHeader(response, name) {
|
||||
return response.headers.get?.(name) || response.headers[name]
|
||||
}
|
||||
|
||||
// take a base url and object of params and generate
|
||||
// a url with query_string parameters for the params
|
||||
//
|
||||
// To build a URL that matches the one build for the prefetch
|
||||
// params are in the following order
|
||||
const paramOrder = [
|
||||
'start_date',
|
||||
'end_date',
|
||||
'include',
|
||||
'filter',
|
||||
'order',
|
||||
'per_page',
|
||||
'observed_user_id',
|
||||
'context_codes',
|
||||
'course_ids'
|
||||
]
|
||||
export function buildURL(url, params = {}) {
|
||||
const result = new URL(url, 'http://localhost/')
|
||||
const params2 = {...params}
|
||||
|
||||
// first the order-dependent params
|
||||
paramOrder.forEach(key => {
|
||||
if (key in params2) {
|
||||
serializeParamIntoURL(key, params2[key], result)
|
||||
delete params2[key]
|
||||
}
|
||||
})
|
||||
// then any left over
|
||||
Object.keys(params2).forEach(key => {
|
||||
serializeParamIntoURL(key, params2[key], result)
|
||||
})
|
||||
return `${result.pathname}${result.search}`
|
||||
}
|
||||
|
||||
function serializeParamIntoURL(key, val, url) {
|
||||
if (val === null || typeof val === 'undefined') return
|
||||
if (Array.isArray(val)) {
|
||||
// assumes values are strings
|
||||
val
|
||||
.sort((a, b) => a.localeCompare(b, 'en', {numeric: true}))
|
||||
.forEach(arrayVal => {
|
||||
if (arrayVal === null || typeof arrayVal === 'undefined') return
|
||||
url.searchParams.append(`${key}[]`, arrayVal)
|
||||
})
|
||||
} else {
|
||||
url.searchParams.append(key, val)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
"main": "lib/index.js",
|
||||
"module": "es/index.js",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@instructure/get-cookie": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo 'no tests yet...'",
|
||||
"build:canvas": "babel --root-mode upward src -d es & JEST_WORKER_ID=true babel --root-mode upward src -d lib"
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import getCookie from '@instructure/get-cookie'
|
||||
|
||||
// These are helpful methods you can use along side the ruby ApplicationHelper::prefetch_xhr helper method in canvas
|
||||
|
||||
|
@ -80,11 +81,16 @@ export function clearPrefetchedXHRs() {
|
|||
* @param {Promise<Response>} fetchRequest
|
||||
* @returns {Promise<import("axios").AxiosResponse>}
|
||||
*/
|
||||
export function asAxios(fetchRequest) {
|
||||
export function asAxios(fetchRequest, type = 'json') {
|
||||
if (!fetchRequest) return
|
||||
return fetchRequest
|
||||
.then(checkStatus)
|
||||
.then(res => res.json().then(data => ({data, headers: {link: res.headers.get('Link')}})))
|
||||
return fetchRequest.then(checkStatus).then(res =>
|
||||
res
|
||||
.clone()
|
||||
[type]()
|
||||
.then(data => {
|
||||
return {data, headers: {link: res.headers.get('Link')}}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,7 +101,7 @@ export function asAxios(fetchRequest) {
|
|||
*/
|
||||
export function asJson(fetchRequest) {
|
||||
if (!fetchRequest) return
|
||||
return fetchRequest.then(checkStatus).then(res => res.json())
|
||||
return fetchRequest.then(checkStatus).then(res => res.clone().json())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -106,7 +112,7 @@ export function asJson(fetchRequest) {
|
|||
*/
|
||||
export function asText(fetchRequest) {
|
||||
if (!fetchRequest) return
|
||||
return fetchRequest.then(checkStatus).then(res => res.text())
|
||||
return fetchRequest.then(checkStatus).then(res => res.clone().text())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,6 +139,7 @@ export const defaultFetchOptions = {
|
|||
credentials: 'same-origin',
|
||||
headers: {
|
||||
Accept: 'application/json+canvas-string-ids, application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-Token': getCookie('_csrf_token')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import fetchMock from 'fetch-mock'
|
||||
import DashboardCardBackgroundStore from '@canvas/dashboard-card/react/DashboardCardBackgroundStore'
|
||||
import fakeENV from 'helpers/fakeENV'
|
||||
|
||||
|
@ -24,7 +25,7 @@ const TEST_COLORS = {
|
|||
'#91349B': '#91349B',
|
||||
'#E1185C': '#E1185C'
|
||||
}
|
||||
DashboardCardBackgroundStore.reset = function() {
|
||||
DashboardCardBackgroundStore.reset = function () {
|
||||
return this.setState({
|
||||
courseColors: TEST_COLORS,
|
||||
usedDefaults: []
|
||||
|
@ -37,26 +38,24 @@ QUnit.module('DashboardCardBackgroundStore', {
|
|||
fakeENV.setup()
|
||||
ENV.PREFERENCES = {custom_colors: TEST_COLORS}
|
||||
ENV.current_user_id = 22
|
||||
this.server = sinon.fakeServer.create()
|
||||
this.response = []
|
||||
this.server.respondWith('POST', '/api/v1/users/22/colors/course_1', [
|
||||
200,
|
||||
{'Content-Type': 'application/json'},
|
||||
''
|
||||
])
|
||||
this.server.respondWith('POST', '/api/v1/users/22/colors/course_2', [
|
||||
200,
|
||||
{'Content-Type': 'application/json'},
|
||||
''
|
||||
])
|
||||
this.server.respondWith('POST', '/api/v1/users/22/colors/course_3', [
|
||||
200,
|
||||
{'Content-Type': 'application/json'},
|
||||
''
|
||||
])
|
||||
fetchMock.put('path:/api/v1/users/22/colors/course_1', {
|
||||
status: 200,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: ''
|
||||
})
|
||||
fetchMock.put('path:/api/v1/users/22/colors/course_2', {
|
||||
status: 200,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: ''
|
||||
})
|
||||
fetchMock.put('path:/api/v1/users/22/colors/course_3', {
|
||||
status: 200,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: ''
|
||||
})
|
||||
},
|
||||
teardown() {
|
||||
this.server.restore()
|
||||
fetchMock.restore()
|
||||
DashboardCardBackgroundStore.reset()
|
||||
fakeENV.teardown()
|
||||
}
|
||||
|
@ -84,19 +83,19 @@ test('maintains list of used defaults', () => {
|
|||
ok(DashboardCardBackgroundStore.getUsedDefaults().includes('#91349B'))
|
||||
})
|
||||
|
||||
test('posts to the server when a default is set', function() {
|
||||
test('PUTs to the server when a default is set', async function () {
|
||||
DashboardCardBackgroundStore.setDefaultColor('course_1')
|
||||
ok(this.server.requests[0].url.match(/course_1/))
|
||||
equal(this.server.requests.length, 1)
|
||||
this.server.respond()
|
||||
await fetchMock.flush()
|
||||
ok(fetchMock.lastUrl().match(/course_1/))
|
||||
equal(fetchMock.calls().length, 1)
|
||||
})
|
||||
|
||||
test('sets multiple defaults properly', function() {
|
||||
test('sets multiple defaults properly', async function () {
|
||||
DashboardCardBackgroundStore.setDefaultColors(['course_2', 'course_3'])
|
||||
ok(this.server.requests[0].url.match(/course_2/))
|
||||
ok(this.server.requests[1].url.match(/course_3/))
|
||||
equal(this.server.requests.length, 2)
|
||||
return this.server.respond()
|
||||
await fetchMock.flush()
|
||||
ok(fetchMock.calls()[0][0].match(/course_2/))
|
||||
ok(fetchMock.calls()[1][0].match(/course_3/))
|
||||
equal(fetchMock.calls().length, 2)
|
||||
})
|
||||
|
||||
// ==========================
|
||||
|
|
|
@ -22,10 +22,12 @@ import {DragDropContext} from 'react-dnd'
|
|||
import ReactDndTestBackend from 'react-dnd-test-backend'
|
||||
import sinon from 'sinon'
|
||||
import {waitFor} from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
||||
import DashboardCard from '@canvas/dashboard-card/react/DashboardCard'
|
||||
import getDroppableDashboardCardBox from '@canvas/dashboard-card/react/getDroppableDashboardCardBox'
|
||||
import CourseActivitySummaryStore from '@canvas/dashboard-card/react/CourseActivitySummaryStore'
|
||||
import fakeENV from 'helpers/fakeENV'
|
||||
|
||||
QUnit.module('DashboardCardBox', suiteHooks => {
|
||||
let $container
|
||||
|
@ -33,6 +35,7 @@ QUnit.module('DashboardCardBox', suiteHooks => {
|
|||
let server
|
||||
|
||||
suiteHooks.beforeEach(() => {
|
||||
fakeENV.setup()
|
||||
$container = document.createElement('div')
|
||||
document.body.appendChild($container)
|
||||
|
||||
|
@ -42,16 +45,24 @@ QUnit.module('DashboardCardBox', suiteHooks => {
|
|||
{
|
||||
id: '1',
|
||||
isFavorited: true,
|
||||
courseName: 'Bio 101'
|
||||
courseName: 'Bio 101',
|
||||
assetString: 'course_1'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
isFavorited: true,
|
||||
courseName: 'Philosophy 201'
|
||||
courseName: 'Philosophy 201',
|
||||
assetString: 'course_1'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
fetchMock.put(/\/api\/v1\/users\/1\/colors\/.*/, {
|
||||
status: 200,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: ''
|
||||
})
|
||||
|
||||
server = sinon.fakeServer.create({respondImmediately: true})
|
||||
return sandbox.stub(CourseActivitySummaryStore, 'getStateForCourse').returns({})
|
||||
})
|
||||
|
@ -60,6 +71,7 @@ QUnit.module('DashboardCardBox', suiteHooks => {
|
|||
ReactDOM.unmountComponentAtNode($container)
|
||||
$container.remove()
|
||||
server.restore()
|
||||
fakeENV.teardown()
|
||||
})
|
||||
|
||||
function mountComponent() {
|
||||
|
@ -99,9 +111,9 @@ QUnit.module('DashboardCardBox', suiteHooks => {
|
|||
}
|
||||
|
||||
function getUnfavoriteButton() {
|
||||
return [
|
||||
...getDashboardMenu().querySelectorAll('.DashboardCardMenu__MovementItem')
|
||||
].find($button => $button.textContent.includes('Unfavorite'))
|
||||
return [...getDashboardMenu().querySelectorAll('.DashboardCardMenu__MovementItem')].find(
|
||||
$button => $button.textContent.includes('Unfavorite')
|
||||
)
|
||||
}
|
||||
|
||||
function getModal() {
|
||||
|
|
|
@ -2728,7 +2728,7 @@ describe UsersController do
|
|||
end
|
||||
end
|
||||
|
||||
context "@cards_prefetch_observer_param" do
|
||||
context "@cards_prefetch_observed_param" do
|
||||
before :once do
|
||||
Account.site_admin.enable_feature!(:k5_parent_support)
|
||||
@user1 = user_factory(active_all: true, account: @account)
|
||||
|
@ -2744,13 +2744,13 @@ describe UsersController do
|
|||
@course.enroll_student(student)
|
||||
@course.enroll_user(@user1, "ObserverEnrollment", { associated_user_id: student.id })
|
||||
get "user_dashboard"
|
||||
expect(controller.instance_variable_get(:@cards_prefetch_observer_param)).to eq student.id
|
||||
expect(controller.instance_variable_get(:@cards_prefetch_observed_param)).to eq student.id
|
||||
end
|
||||
|
||||
it "is undefined when user is not an observer" do
|
||||
@course.enroll_student(@user1)
|
||||
get "user_dashboard"
|
||||
expect(controller.instance_variable_get(:@cards_prefetch_observer_param)).to be_nil
|
||||
expect(controller.instance_variable_get(:@cards_prefetch_observed_param)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -240,9 +240,9 @@ QUnit.module('Dashboard Header', hooks => {
|
|||
dashboardHeader.changeDashboard('cards')
|
||||
|
||||
moxios.wait(() => {
|
||||
const request = moxios.requests.mostRecent()
|
||||
equal(request.url, '/dashboard/view')
|
||||
equal(request.config.data, '{"dashboard_view":"cards"}')
|
||||
const requests = moxios.requests
|
||||
equal(requests.at(1).url, '/dashboard/view')
|
||||
equal(requests.at(1).config.data, '{"dashboard_view":"cards"}')
|
||||
done()
|
||||
})
|
||||
ok(plannerStub.notCalled)
|
||||
|
|
|
@ -30,6 +30,7 @@ let fakeServer
|
|||
|
||||
QUnit.module('DashboardCard Reordering', {
|
||||
setup() {
|
||||
fakeENV.setup()
|
||||
cards = [
|
||||
{
|
||||
id: '1',
|
||||
|
@ -63,6 +64,10 @@ QUnit.module('DashboardCard Reordering', {
|
|||
sandbox.fetch.mock('path:/api/v1/courses/1/activity_stream/summary', 200)
|
||||
sandbox.fetch.mock('path:/api/v1/courses/2/activity_stream/summary', 200)
|
||||
sandbox.fetch.mock('path:/api/v1/courses/3/activity_stream/summary', 200)
|
||||
sandbox.fetch.mock(new RegExp(`\/api\/v1\/users\/${ENV.current_user_id}\/colors.*`), {
|
||||
status: 200,
|
||||
body: '{}'
|
||||
})
|
||||
},
|
||||
teardown() {
|
||||
fakeENV.teardown()
|
||||
|
|
|
@ -25,18 +25,20 @@ import {bool, func, string, object, oneOf} from 'prop-types'
|
|||
import {
|
||||
initializePlanner,
|
||||
loadPlannerDashboard,
|
||||
reloadPlannerForObserver,
|
||||
renderToDoSidebar,
|
||||
responsiviser
|
||||
} from '@instructure/canvas-planner'
|
||||
import {asAxios, getPrefetchedXHR} from '@instructure/js-utils'
|
||||
import {showFlashAlert, showFlashError} from '@canvas/alerts/react/FlashAlert'
|
||||
import apiUserContent from '@canvas/util/jquery/apiUserContent'
|
||||
import DashboardOptionsMenu from './DashboardOptionsMenu'
|
||||
import loadCardDashboard, {resetDashboardCards} from '@canvas/dashboard-card'
|
||||
import {CardDashboardLoader} from '@canvas/dashboard-card'
|
||||
import $ from 'jquery'
|
||||
import {asText, getPrefetchedXHR} from '@instructure/js-utils'
|
||||
import '@canvas/jquery/jquery.disableWhileLoading'
|
||||
import {CreateCourseModal} from '@canvas/create-course-modal/react/CreateCourseModal'
|
||||
import ObserverOptions from '@canvas/observer-picker'
|
||||
import {savedObservedId} from '@canvas/observer-picker/ObserverGetObservee'
|
||||
import {View} from '@instructure/ui-view'
|
||||
|
||||
const [show, hide] = ['block', 'none'].map(displayVal => id => {
|
||||
|
@ -72,8 +74,19 @@ class DashboardHeader extends React.Component {
|
|||
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.cardDashboardLoader = new CardDashboardLoader()
|
||||
this.planner_init_promise = undefined
|
||||
if (this.props.planner_enabled) {
|
||||
// setup observing another user?
|
||||
let observedUser
|
||||
if (observerMode() && ENV.OBSERVED_USERS_LIST.length > 0) {
|
||||
const storedObservedUserId = savedObservedId(ENV.current_user.id)
|
||||
const {id, name, avatar_url} =
|
||||
ENV.OBSERVED_USERS_LIST.find(u => u.id === storedObservedUserId) ||
|
||||
ENV.OBSERVED_USERS_LIST[0]
|
||||
observedUser = id === ENV.current_user_id ? null : {id, name, avatarUrl: avatar_url}
|
||||
}
|
||||
|
||||
this.planner_init_promise = initializePlanner({
|
||||
changeDashboardView: this.changeDashboard,
|
||||
getActiveApp: this.getActiveApp,
|
||||
|
@ -87,6 +100,7 @@ class DashboardHeader extends React.Component {
|
|||
datetimeString: $.datetimeString
|
||||
},
|
||||
externalFallbackFocusable: this.menuButtonFocusable,
|
||||
observedUser,
|
||||
env: this.props.env
|
||||
})
|
||||
}
|
||||
|
@ -130,7 +144,7 @@ class DashboardHeader extends React.Component {
|
|||
|
||||
loadCardDashboard(observedUserId) {
|
||||
// I put this in so I can spy on the imported function in a spec :'(
|
||||
loadCardDashboard(undefined, observedUserId)
|
||||
this.cardDashboardLoader.loadCardDashboard(undefined, observedUserId)
|
||||
}
|
||||
|
||||
loadStreamItemDashboard(observedUserId) {
|
||||
|
@ -174,9 +188,9 @@ class DashboardHeader extends React.Component {
|
|||
.then(() => {
|
||||
this.loadPlannerComponent()
|
||||
})
|
||||
.catch(() =>
|
||||
.catch(_ex => {
|
||||
showFlashAlert({message: I18n.t('Failed initializing dashboard'), type: 'error'})
|
||||
)
|
||||
})
|
||||
} else if (newView === 'cards') {
|
||||
this.loadCardDashboard(this.state.selectedObserveeId)
|
||||
} else if (newView === 'activity') {
|
||||
|
@ -214,6 +228,19 @@ class DashboardHeader extends React.Component {
|
|||
.catch(showFlashError(I18n.t('Failed to save dashboard selection')))
|
||||
}
|
||||
|
||||
handleChangeObservedUser(id) {
|
||||
this.reloadDashboardForObserver(id)
|
||||
if (this.props.planner_enabled) {
|
||||
this.planner_init_promise
|
||||
.then(() => {
|
||||
reloadPlannerForObserver(id)
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore. handled elsewhere
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
changeDashboard = newView => {
|
||||
if (newView === 'elementary') {
|
||||
this.switchToElementary()
|
||||
|
@ -253,8 +280,8 @@ class DashboardHeader extends React.Component {
|
|||
|
||||
reloadDashboardForObserver = userId => {
|
||||
this.sidebarHasLoaded = false
|
||||
resetDashboardCards()
|
||||
this.setState({selectedObserveeId: userId, loadedViews: []}, () => {
|
||||
this.cardDashboardLoader = new CardDashboardLoader()
|
||||
this.loadDashboard(this.state.currentDashboard)
|
||||
})
|
||||
}
|
||||
|
@ -273,7 +300,7 @@ class DashboardHeader extends React.Component {
|
|||
currentUserRoles={ENV.current_user_roles}
|
||||
observedUsersList={ENV.OBSERVED_USERS_LIST}
|
||||
canAddObservee={ENV.CAN_ADD_OBSERVEE}
|
||||
handleChangeObservedUser={this.reloadDashboardForObserver}
|
||||
handleChangeObservedUser={id => this.handleChangeObservedUser(id)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
@ -305,7 +332,6 @@ class DashboardHeader extends React.Component {
|
|||
export {DashboardHeader}
|
||||
export default responsiviser()(DashboardHeader)
|
||||
|
||||
let readSidebarPrefetch = false
|
||||
// extract this out to a property so tests can override it and not have to mock
|
||||
// out the timers in every single test.
|
||||
function loadDashboardSidebar(observedUserId) {
|
||||
|
@ -316,44 +342,40 @@ function loadDashboardSidebar(observedUserId) {
|
|||
|
||||
const rightSide = $('#right-side')
|
||||
const promiseToGetNewCourseForm = import('../jquery/util/newCourseForm')
|
||||
const prefetchedXhr = getPrefetchedXHR(dashboardSidebarUrl)
|
||||
const promiseToGetHtml =
|
||||
!readSidebarPrefetch && prefetchedXhr !== undefined
|
||||
? asText(prefetchedXhr)
|
||||
: $.get(dashboardSidebarUrl)
|
||||
readSidebarPrefetch = true
|
||||
asAxios(getPrefetchedXHR(dashboardSidebarUrl), 'text') || axios.get(dashboardSidebarUrl)
|
||||
|
||||
rightSide.disableWhileLoading(
|
||||
Promise.all([promiseToGetNewCourseForm, promiseToGetHtml]).then(
|
||||
([{default: newCourseForm}, html]) => {
|
||||
// inject the erb html we got from the server
|
||||
rightSide.html(html)
|
||||
newCourseForm()
|
||||
Promise.all([promiseToGetNewCourseForm, promiseToGetHtml]).then(response => {
|
||||
const newCourseForm = response[0].default
|
||||
const html = response[1].data
|
||||
// inject the erb html we got from the server
|
||||
rightSide.html(html)
|
||||
newCourseForm()
|
||||
|
||||
// the injected html has a .Sidebar__TodoListContainer element in it,
|
||||
// render the canvas-planner ToDo list into it
|
||||
const container = document.querySelector('.Sidebar__TodoListContainer')
|
||||
if (container) renderToDoSidebar(container)
|
||||
// the injected html has a .Sidebar__TodoListContainer element in it,
|
||||
// render the canvas-planner ToDo list into it
|
||||
const container = document.querySelector('.Sidebar__TodoListContainer')
|
||||
if (container) renderToDoSidebar(container)
|
||||
|
||||
const startButton = document.getElementById('start_new_course')
|
||||
const modalContainer = document.getElementById('create_course_modal_container')
|
||||
if (startButton && modalContainer && ENV.FEATURES?.create_course_subaccount_picker) {
|
||||
startButton.addEventListener('click', () => {
|
||||
ReactDOM.render(
|
||||
<CreateCourseModal
|
||||
isModalOpen
|
||||
setModalOpen={isOpen => {
|
||||
if (!isOpen) ReactDOM.unmountComponentAtNode(modalContainer)
|
||||
}}
|
||||
permissions={ENV.CREATE_COURSES_PERMISSIONS.PERMISSION}
|
||||
restrictToMCCAccount={ENV.CREATE_COURSES_PERMISSIONS.RESTRICT_TO_MCC_ACCOUNT}
|
||||
isK5User={false} // can't be k5 user if classic dashboard is showing
|
||||
/>,
|
||||
modalContainer
|
||||
)
|
||||
})
|
||||
}
|
||||
const startButton = document.getElementById('start_new_course')
|
||||
const modalContainer = document.getElementById('create_course_modal_container')
|
||||
if (startButton && modalContainer && ENV.FEATURES?.create_course_subaccount_picker) {
|
||||
startButton.addEventListener('click', () => {
|
||||
ReactDOM.render(
|
||||
<CreateCourseModal
|
||||
isModalOpen
|
||||
setModalOpen={isOpen => {
|
||||
if (!isOpen) ReactDOM.unmountComponentAtNode(modalContainer)
|
||||
}}
|
||||
permissions={ENV.CREATE_COURSES_PERMISSIONS.PERMISSION}
|
||||
restrictToMCCAccount={ENV.CREATE_COURSES_PERMISSIONS.RESTRICT_TO_MCC_ACCOUNT}
|
||||
isK5User={false} // can't be k5 user if classic dashboard is showing
|
||||
/>,
|
||||
modalContainer
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -470,7 +470,7 @@ export function K5Course({
|
|||
}) {
|
||||
const initialObservedId = observedUsersList.find(o => o.id === savedObservedId(currentUser.id))
|
||||
? savedObservedId(currentUser.id)
|
||||
: undefined
|
||||
: null
|
||||
|
||||
const renderTabs = toRenderTabs(tabs, hasSyllabusBody)
|
||||
const {activeTab, currentTab, handleTabChange} = useTabState(defaultTab, renderTabs)
|
||||
|
|
|
@ -44,7 +44,7 @@ import GradesPage from './GradesPage'
|
|||
import HomeroomPage from './HomeroomPage'
|
||||
import TodosPage from './TodosPage'
|
||||
import K5DashboardContext from '@canvas/k5/react/K5DashboardContext'
|
||||
import loadCardDashboard, {resetDashboardCards} from '@canvas/dashboard-card'
|
||||
import {CardDashboardLoader} from '@canvas/dashboard-card'
|
||||
import {mapStateToProps} from '@canvas/k5/redux/redux-helpers'
|
||||
import SchedulePage from '@canvas/k5/react/SchedulePage'
|
||||
import ResourcesPage from '@canvas/k5/react/ResourcesPage'
|
||||
|
@ -158,6 +158,7 @@ export const K5Dashboard = ({
|
|||
const [tabsRef, setTabsRef] = useState(null)
|
||||
const [trayOpen, setTrayOpen] = useState(false)
|
||||
const [observedUserId, setObservedUserId] = useState(initialObservedId)
|
||||
const [cardDashboardLoader, setCardDashboardLoader] = useState(null)
|
||||
const plannerInitialized = usePlanner({
|
||||
plannerEnabled,
|
||||
isPlannerActive: () => activeTab.current === TAB_IDS.SCHEDULE,
|
||||
|
@ -189,7 +190,7 @@ export const K5Dashboard = ({
|
|||
|
||||
const handleChangeObservedUser = id => {
|
||||
if (id !== observedUserId) {
|
||||
resetDashboardCards()
|
||||
setCardDashboardLoader(null)
|
||||
setCardsSettled(false)
|
||||
setObservedUserId(id)
|
||||
}
|
||||
|
@ -198,8 +199,11 @@ export const K5Dashboard = ({
|
|||
useEffect(() => {
|
||||
// don't call on the initial load when we know we're in observer mode but don't have the ID yet
|
||||
if (!observerMode || (observerMode && observedUserId)) {
|
||||
loadCardDashboard(loadCardDashboardCallBack, observerMode ? observedUserId : undefined)
|
||||
const dcl = new CardDashboardLoader()
|
||||
dcl.loadCardDashboard(loadCardDashboardCallBack, observerMode ? observedUserId : undefined)
|
||||
setCardDashboardLoader(dcl)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [observedUserId, observerMode])
|
||||
|
||||
useFetchApi({
|
||||
|
@ -349,7 +353,6 @@ export const K5Dashboard = ({
|
|||
visible={currentTab === TAB_IDS.SCHEDULE}
|
||||
singleCourse={false}
|
||||
observedUserId={observedUserId}
|
||||
contextCodes={cardsSettled ? cards?.map(c => c.assetString) : undefined}
|
||||
/>
|
||||
<GradesPage
|
||||
visible={currentTab === TAB_IDS.GRADES}
|
||||
|
|
|
@ -15,17 +15,15 @@
|
|||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import moment from 'moment-timezone'
|
||||
import moxios from 'moxios'
|
||||
import {act, render, screen, waitFor} from '@testing-library/react'
|
||||
import {resetDashboardCards, resetCardCache} from '@canvas/dashboard-card'
|
||||
import {resetCardCache} from '@canvas/dashboard-card'
|
||||
import {resetPlanner} from '@instructure/canvas-planner'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import {OBSERVER_COOKIE_PREFIX} from '@canvas/observer-picker/ObserverGetObservee'
|
||||
import {cloneDeep} from 'lodash'
|
||||
|
||||
import {MOCK_TODOS} from './mocks'
|
||||
import {
|
||||
MOCK_ASSIGNMENTS,
|
||||
|
@ -39,13 +37,12 @@ import K5Dashboard from '../K5Dashboard'
|
|||
import {destroyContainer} from '@canvas/alerts/react/FlashAlert'
|
||||
|
||||
const ASSIGNMENTS_URL = /\/api\/v1\/calendar_events\?type=assignment&important_dates=true&.*/
|
||||
|
||||
const currentUserId = '1'
|
||||
const observedUserCookieName = `${OBSERVER_COOKIE_PREFIX}${currentUserId}`
|
||||
|
||||
const currentUser = {
|
||||
id: currentUserId,
|
||||
display_name: 'Geoffrey Jellineck',
|
||||
name: 'Geoffrey Jellineck',
|
||||
avatar_image_url: 'http://avatar'
|
||||
}
|
||||
const cardSummary = [
|
||||
|
@ -196,18 +193,8 @@ const defaultProps = {
|
|||
observedUsersList: MOCK_OBSERVED_USERS_LIST,
|
||||
openTodosInNewTab: true
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
jest.setTimeout(15000)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.setTimeout(5000)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
moxios.install()
|
||||
moxios.stubRequest('/api/v1/dashboard/dashboard_cards', {
|
||||
function createPlannerMocks() {
|
||||
moxios.stubRequest(/\/api\/v1\/dashboard\/dashboard_cards$/, {
|
||||
status: 200,
|
||||
response: MOCK_CARDS
|
||||
})
|
||||
|
@ -269,6 +256,16 @@ beforeEach(() => {
|
|||
headers: {link: 'url; rel="current"'},
|
||||
response: opportunities
|
||||
})
|
||||
}
|
||||
beforeAll(() => {
|
||||
jest.setTimeout(15000)
|
||||
})
|
||||
afterAll(() => {
|
||||
jest.setTimeout(5000)
|
||||
})
|
||||
beforeEach(() => {
|
||||
moxios.install()
|
||||
createPlannerMocks()
|
||||
fetchMock.get('/api/v1/courses/1/activity_stream/summary', JSON.stringify(cardSummary))
|
||||
fetchMock.get(/\/api\/v1\/announcements.*/, announcements)
|
||||
fetchMock.get(/\/api\/v1\/users\/self\/courses.*/, JSON.stringify(gradeCourses))
|
||||
|
@ -283,23 +280,20 @@ beforeEach(() => {
|
|||
/\/api\/v1\/calendar_events\/save_selected_contexts.*/,
|
||||
JSON.stringify({status: 'ok'})
|
||||
)
|
||||
|
||||
fetchMock.put(/\/api\/v1\/users\/\d+\/colors\.*/, {status: 200, body: []})
|
||||
global.ENV = defaultEnv
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
moxios.uninstall()
|
||||
fetchMock.restore()
|
||||
global.ENV = {}
|
||||
resetDashboardCards()
|
||||
resetPlanner()
|
||||
resetCardCache()
|
||||
sessionStorage.clear()
|
||||
window.location.hash = ''
|
||||
destroyContainer()
|
||||
document.cookie = `${observedUserCookieName}=`
|
||||
resetCardCache()
|
||||
})
|
||||
|
||||
describe('K-5 Dashboard', () => {
|
||||
it('displays a welcome message to the logged-in user', () => {
|
||||
const {getByText} = render(<K5Dashboard {...defaultProps} />)
|
||||
|
@ -310,21 +304,18 @@ describe('K-5 Dashboard', () => {
|
|||
const {getByRole} = render(<K5Dashboard {...defaultProps} canDisableElementaryDashboard />)
|
||||
const optionsButton = getByRole('button', {name: 'Dashboard Options'})
|
||||
act(() => optionsButton.click())
|
||||
|
||||
// There should be an Homeroom View menu option already checked
|
||||
const elementaryViewOption = screen.getByRole('menuitemradio', {
|
||||
name: 'Homeroom View',
|
||||
checked: true
|
||||
})
|
||||
expect(elementaryViewOption).toBeInTheDocument()
|
||||
|
||||
// There should be a Classic View menu option initially un-checked
|
||||
const classicViewOption = screen.getByRole('menuitemradio', {
|
||||
name: 'Classic View',
|
||||
checked: false
|
||||
})
|
||||
expect(classicViewOption).toBeInTheDocument()
|
||||
|
||||
// Clicking the Classic View option should update the user's dashboard setting
|
||||
act(() => classicViewOption.click())
|
||||
await waitFor(() => {
|
||||
|
@ -336,7 +327,6 @@ describe('K-5 Dashboard', () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tabs', () => {
|
||||
it('show Homeroom, Schedule, Grades, and Resources options', async () => {
|
||||
const {getByText} = render(<K5Dashboard {...defaultProps} />)
|
||||
|
@ -351,7 +341,6 @@ describe('K-5 Dashboard', () => {
|
|||
const {findByRole} = render(<K5Dashboard {...defaultProps} />)
|
||||
expect(await findByRole('tab', {name: 'Homeroom', selected: true})).toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('store current tab ID to URL', () => {
|
||||
afterEach(() => {
|
||||
window.location.hash = ''
|
||||
|
@ -371,18 +360,15 @@ describe('K-5 Dashboard', () => {
|
|||
|
||||
it('and update the current tab as tabs are changed', async () => {
|
||||
const {findByRole, getByRole, queryByRole} = render(<K5Dashboard {...defaultProps} />)
|
||||
|
||||
const gradesTab = await findByRole('tab', {name: 'Grades'})
|
||||
act(() => gradesTab.click())
|
||||
expect(await findByRole('tab', {name: 'Grades', selected: true})).toBeInTheDocument()
|
||||
|
||||
act(() => getByRole('tab', {name: 'Resources'}).click())
|
||||
expect(await findByRole('tab', {name: 'Resources', selected: true})).toBeInTheDocument()
|
||||
expect(queryByRole('tab', {name: 'Grades', selected: true})).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Homeroom Section', () => {
|
||||
it('displays "My Subjects" heading', async () => {
|
||||
const {findByText} = render(<K5Dashboard {...defaultProps} />)
|
||||
|
@ -411,7 +397,7 @@ describe('K-5 Dashboard', () => {
|
|||
expect(getByText('Your homeroom is currently unpublished.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows a due today link pointing to the schedule tab of the course', async () => {
|
||||
it('shows due today and missing items links pointing to the schedule tab of the course', async () => {
|
||||
const {findByRole} = render(<K5Dashboard {...defaultProps} plannerEnabled />)
|
||||
const dueTodayLink = await findByRole('link', {
|
||||
name: 'View 1 items due today for course Economics 101',
|
||||
|
@ -419,16 +405,13 @@ describe('K-5 Dashboard', () => {
|
|||
})
|
||||
expect(dueTodayLink).toBeInTheDocument()
|
||||
expect(dueTodayLink.getAttribute('href')).toMatch('/courses/1?focusTarget=today#schedule')
|
||||
})
|
||||
|
||||
it('shows a missing items link pointing to the schedule tab of the course', async () => {
|
||||
const {findByRole} = render(<K5Dashboard {...defaultProps} plannerEnabled />)
|
||||
const dueTodayLink = await findByRole('link', {
|
||||
const misingItemsLink = await findByRole('link', {
|
||||
name: 'View 2 missing items for course Economics 101',
|
||||
timeout: 5000
|
||||
})
|
||||
expect(dueTodayLink).toBeInTheDocument()
|
||||
expect(dueTodayLink.getAttribute('href')).toMatch(
|
||||
expect(misingItemsLink).toBeInTheDocument()
|
||||
expect(misingItemsLink.getAttribute('href')).toMatch(
|
||||
'/courses/1?focusTarget=missing-items#schedule'
|
||||
)
|
||||
})
|
||||
|
@ -458,10 +441,8 @@ describe('K-5 Dashboard', () => {
|
|||
expect(getByText("You don't have any active courses yet.")).toBeInTheDocument()
|
||||
)
|
||||
expect(getByTestId('empty-dash-panda')).toBeInTheDocument()
|
||||
|
||||
const scheduleTab = getByRole('tab', {name: 'Schedule'})
|
||||
act(() => scheduleTab.click())
|
||||
|
||||
expect(getByText("You don't have any active courses yet.")).toBeInTheDocument()
|
||||
expect(getByTestId('empty-dash-panda')).toBeInTheDocument()
|
||||
})
|
||||
|
@ -470,7 +451,6 @@ describe('K-5 Dashboard', () => {
|
|||
sessionStorage.setItem('dashcards_for_user_1', JSON.stringify(MOCK_CARDS))
|
||||
moxios.withMock(() => {
|
||||
render(<K5Dashboard {...defaultProps} />)
|
||||
|
||||
// Don't respond immediately, let the cards from sessionStorage return first
|
||||
moxios.wait(() =>
|
||||
moxios.requests
|
||||
|
@ -492,7 +472,6 @@ describe('K-5 Dashboard', () => {
|
|||
sessionStorage.setItem('dashcards_for_user_1', JSON.stringify([]))
|
||||
moxios.withMock(() => {
|
||||
render(<K5Dashboard {...defaultProps} />)
|
||||
|
||||
moxios.wait(() =>
|
||||
moxios.requests
|
||||
.mostRecent()
|
||||
|
@ -511,7 +490,6 @@ describe('K-5 Dashboard', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Schedule Section', () => {
|
||||
it('displays the planner with a planned item', async () => {
|
||||
const {findByText} = render(
|
||||
|
@ -523,19 +501,16 @@ describe('K-5 Dashboard', () => {
|
|||
// expect(await findByText("Looks like there isn't anything here")).toBeInTheDocument()
|
||||
// expect(await findByText('Nothing More To Do')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Skipping for flakiness. See https://instructure.atlassian.net/browse/LS-2243.
|
||||
it.skip('displays a list of missing assignments if there are any', async () => {
|
||||
const {findByRole, getByRole, getByText} = render(
|
||||
<K5Dashboard {...defaultProps} defaultTab="tab-schedule" plannerEnabled />
|
||||
)
|
||||
|
||||
const missingAssignments = await findByRole('button', {
|
||||
name: 'Show 2 missing items',
|
||||
timeout: 5000
|
||||
})
|
||||
expect(missingAssignments).toBeInTheDocument()
|
||||
|
||||
act(() => missingAssignments.click())
|
||||
expect(getByRole('button', {name: 'Hide 2 missing items'})).toBeInTheDocument()
|
||||
expect(getByText('Assignment 1')).toBeInTheDocument()
|
||||
|
@ -548,7 +523,6 @@ describe('K-5 Dashboard', () => {
|
|||
)
|
||||
const planner = await findByTestId('PlannerApp', {timeout: 4000}) // give it some more time
|
||||
expect(planner).toBeInTheDocument()
|
||||
|
||||
const header = await findByTestId('WeeklyPlannerHeader')
|
||||
expect(header).toBeInTheDocument()
|
||||
})
|
||||
|
@ -557,13 +531,10 @@ describe('K-5 Dashboard', () => {
|
|||
const {findByRole} = render(
|
||||
<K5Dashboard {...defaultProps} defaultTab="tab-schedule" plannerEnabled />
|
||||
)
|
||||
|
||||
const jumpToNavButton = await findByRole('button', {name: 'Jump to navigation toolbar'})
|
||||
expect(jumpToNavButton).not.toBeVisible()
|
||||
|
||||
act(() => jumpToNavButton.focus())
|
||||
expect(jumpToNavButton).toBeVisible()
|
||||
|
||||
act(() => jumpToNavButton.click())
|
||||
expect(document.activeElement.id).toBe('weekly-header-active-button')
|
||||
expect(jumpToNavButton).not.toBeVisible()
|
||||
|
@ -585,7 +556,6 @@ describe('K-5 Dashboard', () => {
|
|||
const {findByTestId, getByText} = render(
|
||||
<K5Dashboard {...defaultProps} defaultTab="tab-schedule" plannerEnabled={false} />
|
||||
)
|
||||
|
||||
expect(await findByTestId('kinder-panda')).toBeInTheDocument()
|
||||
expect(getByText('Schedule Preview')).toBeInTheDocument()
|
||||
expect(
|
||||
|
@ -598,12 +568,14 @@ describe('K-5 Dashboard', () => {
|
|||
})
|
||||
|
||||
it('preloads surrounding weeks only once schedule tab is visible', async done => {
|
||||
const {findByText, getByRole} = render(<K5Dashboard {...defaultProps} plannerEnabled />)
|
||||
const {findByText, getByRole} = render(
|
||||
<K5Dashboard {...defaultProps} currentUserRoles={['user', 'student']} plannerEnabled />
|
||||
)
|
||||
expect(await findByText('Assignment 15')).toBeInTheDocument()
|
||||
expect(moxios.requests.count()).toBe(5)
|
||||
expect(moxios.requests.count()).toBe(6)
|
||||
act(() => getByRole('tab', {name: 'Schedule'}).click())
|
||||
moxios.wait(() => {
|
||||
expect(moxios.requests.count()).toBe(7) // 2 more requests for prev and next week preloads
|
||||
expect(moxios.requests.count()).toBe(8) // 2 more requests for prev and next week preloads
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
@ -613,7 +585,6 @@ describe('K-5 Dashboard', () => {
|
|||
status: 200,
|
||||
response: MOCK_CARDS
|
||||
})
|
||||
|
||||
const observerPlannerItem = cloneDeep(MOCK_PLANNER_ITEM)
|
||||
observerPlannerItem[0].plannable.title = 'Assignment for Observee'
|
||||
const observedUsersList = [
|
||||
|
@ -626,7 +597,6 @@ describe('K-5 Dashboard', () => {
|
|||
name: 'Student 2'
|
||||
}
|
||||
]
|
||||
|
||||
const {findByText, findByRole, getByRole, getByText} = render(
|
||||
<K5Dashboard
|
||||
{...defaultProps}
|
||||
|
@ -645,7 +615,6 @@ describe('K-5 Dashboard', () => {
|
|||
timeout: 5000
|
||||
})
|
||||
).toBeInTheDocument()
|
||||
|
||||
moxios.stubs.reset()
|
||||
moxios.stubRequest('/api/v1/dashboard/dashboard_cards?observed_user=2', {
|
||||
status: 200,
|
||||
|
@ -661,7 +630,6 @@ describe('K-5 Dashboard', () => {
|
|||
headers: {link: 'url; rel="current"'},
|
||||
response: [opportunities[0]]
|
||||
})
|
||||
|
||||
const observerSelect = getByRole('combobox', {name: 'Select a student to view'})
|
||||
act(() => observerSelect.click())
|
||||
act(() => getByText('Student 2').click())
|
||||
|
@ -679,7 +647,6 @@ describe('K-5 Dashboard', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Grades Section', () => {
|
||||
it('does not show the grades tab to students if hideGradesTabForStudents is set', async () => {
|
||||
const {queryByRole} = render(
|
||||
|
@ -697,7 +664,6 @@ describe('K-5 Dashboard', () => {
|
|||
expect(queryByText('Homeroom Class')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Resources Section', () => {
|
||||
it('displays syllabus content for homeroom under important info section', async () => {
|
||||
const {getByText, findByText} = render(
|
||||
|
@ -724,15 +690,12 @@ describe('K-5 Dashboard', () => {
|
|||
expect(wrapper.getByText('Teaching Assistant')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Todos Section', () => {
|
||||
it('displays todo tab to teachers', async () => {
|
||||
const {findByRole} = render(<K5Dashboard {...defaultProps} currentUserRoles={['teacher']} />)
|
||||
const todoTab = await findByRole('tab', {name: 'To Do'})
|
||||
expect(todoTab).toBeInTheDocument()
|
||||
|
||||
act(() => todoTab.click())
|
||||
|
||||
expect(await findByRole('link', {name: 'Grade Plant a plant'})).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
@ -744,7 +707,6 @@ describe('K-5 Dashboard', () => {
|
|||
expect(queryByRole('tab', {name: 'To Do'})).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Important Dates', () => {
|
||||
it('renders a sidebar with important dates and no tray buttons on large screens', async () => {
|
||||
const {getByText, queryByText} = render(<K5Dashboard {...defaultProps} />)
|
||||
|
@ -764,7 +726,6 @@ describe('K-5 Dashboard', () => {
|
|||
})
|
||||
// Only return assignments associated with course_1 on next call
|
||||
fetchMock.get(ASSIGNMENTS_URL, MOCK_ASSIGNMENTS.slice(0, 1), {overwriteRoutes: true})
|
||||
|
||||
const {getByRole, getByText, queryByText} = render(
|
||||
<K5Dashboard
|
||||
{...defaultProps}
|
||||
|
@ -779,17 +740,14 @@ describe('K-5 Dashboard', () => {
|
|||
})
|
||||
expect(fetchMock.lastUrl(ASSIGNMENTS_URL)).toMatch('context_codes%5B%5D=course_1')
|
||||
expect(fetchMock.lastUrl(ASSIGNMENTS_URL)).not.toMatch('context_codes%5B%5D=course_3')
|
||||
|
||||
// Only return assignments associated with course_3 on next call
|
||||
fetchMock.get(ASSIGNMENTS_URL, MOCK_ASSIGNMENTS.slice(1, 3), {overwriteRoutes: true})
|
||||
|
||||
act(() =>
|
||||
getByRole('button', {name: 'Select calendars to retrieve important dates from'}).click()
|
||||
)
|
||||
act(() => getByRole('checkbox', {name: 'Economics 101', checked: true}).click())
|
||||
act(() => getByRole('checkbox', {name: 'The Maths', checked: false}).click())
|
||||
act(() => getByRole('button', {name: 'Submit'}).click())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('Algebra 2')).not.toBeInTheDocument()
|
||||
expect(getByText('History Discussion')).toBeInTheDocument()
|
||||
|
@ -804,12 +762,10 @@ describe('K-5 Dashboard', () => {
|
|||
await waitFor(() => expect(getByText('History Discussion')).toBeInTheDocument())
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parent Support', () => {
|
||||
beforeEach(() => {
|
||||
document.cookie = `${observedUserCookieName}=4;path=/`
|
||||
})
|
||||
|
||||
const opportunities2 = [
|
||||
{
|
||||
id: '3',
|
||||
|
@ -836,27 +792,34 @@ describe('K-5 Dashboard', () => {
|
|||
expect(select.value).toBe('Student 4')
|
||||
})
|
||||
|
||||
it('prefetches dashboard cards with the correct url param', done => {
|
||||
moxios.withMock(async () => {
|
||||
render(
|
||||
<K5Dashboard
|
||||
{...defaultProps}
|
||||
currentUserRoles={['user', 'observer', 'teacher']}
|
||||
canAddObservee
|
||||
parentSupportEnabled
|
||||
/>
|
||||
)
|
||||
moxios.wait(() => {
|
||||
const preFetchedRequest = moxios.requests.mostRecent()
|
||||
expect(preFetchedRequest.url).toBe('/api/v1/dashboard/dashboard_cards?observed_user=4')
|
||||
expect(moxios.requests.count()).toBe(1)
|
||||
done()
|
||||
})
|
||||
it('prefetches dashboard cards with the correct url param', async done => {
|
||||
moxios.stubRequest('/api/v1/dashboard/dashboard_cards?observed_user=4', {
|
||||
status: 200,
|
||||
response: MOCK_CARDS
|
||||
})
|
||||
|
||||
render(
|
||||
<K5Dashboard
|
||||
{...defaultProps}
|
||||
currentUserRoles={['user', 'observer', 'teacher']}
|
||||
canAddObservee
|
||||
parentSupportEnabled
|
||||
/>
|
||||
)
|
||||
// let the dashboard execute all its queries and render
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(moxios.requests.mostRecent()).not.toBeNull()
|
||||
},
|
||||
{timeout: 5000}
|
||||
)
|
||||
const preFetchedRequest = moxios.requests.mostRecent()
|
||||
expect(preFetchedRequest.url).toBe('/api/v1/dashboard/dashboard_cards?observed_user=4')
|
||||
expect(moxios.requests.count()).toBe(1)
|
||||
done()
|
||||
})
|
||||
|
||||
it('does not make a request if the user has been already requested', async () => {
|
||||
moxios.stubs.reset()
|
||||
moxios.stubRequest('/api/v1/dashboard/dashboard_cards?observed_user=4', {
|
||||
status: 200,
|
||||
response: MOCK_CARDS
|
||||
|
@ -865,7 +828,6 @@ describe('K-5 Dashboard', () => {
|
|||
status: 200,
|
||||
response: MOCK_CARDS_2
|
||||
})
|
||||
|
||||
const {findByText, getByRole, getByText, queryByText} = render(
|
||||
<K5Dashboard
|
||||
{...defaultProps}
|
||||
|
@ -874,7 +836,6 @@ describe('K-5 Dashboard', () => {
|
|||
canAddObservee
|
||||
/>
|
||||
)
|
||||
|
||||
expect(await findByText('Economics 101')).toBeInTheDocument()
|
||||
expect(queryByText('Economics 203')).not.toBeInTheDocument()
|
||||
const select = getByRole('combobox', {name: 'Select a student to view'})
|
||||
|
@ -882,7 +843,6 @@ describe('K-5 Dashboard', () => {
|
|||
expect(moxios.requests.mostRecent().url).toBe(
|
||||
'/api/v1/dashboard/dashboard_cards?observed_user=4'
|
||||
)
|
||||
|
||||
act(() => select.click())
|
||||
act(() => getByText('Student 2').click())
|
||||
expect(await findByText('Economics 203')).toBeInTheDocument()
|
||||
|
@ -890,7 +850,6 @@ describe('K-5 Dashboard', () => {
|
|||
expect(moxios.requests.mostRecent().url).toBe(
|
||||
'/api/v1/dashboard/dashboard_cards?observed_user=2'
|
||||
)
|
||||
|
||||
act(() => select.click())
|
||||
act(() => getByText('Student 4').click())
|
||||
expect(await findByText('Economics 101')).toBeInTheDocument()
|
||||
|
@ -904,6 +863,28 @@ describe('K-5 Dashboard', () => {
|
|||
})
|
||||
|
||||
it('shows the observee missing items on dashboard cards', async () => {
|
||||
moxios.stubs.reset()
|
||||
moxios.requests.reset()
|
||||
moxios.stubRequest('/api/v1/dashboard/dashboard_cards?observed_user=4', {
|
||||
status: 200,
|
||||
response: MOCK_CARDS
|
||||
})
|
||||
moxios.stubRequest('/api/v1/dashboard/dashboard_cards?observed_user=2', {
|
||||
status: 200,
|
||||
response: MOCK_CARDS_2
|
||||
})
|
||||
moxios.stubRequest(/\/api\/v1\/users\/self\/missing_submissions\?.*observed_user_id=4.*/, {
|
||||
status: 200,
|
||||
headers: {link: 'url; rel="current"'},
|
||||
response: opportunities
|
||||
})
|
||||
moxios.stubRequest(/\/api\/v1\/users\/self\/missing_submissions\?.*observed_user_id=2.*/, {
|
||||
status: 200,
|
||||
headers: {link: 'url; rel="current"'},
|
||||
response: opportunities2
|
||||
})
|
||||
createPlannerMocks()
|
||||
|
||||
const {getByText, findByRole, getByRole} = render(
|
||||
<K5Dashboard
|
||||
{...defaultProps}
|
||||
|
@ -913,43 +894,28 @@ describe('K-5 Dashboard', () => {
|
|||
plannerEnabled
|
||||
/>
|
||||
)
|
||||
moxios.stubs.reset()
|
||||
moxios.stubRequest('/api/v1/dashboard/dashboard_cards?observed_user=4', {
|
||||
status: 200,
|
||||
response: MOCK_CARDS
|
||||
})
|
||||
|
||||
moxios.stubRequest('/api/v1/dashboard/dashboard_cards?observed_user=2', {
|
||||
status: 200,
|
||||
response: MOCK_CARDS_2
|
||||
})
|
||||
|
||||
moxios.stubRequest(/\/api\/v1\/users\/self\/missing_submissions\?.*observed_user_id=4.*/, {
|
||||
status: 200,
|
||||
headers: {link: 'url; rel="current"'},
|
||||
response: opportunities
|
||||
})
|
||||
|
||||
moxios.stubRequest(/\/api\/v1\/users\/self\/missing_submissions\?.*observed_user_id=2.*/, {
|
||||
status: 200,
|
||||
headers: {link: 'url; rel="current"'},
|
||||
response: opportunities2
|
||||
})
|
||||
|
||||
// let the dashboard execute all its queries and render
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(document.querySelectorAll('.ic-DashboardCard').length).toBeGreaterThan(0)
|
||||
},
|
||||
{timeout: 5000}
|
||||
)
|
||||
expect(
|
||||
await findByRole('link', {
|
||||
name: 'View 2 missing items for course Economics 101',
|
||||
timeout: 5000
|
||||
timeout: 5000,
|
||||
exact: false
|
||||
})
|
||||
).toBeInTheDocument()
|
||||
|
||||
const observerSelect = getByRole('combobox', {name: 'Select a student to view'})
|
||||
act(() => observerSelect.click())
|
||||
act(() => getByText('Student 2').click())
|
||||
expect(
|
||||
await findByRole('link', {
|
||||
name: 'View 1 missing items for course Economics 203',
|
||||
timeout: 5000
|
||||
timeout: 5000,
|
||||
exact: false
|
||||
})
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable promise/no-callback-in-promise */
|
||||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
|
@ -17,23 +18,27 @@
|
|||
*/
|
||||
|
||||
import moxios from 'moxios'
|
||||
import {showFlashAlert} from '@canvas/alerts/react/FlashAlert'
|
||||
import {CardDashboardLoader, resetCardCache} from '../loadCardDashboard'
|
||||
|
||||
import loadCardDashboard, {resetDashboardCards} from '../loadCardDashboard'
|
||||
jest.mock('@canvas/alerts/react/FlashAlert')
|
||||
|
||||
describe('loadCardDashboard', () => {
|
||||
let cardDashboardLoader
|
||||
beforeEach(() => {
|
||||
moxios.install()
|
||||
cardDashboardLoader = new CardDashboardLoader()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
moxios.uninstall()
|
||||
resetDashboardCards()
|
||||
resetCardCache()
|
||||
})
|
||||
|
||||
describe('with observer', () => {
|
||||
it('loads student cards asynchronously and calls back renderFn', done => {
|
||||
const callback = jest.fn()
|
||||
loadCardDashboard(callback, 2)
|
||||
cardDashboardLoader.loadCardDashboard(callback, 2)
|
||||
moxios.wait(() => {
|
||||
expect(callback).not.toHaveBeenCalled()
|
||||
moxios.requests
|
||||
|
@ -46,12 +51,15 @@ describe('loadCardDashboard', () => {
|
|||
expect(callback).toHaveBeenCalledWith(['card'], true)
|
||||
done()
|
||||
})
|
||||
.catch(e => {
|
||||
throw e
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('saves student cards and calls back renderFn immediately if requested again', done => {
|
||||
const callback = jest.fn()
|
||||
loadCardDashboard(callback, 5)
|
||||
cardDashboardLoader.loadCardDashboard(callback, 5)
|
||||
moxios.wait(() => {
|
||||
moxios.requests
|
||||
.mostRecent()
|
||||
|
@ -60,14 +68,34 @@ describe('loadCardDashboard', () => {
|
|||
response: ['card']
|
||||
})
|
||||
.then(() => {
|
||||
resetDashboardCards()
|
||||
loadCardDashboard(callback, 5)
|
||||
resetCardCache()
|
||||
cardDashboardLoader.loadCardDashboard(callback, 5)
|
||||
moxios.wait(() => {
|
||||
expect(callback).toHaveBeenCalledWith(['card'], true)
|
||||
expect(moxios.requests.count()).toBe(1)
|
||||
done()
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
throw e
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fails gracefully', done => {
|
||||
const callback = jest.fn()
|
||||
cardDashboardLoader.loadCardDashboard(callback, 2)
|
||||
moxios.wait(() => {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
moxios.requests
|
||||
.mostRecent()
|
||||
.respondWith({
|
||||
status: 500
|
||||
})
|
||||
.then(() => {
|
||||
expect(showFlashAlert).toHaveBeenCalledTimes(1)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -21,11 +21,10 @@ import ReactDOM from 'react-dom'
|
|||
import getDroppableDashboardCardBox from './react/getDroppableDashboardCardBox'
|
||||
import DashboardCard from './react/DashboardCard'
|
||||
import axios from '@canvas/axios'
|
||||
import {asJson, getPrefetchedXHR} from '@instructure/js-utils'
|
||||
import {showFlashAlert} from '@canvas/alerts/react/FlashAlert'
|
||||
import {asJson, checkStatus, getPrefetchedXHR} from '@instructure/js-utils'
|
||||
import buildURL from 'axios/lib/helpers/buildURL'
|
||||
|
||||
let promiseToGetDashboardCards
|
||||
let observedUsersDashboardCards = {}
|
||||
import I18n from 'i18n!load_card_dashboard'
|
||||
|
||||
export function createDashboardCards(dashboardCards, cardComponent = DashboardCard, extraProps) {
|
||||
const Box = getDroppableDashboardCardBox()
|
||||
|
@ -43,75 +42,110 @@ export function createDashboardCards(dashboardCards, cardComponent = DashboardCa
|
|||
/>
|
||||
)
|
||||
}
|
||||
export class CardDashboardLoader {
|
||||
static observedUsersDashboardCards = {}
|
||||
|
||||
function renderIntoDOM(dashboardCards) {
|
||||
const dashboardContainer = document.getElementById('DashboardCard_Container')
|
||||
ReactDOM.render(createDashboardCards(dashboardCards), dashboardContainer)
|
||||
}
|
||||
|
||||
export default function loadCardDashboard(renderFn = renderIntoDOM, observedUserId) {
|
||||
if (observedUserId && observedUsersDashboardCards[observedUserId]) {
|
||||
renderFn(observedUsersDashboardCards[observedUserId], true)
|
||||
} else if (promiseToGetDashboardCards) {
|
||||
promiseToGetDashboardCards.then(cards => renderFn(cards, true))
|
||||
} else {
|
||||
let xhrHasReturned = false
|
||||
let sessionStorageTimeout
|
||||
const sessionStorageKey = `dashcards_for_user_${ENV && ENV.current_user_id}`
|
||||
const urlPrefix = '/api/v1/dashboard/dashboard_cards'
|
||||
const url = buildURL(urlPrefix, {observed_user: observedUserId})
|
||||
promiseToGetDashboardCards =
|
||||
asJson(getPrefetchedXHR(url)) || axios.get(url).then(({data}) => data)
|
||||
promiseToGetDashboardCards.then(() => (xhrHasReturned = true))
|
||||
|
||||
// Because we use prefetch_xhr to prefetch this xhr request from our rails erb, there is a
|
||||
// chance that the XHR to get the latest dashcard data has already come back before we get
|
||||
// to this point. So if the XHR is ready, there's no need to render twice, just render
|
||||
// once with the newest data.
|
||||
// Otherwise, render with the cached stuff from session storage now, then render again
|
||||
// when the xhr comes back with the latest data.
|
||||
const promiseToGetCardsFromSessionStorage = new Promise(resolve => {
|
||||
sessionStorageTimeout = setTimeout(() => {
|
||||
const cachedCards = sessionStorage.getItem(sessionStorageKey)
|
||||
if (cachedCards) resolve(JSON.parse(cachedCards))
|
||||
}, 1)
|
||||
})
|
||||
Promise.race([promiseToGetDashboardCards, promiseToGetCardsFromSessionStorage]).then(
|
||||
dashboardCards => {
|
||||
clearTimeout(sessionStorageTimeout)
|
||||
// calling the renderFn with `false` indicates to consumers that we're still waiting
|
||||
// on the follow-up xhr request to complete.
|
||||
renderFn(dashboardCards, xhrHasReturned)
|
||||
// calling it with `true` indicates that all outstanding card promises have settled.
|
||||
if (!xhrHasReturned) return promiseToGetDashboardCards.then(cards => renderFn(cards, true))
|
||||
}
|
||||
)
|
||||
|
||||
// Cache the fetched dashcards in sessionStorage so we can render instantly next
|
||||
// time they come to their dashboard (while still fetching the most current data)
|
||||
// Also save the observed user's cards if observing so observer can switch between students
|
||||
// without any delay
|
||||
promiseToGetDashboardCards.then(dashboardCards => {
|
||||
try {
|
||||
sessionStorage.setItem(sessionStorageKey, JSON.stringify(dashboardCards))
|
||||
} catch (_e) {
|
||||
// If saving the cards to session storage fails, we can just ignore the exception; the cards
|
||||
// will still be fetched and displayed on the next load. Telling the user probably doesn't
|
||||
// make sense since it doesn't change the way the app works, nor does it make sense to log
|
||||
// the error since it could happen in normal circumstances (like using Safari in private mode).
|
||||
}
|
||||
if (observedUserId) {
|
||||
observedUsersDashboardCards[observedUserId] = dashboardCards
|
||||
}
|
||||
})
|
||||
constructor() {
|
||||
this.promiseToGetDashboardCards = undefined
|
||||
this.errorShown = false
|
||||
}
|
||||
}
|
||||
|
||||
export function resetDashboardCards() {
|
||||
promiseToGetDashboardCards = undefined
|
||||
renderIntoDOM = dashboardCards => {
|
||||
const dashboardContainer = document.getElementById('DashboardCard_Container')
|
||||
ReactDOM.render(createDashboardCards(dashboardCards), dashboardContainer)
|
||||
}
|
||||
|
||||
loadCardDashboard(renderFn = this.renderIntoDOM, observedUserId) {
|
||||
if (observedUserId && CardDashboardLoader.observedUsersDashboardCards[observedUserId]) {
|
||||
renderFn(CardDashboardLoader.observedUsersDashboardCards[observedUserId], true)
|
||||
} else if (this.promiseToGetDashboardCards) {
|
||||
this.promiseToGetDashboardCards
|
||||
.then(cards => {
|
||||
renderFn(cards, true)
|
||||
})
|
||||
.catch(e => {
|
||||
this.showError(e)
|
||||
})
|
||||
} else {
|
||||
let xhrHasReturned = false
|
||||
let sessionStorageTimeout
|
||||
const sessionStorageKey = `dashcards_for_user_${ENV && ENV.current_user_id}`
|
||||
const urlPrefix = '/api/v1/dashboard/dashboard_cards'
|
||||
const url = buildURL(urlPrefix, {observed_user: observedUserId})
|
||||
this.promiseToGetDashboardCards =
|
||||
asJson(getPrefetchedXHR(url)) ||
|
||||
axios
|
||||
.get(url)
|
||||
.then(checkStatus)
|
||||
.then(({data}) => data)
|
||||
.catch(e => {
|
||||
this.showError(e)
|
||||
})
|
||||
this.promiseToGetDashboardCards
|
||||
.then(() => (xhrHasReturned = true))
|
||||
.catch(e => {
|
||||
this.showError(e)
|
||||
})
|
||||
|
||||
// Because we use prefetch_xhr to prefetch this xhr request from our rails erb, there is a
|
||||
// chance that the XHR to get the latest dashcard data has already come back before we get
|
||||
// to this point. So if the XHR is ready, there's no need to render twice, just render
|
||||
// once with the newest data.
|
||||
// Otherwise, render with the cached stuff from session storage now, then render again
|
||||
// when the xhr comes back with the latest data.
|
||||
const promiseToGetCardsFromSessionStorage = new Promise(resolve => {
|
||||
sessionStorageTimeout = setTimeout(() => {
|
||||
const cachedCards = sessionStorage.getItem(sessionStorageKey)
|
||||
if (cachedCards) resolve(JSON.parse(cachedCards))
|
||||
}, 1)
|
||||
})
|
||||
Promise.race([this.promiseToGetDashboardCards, promiseToGetCardsFromSessionStorage])
|
||||
.then(dashboardCards => {
|
||||
clearTimeout(sessionStorageTimeout)
|
||||
// calling the renderFn with `false` indicates to consumers that we're still waiting
|
||||
// on the follow-up xhr request to complete.
|
||||
renderFn(dashboardCards, xhrHasReturned)
|
||||
// calling it with `true` indicates that all outstanding card promises have settled.
|
||||
if (!xhrHasReturned)
|
||||
return this.promiseToGetDashboardCards.then(cards => renderFn(cards, true))
|
||||
})
|
||||
.catch(e => {
|
||||
this.showError(e)
|
||||
})
|
||||
|
||||
// Cache the fetched dashcards in sessionStorage so we can render instantly next
|
||||
// time they come to their dashboard (while still fetching the most current data)
|
||||
// Also save the observed user's cards if observing so observer can switch between students
|
||||
// without any delay
|
||||
this.promiseToGetDashboardCards
|
||||
.then(dashboardCards => {
|
||||
try {
|
||||
sessionStorage.setItem(sessionStorageKey, JSON.stringify(dashboardCards))
|
||||
} catch (_e) {
|
||||
// If saving the cards to session storage fails, we can just ignore the exception; the cards
|
||||
// will still be fetched and displayed on the next load. Telling the user probably doesn't
|
||||
// make sense since it doesn't change the way the app works, nor does it make sense to log
|
||||
// the error since it could happen in normal circumstances (like using Safari in private mode).
|
||||
}
|
||||
if (observedUserId) {
|
||||
CardDashboardLoader.observedUsersDashboardCards[observedUserId] = dashboardCards
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this.showError(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
showError(e) {
|
||||
if (!this.errorShown) {
|
||||
this.errorShown = true
|
||||
showFlashAlert({message: I18n.t('Failed loading course cards'), err: e, type: 'error'})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clears the cache for use in test suites
|
||||
export function resetCardCache() {
|
||||
observedUsersDashboardCards = {}
|
||||
CardDashboardLoader.observedUsersDashboardCards = {}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,9 @@ describe('CourseActivitySummaryStore', () => {
|
|||
const spy = jest.spyOn(window, 'fetch').mockImplementation(() =>
|
||||
Promise.resolve().then(() => ({
|
||||
status: 200,
|
||||
json: () => Promise.resolve().then(() => stream)
|
||||
clone: () => ({
|
||||
json: () => Promise.resolve().then(() => stream)
|
||||
})
|
||||
}))
|
||||
)
|
||||
CourseActivitySummaryStore._fetchForCourse(1)
|
||||
|
|
|
@ -39,8 +39,7 @@ const SchedulePage = ({
|
|||
userHasEnrollments,
|
||||
visible,
|
||||
singleCourse,
|
||||
observedUserId,
|
||||
contextCodes
|
||||
observedUserId
|
||||
}) => {
|
||||
const [isPlannerCreated, setPlannerCreated] = useState(false)
|
||||
const [hasPreloadedItems, setHasPreloadedItems] = useState(false)
|
||||
|
@ -67,16 +66,9 @@ const SchedulePage = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (plannerReady) {
|
||||
reloadPlannerForObserver(observedUserId, contextCodes)
|
||||
reloadPlannerForObserver(observedUserId)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
plannerReady,
|
||||
observedUserId,
|
||||
// contextCodes is included in the dependency array in its stringified form
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
JSON.stringify(contextCodes)
|
||||
])
|
||||
}, [plannerReady, observedUserId])
|
||||
|
||||
let content = <></>
|
||||
if (plannerInitialized && isPlannerCreated) {
|
||||
|
@ -116,8 +108,7 @@ SchedulePage.propTypes = {
|
|||
userHasEnrollments: PropTypes.bool.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
singleCourse: PropTypes.bool.isRequired,
|
||||
observedUserId: PropTypes.string,
|
||||
contextCodes: PropTypes.arrayOf(PropTypes.string)
|
||||
observedUserId: PropTypes.string
|
||||
}
|
||||
|
||||
export default SchedulePage
|
||||
|
|
|
@ -52,10 +52,17 @@ export default function usePlanner({
|
|||
observedUserId,
|
||||
isObserver = false
|
||||
}) {
|
||||
const [plannerInitializing, setPlannerInitializing] = useState(false)
|
||||
const [plannerInitialized, setPlannerInitialized] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (plannerEnabled && !plannerInitialized && (!isObserver || !!observedUserId)) {
|
||||
if (
|
||||
plannerEnabled &&
|
||||
!plannerInitializing &&
|
||||
!plannerInitialized &&
|
||||
(!isObserver || !!observedUserId)
|
||||
) {
|
||||
setPlannerInitializing(true)
|
||||
initializePlanner({
|
||||
getActiveApp: () => (isPlannerActive() ? 'planner' : ''),
|
||||
flashError: message => showFlashAlert({message, type: 'error'}),
|
||||
|
@ -68,8 +75,12 @@ export default function usePlanner({
|
|||
singleCourse,
|
||||
observedUserId
|
||||
})
|
||||
.then(setPlannerInitialized)
|
||||
.catch(showFlashError(I18n.t('Failed to load the schedule tab')))
|
||||
.then(val => {
|
||||
setPlannerInitialized(val)
|
||||
})
|
||||
.catch(_ex => {
|
||||
showFlashError(I18n.t('Failed to load the schedule tab'))()
|
||||
})
|
||||
}
|
||||
// The rest of the dependencies don't change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
|
@ -297,7 +297,7 @@ export const transformAnnouncement = announcement => {
|
|||
if (!announcement) return undefined
|
||||
|
||||
let attachment
|
||||
if (announcement.attachments[0]) {
|
||||
if (announcement?.attachments[0]) {
|
||||
attachment = {
|
||||
display_name: announcement.attachments[0].display_name,
|
||||
url: announcement.attachments[0].url,
|
||||
|
@ -345,6 +345,7 @@ export const ignoreTodo = ignoreUrl =>
|
|||
})
|
||||
|
||||
export const groupImportantDates = (assignments, events, timeZone) => {
|
||||
if (!assignments) return []
|
||||
const groups = assignments.concat(events).reduce((acc, item) => {
|
||||
const parsedItem = {
|
||||
id: item.id,
|
||||
|
|
|
@ -15,17 +15,22 @@
|
|||
// You should have received a copy of the GNU Affero General Public License along
|
||||
// with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import $ from 'jquery'
|
||||
import _ from 'underscore'
|
||||
import rgb2hex from './rgb2hex'
|
||||
import {defaultFetchOptions} from '@instructure/js-utils'
|
||||
|
||||
export default {
|
||||
persistContextColors(colorsByContext, userId) {
|
||||
_.each(colorsByContext, (color, contextCode) => {
|
||||
const hexcode = color.match(/rgb/) ? rgb2hex(color) : color
|
||||
const hexcode = (color.match(/rgb/) ? rgb2hex(color) : color).replace(/^#/, '')
|
||||
|
||||
const url = `/api/v1/users/${userId}/colors/${contextCode}`
|
||||
$.ajax({url, type: 'PUT', data: {hexcode}})
|
||||
// I don't know why, but when the hexcode was in the body, it failed to
|
||||
// work from selenium
|
||||
const url = `/api/v1/users/${userId}/colors/${contextCode}?hexcode=${hexcode}`
|
||||
fetch(url, {
|
||||
method: 'PUT',
|
||||
...defaultFetchOptions
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue