Apply new filter present logic to filter tray
Test plan: - Regression test enhanced filters: - Create filters - Update filters - Delete filters - Known issue: Submissions conditions do not persist Closes EVAL-2364 flag=enhanced_gradebook_filters Change-Id: I2bef980aefdeca07167af4b16d729426aa78bfc1 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/289539 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Kai Bjorkman <kbjorkman@instructure.com> Product-Review: Syed Hussain <shussain@instructure.com> Reviewed-by: Spencer Olson <solson@instructure.com> Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com>
This commit is contained in:
parent
c58f903dc0
commit
0362552610
|
@ -41,7 +41,7 @@ import type {
|
|||
Course,
|
||||
CourseContent,
|
||||
EffectiveDueDateAssignmentUserMap,
|
||||
Filter,
|
||||
FilterCondition,
|
||||
FilteredContentInfo,
|
||||
FlashAlertType,
|
||||
GradebookOptions,
|
||||
|
@ -146,24 +146,23 @@ import {
|
|||
compareAssignmentDueDates,
|
||||
confirmViewUngradedAsZero,
|
||||
ensureAssignmentVisibility,
|
||||
findConditionValuesOfType,
|
||||
forEachSubmission,
|
||||
getAssignmentColumnId,
|
||||
getAssignmentGroupColumnId,
|
||||
getAssignmentGroupPointsPossible,
|
||||
getCourseFeaturesFromOptions,
|
||||
getCourseFromOptions,
|
||||
getCustomColumnId,
|
||||
getDefaultSettingKeyForColumnType,
|
||||
getGradeAsPercent,
|
||||
getStudentGradeForColumn,
|
||||
hiddenStudentIdsForAssignment,
|
||||
htmlDecode,
|
||||
isAdmin,
|
||||
onGridKeyDown,
|
||||
renderComponent,
|
||||
hiddenStudentIdsForAssignment,
|
||||
getDefaultSettingKeyForColumnType,
|
||||
sectionList,
|
||||
getCustomColumnId,
|
||||
getAssignmentColumnId,
|
||||
getAssignmentGroupColumnId,
|
||||
findAllAppliedFilterValuesOfType,
|
||||
getAllAppliedFilterValues
|
||||
sectionList
|
||||
} from './Gradebook.utils'
|
||||
import {
|
||||
compareAssignmentPointsPossible,
|
||||
|
@ -204,11 +203,11 @@ export function Portal({node, children}) {
|
|||
}
|
||||
|
||||
type GradebookProps = {
|
||||
appliedFilterConditions: FilterCondition[]
|
||||
applyScoreToUngradedModalNode: HTMLElement
|
||||
colors: StatusColors
|
||||
dispatch: RequestDispatch
|
||||
filterNavNode: HTMLElement
|
||||
filters: Filter[]
|
||||
flashAlerts: FlashAlertType[]
|
||||
flashMessageContainer: HTMLElement
|
||||
gradebookEnv: any
|
||||
|
@ -1140,9 +1139,9 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
|
|||
return this.getAssignmentGroupToShow() === assignment.assignment_group_id
|
||||
}
|
||||
|
||||
const assignmentGroupIds = findAllAppliedFilterValuesOfType(
|
||||
const assignmentGroupIds = findConditionValuesOfType(
|
||||
'assignment-group',
|
||||
this.props.filters
|
||||
this.props.appliedFilterConditions
|
||||
)
|
||||
return (
|
||||
assignmentGroupIds.length === 0 || assignmentGroupIds.includes(assignment.assignment_group_id)
|
||||
|
@ -1168,12 +1167,15 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
|
|||
)
|
||||
}
|
||||
|
||||
const moduleIds = findAllAppliedFilterValuesOfType('module', this.props.filters)
|
||||
const moduleIds = findConditionValuesOfType('module', this.props.appliedFilterConditions)
|
||||
return moduleIds.length === 0 || intersection(assignment.module_ids, moduleIds).length > 0
|
||||
}
|
||||
|
||||
filterAssignmentsBySubmissions = (assignment: Assignment) => {
|
||||
const submissionFilters = findAllAppliedFilterValuesOfType('submissions', this.props.filters)
|
||||
const submissionFilters = findConditionValuesOfType(
|
||||
'submissions',
|
||||
this.props.appliedFilterConditions
|
||||
)
|
||||
|
||||
if (submissionFilters.length === 0) {
|
||||
return true
|
||||
|
@ -1198,7 +1200,7 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
|
|||
}
|
||||
|
||||
filterAssignmentByStartDate = (assignment: Assignment) => {
|
||||
const date = findAllAppliedFilterValuesOfType('start-date', this.props.filters)[0]
|
||||
const date = findConditionValuesOfType('start-date', this.props.appliedFilterConditions)[0]
|
||||
if (!date) {
|
||||
return true
|
||||
}
|
||||
|
@ -1209,7 +1211,7 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
|
|||
}
|
||||
|
||||
filterAssignmentByEndDate = (assignment: Assignment) => {
|
||||
const date = findAllAppliedFilterValuesOfType('end-date', this.props.filters)[0]
|
||||
const date = findConditionValuesOfType('end-date', this.props.appliedFilterConditions)[0]
|
||||
if (!date) {
|
||||
return true
|
||||
}
|
||||
|
@ -1954,7 +1956,7 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
|
|||
if (matches) {
|
||||
if (
|
||||
assignmentGroupId === undefined ||
|
||||
this.assignments[parseInt(matches[1])]?.assignment_group_id === assignmentGroupId
|
||||
this.assignments[parseInt(matches[1], 10)]?.assignment_group_id === assignmentGroupId
|
||||
) {
|
||||
const assignmentId = matches[1]
|
||||
acc.push(assignmentId)
|
||||
|
@ -4747,11 +4749,11 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
|
|||
}
|
||||
|
||||
const didAppliedFilterValuesChange =
|
||||
getAllAppliedFilterValues(prevProps.filters).join(',') !==
|
||||
getAllAppliedFilterValues(this.props.filters).join(',')
|
||||
prevProps.appliedFilterConditions.map(c => c.value).join(',') !==
|
||||
this.props.appliedFilterConditions.map(c => c.value).join(',')
|
||||
if (didAppliedFilterValuesChange) {
|
||||
const prevSectionIds = findAllAppliedFilterValuesOfType('section', prevProps.filters)
|
||||
const sectionIds = findAllAppliedFilterValuesOfType('section', this.props.filters)
|
||||
const prevSectionIds = findConditionValuesOfType('section', prevProps.appliedFilterConditions)
|
||||
const sectionIds = findConditionValuesOfType('section', this.props.appliedFilterConditions)
|
||||
if (prevSectionIds[0] !== sectionIds[0]) {
|
||||
if (sectionIds.length === 0) {
|
||||
this.updateCurrentSection(null)
|
||||
|
@ -4760,11 +4762,14 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
|
|||
}
|
||||
}
|
||||
|
||||
const prevStudentGroupIds = findAllAppliedFilterValuesOfType(
|
||||
const prevStudentGroupIds = findConditionValuesOfType(
|
||||
'student-group',
|
||||
prevProps.filters
|
||||
prevProps.appliedFilterConditions
|
||||
)
|
||||
const studentGroupIds = findConditionValuesOfType(
|
||||
'student-group',
|
||||
this.props.appliedFilterConditions
|
||||
)
|
||||
const studentGroupIds = findAllAppliedFilterValuesOfType('student-group', this.props.filters)
|
||||
if (prevStudentGroupIds[0] !== studentGroupIds[0]) {
|
||||
if (studentGroupIds.length === 0 || !studentGroupIds[0]) {
|
||||
this.updateCurrentStudentGroup(null)
|
||||
|
@ -4773,13 +4778,13 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
|
|||
}
|
||||
}
|
||||
|
||||
const prevGradingPeriodId = findAllAppliedFilterValuesOfType(
|
||||
const prevGradingPeriodId = findConditionValuesOfType(
|
||||
'grading-period',
|
||||
prevProps.filters
|
||||
prevProps.appliedFilterConditions
|
||||
)[0]
|
||||
const gradingPeriodId = findAllAppliedFilterValuesOfType(
|
||||
const gradingPeriodId = findConditionValuesOfType(
|
||||
'grading-period',
|
||||
this.props.filters
|
||||
this.props.appliedFilterConditions
|
||||
)[0]
|
||||
if (prevGradingPeriodId !== gradingPeriodId) {
|
||||
if (!gradingPeriodId) {
|
||||
|
|
|
@ -20,7 +20,6 @@ import React from 'react'
|
|||
import ReactDOM from 'react-dom'
|
||||
import assignmentHelper from '../shared/helpers/assignmentHelper'
|
||||
import {showConfirmationDialog} from '@canvas/feature-flags/react/ConfirmationDialog'
|
||||
// @ts-ignore
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import _ from 'lodash'
|
||||
import htmlEscape from 'html-escape'
|
||||
|
@ -191,18 +190,15 @@ export function getAssignmentGroupColumnId(assignmentGroupId: string) {
|
|||
return `assignment_group_${assignmentGroupId}`
|
||||
}
|
||||
|
||||
export function findAllAppliedFilterValuesOfType(type: FilterConditionType, filters: Filter[]) {
|
||||
return filters
|
||||
.filter(f => f.is_applied)
|
||||
.flatMap(f => f.conditions.filter(c => c.type === type && c.value))
|
||||
.map(c => c.value)
|
||||
}
|
||||
|
||||
export function getAllAppliedFilterValues(filters: Filter[]) {
|
||||
return filters
|
||||
.filter(f => f.is_applied)
|
||||
.flatMap(f => f.conditions.filter(c => c.value))
|
||||
.map(c => c.value)
|
||||
export function findConditionValuesOfType(
|
||||
type: FilterConditionType,
|
||||
appliedConditions: FilterCondition[]
|
||||
) {
|
||||
return appliedConditions.reduce(
|
||||
(values: string[], condition: FilterCondition) =>
|
||||
condition.type === type && condition.value ? values.concat(condition.value) : values,
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
// Extra normalization; comes from jsonb payload
|
||||
|
@ -222,7 +218,6 @@ export const deserializeFilter = (json: GradebookFilterApiResponse): Filter => {
|
|||
id: filter.id,
|
||||
name: String(filter.name),
|
||||
conditions,
|
||||
is_applied: !!filter.payload.is_applied,
|
||||
created_at: String(filter.created_at)
|
||||
}
|
||||
}
|
||||
|
@ -231,7 +226,6 @@ export const serializeFilter = (filter: PartialFilter): GradebookFilterApiReques
|
|||
return {
|
||||
name: filter.name,
|
||||
payload: {
|
||||
is_applied: filter.is_applied,
|
||||
conditions: filter.conditions
|
||||
}
|
||||
}
|
||||
|
@ -301,3 +295,18 @@ export const getLabelForFilterCondition = (
|
|||
// unrecognized types should have been filtered out by deserializeFilter
|
||||
throw new Error('invalid condition type')
|
||||
}
|
||||
|
||||
export function doFilterConditionsMatch(
|
||||
conditions1: FilterCondition[],
|
||||
conditions2: FilterCondition[]
|
||||
) {
|
||||
const conditionsWithValues1 = conditions1.filter(c => c.value)
|
||||
const conditionsWithValues2 = conditions2.filter(c => c.value)
|
||||
return (
|
||||
conditionsWithValues1.length > 0 &&
|
||||
conditionsWithValues1.length === conditionsWithValues2.length &&
|
||||
conditionsWithValues1.every(c1 =>
|
||||
conditionsWithValues2.some(c2 => c2.type === c1.type && c2.value === c1.value)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export default function GradebookData(props) {
|
|||
const courseId = props.gradebookEnv.context_id
|
||||
const flashMessages = useStore(state => state.flashMessages)
|
||||
|
||||
const appliedFilters = useStore(state => state.appliedFilters(), shallow)
|
||||
const appliedFilterConditions = useStore(state => state.appliedFilterConditions, shallow)
|
||||
const isFiltersLoading = useStore(state => state.isFiltersLoading)
|
||||
const initializeStagedFilter = useStore(state => state.initializeStagedFilter)
|
||||
const fetchFilters = useStore(state => state.fetchFilters)
|
||||
|
@ -86,8 +86,8 @@ export default function GradebookData(props) {
|
|||
<Gradebook
|
||||
{...props}
|
||||
flashAlerts={flashMessages}
|
||||
filters={appliedFilters}
|
||||
hideGrid={false}
|
||||
appliedFilterConditions={appliedFilterConditions}
|
||||
isFiltersLoading={isFiltersLoading}
|
||||
isModulesLoading={isModulesLoading}
|
||||
modules={modules}
|
||||
|
|
|
@ -20,7 +20,6 @@ import React, {PureComponent} from 'react'
|
|||
import {View} from '@instructure/ui-view'
|
||||
import {Grid} from '@instructure/ui-grid'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
|
||||
const I18n = useI18nScope('gradebook')
|
||||
|
|
|
@ -26,8 +26,8 @@ import {
|
|||
getCustomColumnId,
|
||||
getAssignmentColumnId,
|
||||
getAssignmentGroupColumnId,
|
||||
findAllAppliedFilterValuesOfType,
|
||||
getAllAppliedFilterValues
|
||||
findConditionValuesOfType,
|
||||
doFilterConditionsMatch
|
||||
} from '../Gradebook.utils'
|
||||
import {isDefaultSortOrder, localeSort} from '../Gradebook.sorting'
|
||||
import {createGradebook} from './GradebookSpecHelper'
|
||||
|
@ -273,12 +273,11 @@ describe('getAssignmentGroupColumnId', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('findAllAppliedFilterValuesOfType', () => {
|
||||
describe('findConditionValuesOfType', () => {
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Filter 1',
|
||||
is_applied: true,
|
||||
conditions: [
|
||||
{id: '1', type: 'module', value: '1', created_at: ''},
|
||||
{id: '2', type: 'assignment-group', value: '2', created_at: ''},
|
||||
|
@ -290,7 +289,6 @@ describe('findAllAppliedFilterValuesOfType', () => {
|
|||
{
|
||||
id: '2',
|
||||
name: 'Filter 2',
|
||||
is_applied: false,
|
||||
conditions: [
|
||||
{id: '1', type: 'module', value: '4', created_at: ''},
|
||||
{id: '2', type: 'assignment-group', value: '5', created_at: ''},
|
||||
|
@ -300,21 +298,22 @@ describe('findAllAppliedFilterValuesOfType', () => {
|
|||
}
|
||||
]
|
||||
|
||||
it('only returns applied filters', () => {
|
||||
expect(findAllAppliedFilterValuesOfType('module', filters)).toStrictEqual(['1', '3'])
|
||||
it('returns module condition values', () => {
|
||||
expect(findConditionValuesOfType('module', filters[0].conditions)).toStrictEqual(['1', '3'])
|
||||
})
|
||||
|
||||
it('only returns selected type', () => {
|
||||
expect(findAllAppliedFilterValuesOfType('assignment-group', filters)).toStrictEqual(['2', '7'])
|
||||
it('returns assignment-group condition values', () => {
|
||||
expect(findConditionValuesOfType('assignment-group', filters[1].conditions)).toStrictEqual([
|
||||
'5'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAllAppliedFilterValues', () => {
|
||||
describe('doFilterConditionsMatch', () => {
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Filter 1',
|
||||
is_applied: true,
|
||||
conditions: [
|
||||
{id: '1', type: 'module', value: '1', created_at: ''},
|
||||
{id: '2', type: 'assignment-group', value: '2', created_at: ''},
|
||||
|
@ -326,7 +325,16 @@ describe('getAllAppliedFilterValues', () => {
|
|||
{
|
||||
id: '2',
|
||||
name: 'Filter 2',
|
||||
is_applied: false,
|
||||
conditions: [
|
||||
{id: '1', type: 'module', value: '4', created_at: ''},
|
||||
{id: '2', type: 'assignment-group', value: '5', created_at: ''},
|
||||
{id: '3', type: 'module', value: '6', created_at: ''}
|
||||
],
|
||||
created_at: '2019-01-01T00:00:01Z'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Filter 3',
|
||||
conditions: [
|
||||
{id: '1', type: 'module', value: '4', created_at: ''},
|
||||
{id: '2', type: 'assignment-group', value: '5', created_at: ''},
|
||||
|
@ -336,7 +344,15 @@ describe('getAllAppliedFilterValues', () => {
|
|||
}
|
||||
]
|
||||
|
||||
it('returns only applied filter values', () => {
|
||||
expect(getAllAppliedFilterValues(filters)).toStrictEqual(['1', '2', '7', '3'])
|
||||
it('returns false if filter conditions are different', () => {
|
||||
expect(doFilterConditionsMatch(filters[0].conditions, filters[1].conditions)).toStrictEqual(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('returns true if filter conditions are the same', () => {
|
||||
expect(doFilterConditionsMatch(filters[1].conditions, filters[2].conditions)).toStrictEqual(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -92,6 +92,7 @@ export const defaultGradebookEnv = {
|
|||
}
|
||||
|
||||
export const defaultGradebookProps = {
|
||||
appliedFilterConditions: [],
|
||||
filters: [],
|
||||
isFiltersLoading: false,
|
||||
onFiltersChange: () => {},
|
||||
|
|
|
@ -26,7 +26,6 @@ import GradebookExportManager from '../../shared/GradebookExportManager'
|
|||
import PostGradesApp from '../../SISGradePassback/PostGradesApp'
|
||||
import tz from '@canvas/timezone'
|
||||
import DateHelper from '@canvas/datetime/dateHelper'
|
||||
// @ts-ignore
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import '@canvas/rails-flash-notifications'
|
||||
|
||||
|
|
|
@ -20,9 +20,7 @@ import React, {useState} from 'react'
|
|||
import {Button, CloseButton} from '@instructure/ui-buttons'
|
||||
import {AccessibleContent} from '@instructure/ui-a11y-content'
|
||||
import uuid from 'uuid'
|
||||
// @ts-ignore
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import {Tooltip} from '@instructure/ui-tooltip'
|
||||
import {IconFilterSolid, IconFilterLine} from '@instructure/ui-icons'
|
||||
import {TextInput} from '@instructure/ui-text-input'
|
||||
import {View, ContextView} from '@instructure/ui-view'
|
||||
|
@ -37,11 +35,10 @@ import type {
|
|||
Filter,
|
||||
GradingPeriod,
|
||||
Module,
|
||||
PartialFilter,
|
||||
Section,
|
||||
StudentGroupCategoryMap
|
||||
} from '../gradebook.d'
|
||||
import {getLabelForFilterCondition} from '../Gradebook.utils'
|
||||
import {getLabelForFilterCondition, doFilterConditionsMatch} from '../Gradebook.utils'
|
||||
import useStore from '../stores/index'
|
||||
|
||||
const I18n = useI18nScope('gradebook')
|
||||
|
@ -56,20 +53,6 @@ export type FilterNavProps = {
|
|||
studentGroupCategories: StudentGroupCategoryMap
|
||||
}
|
||||
|
||||
const newFilter = (): PartialFilter => ({
|
||||
name: '',
|
||||
conditions: [
|
||||
{
|
||||
id: uuid(),
|
||||
type: undefined,
|
||||
value: undefined,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
],
|
||||
is_applied: false,
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
|
||||
export default function FilterNav({
|
||||
assignmentGroups,
|
||||
gradingPeriods,
|
||||
|
@ -78,74 +61,47 @@ export default function FilterNav({
|
|||
studentGroupCategories
|
||||
}: FilterNavProps) {
|
||||
const [isTrayOpen, setIsTrayOpen] = useState(false)
|
||||
const [stagedFilterName, setStagedFilterName] = useState('')
|
||||
const filters = useStore(state => state.filters)
|
||||
const stagedFilter = useStore(state => state.stagedFilter)
|
||||
const stagedFilterConditions = useStore(state => state.stagedFilterConditions)
|
||||
const saveStagedFilter = useStore(state => state.saveStagedFilter)
|
||||
const updateFilter = useStore(state => state.updateFilter)
|
||||
const deleteFilter = useStore(state => state.deleteFilter)
|
||||
const applyConditions = useStore(state => state.applyConditions)
|
||||
const appliedFilterConditions = useStore(state => state.appliedFilterConditions)
|
||||
const updateStagedFilter = useStore(state => state.updateStagedFilter)
|
||||
const deleteStagedFilter = useStore(state => state.deleteStagedFilter)
|
||||
|
||||
const filterComponents = filters
|
||||
.filter(f => f.is_applied)
|
||||
.map(filter => {
|
||||
const tooltip = filter.conditions
|
||||
.filter(c => c.value)
|
||||
.map(condition => {
|
||||
const label = getLabelForFilterCondition(
|
||||
condition,
|
||||
assignmentGroups,
|
||||
gradingPeriods,
|
||||
modules,
|
||||
sections,
|
||||
studentGroupCategories
|
||||
)
|
||||
return <div key={`filter-${filter.id}-condition-${condition.id}}`}>{label}</div>
|
||||
})
|
||||
|
||||
const filterComponents = appliedFilterConditions
|
||||
.filter(c => c.value)
|
||||
.map(condition => {
|
||||
const label = getLabelForFilterCondition(
|
||||
condition,
|
||||
assignmentGroups,
|
||||
gradingPeriods,
|
||||
modules,
|
||||
sections,
|
||||
studentGroupCategories
|
||||
)
|
||||
return (
|
||||
<Tooltip key={filter.id} renderTip={tooltip} placement="top" on={['hover', 'focus']}>
|
||||
<Tag
|
||||
data-testid={`filter-tag-${filter.id}`}
|
||||
text={
|
||||
<AccessibleContent alt={I18n.t('Remove filter')}>{filter.name}</AccessibleContent>
|
||||
}
|
||||
dismissible
|
||||
onClick={() => updateFilter({...filter, is_applied: false})}
|
||||
margin="0 xx-small 0 0"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tag
|
||||
data-testid="staged-filter-condition-tag"
|
||||
key={`staged-condition-${condition.id}`}
|
||||
text={<AccessibleContent alt={I18n.t('Remove condition')}>{label}</AccessibleContent>}
|
||||
dismissible
|
||||
onClick={() =>
|
||||
useStore.setState({
|
||||
appliedFilterConditions: appliedFilterConditions.filter(c => c.id !== condition.id)
|
||||
})
|
||||
}
|
||||
margin="0 xx-small 0 0"
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
if (stagedFilter && stagedFilter.is_applied) {
|
||||
stagedFilter.conditions
|
||||
.filter(c => c.value)
|
||||
.forEach(condition => {
|
||||
const label = getLabelForFilterCondition(
|
||||
condition,
|
||||
assignmentGroups,
|
||||
gradingPeriods,
|
||||
modules,
|
||||
sections,
|
||||
studentGroupCategories
|
||||
)
|
||||
filterComponents.push(
|
||||
<Tag
|
||||
data-testid="staged-filter-condition-tag"
|
||||
key={`staged-condition-${condition.id}`}
|
||||
text={<AccessibleContent alt={I18n.t('Remove condition')}>{label}</AccessibleContent>}
|
||||
dismissible
|
||||
onClick={() =>
|
||||
useStore.setState({
|
||||
stagedFilter: {
|
||||
...stagedFilter,
|
||||
conditions: stagedFilter.conditions.filter(c => c.id !== condition.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
margin="0 xx-small 0 0"
|
||||
/>
|
||||
)
|
||||
})
|
||||
const handleSaveStagedFilter = async () => {
|
||||
await saveStagedFilter(stagedFilterName)
|
||||
setStagedFilterName('')
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -195,7 +151,7 @@ export default function FilterNav({
|
|||
</FlexItem>
|
||||
</Flex>
|
||||
|
||||
{filters.length === 0 && !stagedFilter && (
|
||||
{filters.length === 0 && !stagedFilterConditions.length && (
|
||||
<Flex as="div" margin="small">
|
||||
<FlexItem display="inline-block" width="100px" height="128px">
|
||||
<img
|
||||
|
@ -226,10 +182,12 @@ export default function FilterNav({
|
|||
<FilterNavFilter
|
||||
assignmentGroups={assignmentGroups}
|
||||
filter={filter}
|
||||
isApplied={doFilterConditionsMatch(appliedFilterConditions, filter.conditions)}
|
||||
gradingPeriods={gradingPeriods}
|
||||
key={filter.id}
|
||||
modules={modules}
|
||||
onChange={(f: Filter) => updateFilter(f)}
|
||||
onChange={updateFilter}
|
||||
applyConditions={applyConditions}
|
||||
onDelete={() => deleteFilter(filter)}
|
||||
sections={sections}
|
||||
studentGroupCategories={studentGroupCategories}
|
||||
|
@ -242,16 +200,25 @@ export default function FilterNav({
|
|||
padding="small none none none"
|
||||
borderWidth="small none none none"
|
||||
>
|
||||
{stagedFilter ? (
|
||||
{stagedFilterConditions.length > 0 ? (
|
||||
<>
|
||||
<FilterNavFilter
|
||||
assignmentGroups={assignmentGroups}
|
||||
filter={stagedFilter}
|
||||
filter={{
|
||||
name: '',
|
||||
conditions: stagedFilterConditions,
|
||||
created_at: new Date().toISOString()
|
||||
}}
|
||||
isApplied={doFilterConditionsMatch(
|
||||
appliedFilterConditions,
|
||||
stagedFilterConditions
|
||||
)}
|
||||
gradingPeriods={gradingPeriods}
|
||||
key="staged"
|
||||
modules={modules}
|
||||
onChange={(f: PartialFilter) => useStore.setState({stagedFilter: f})}
|
||||
onDelete={() => useStore.setState({stagedFilter: null})}
|
||||
applyConditions={applyConditions}
|
||||
onChange={(filter: Filter) => updateStagedFilter(filter.conditions)}
|
||||
onDelete={deleteStagedFilter}
|
||||
sections={sections}
|
||||
studentGroupCategories={studentGroupCategories}
|
||||
/>
|
||||
|
@ -262,14 +229,9 @@ export default function FilterNav({
|
|||
width="100%"
|
||||
renderLabel={I18n.t('Save these conditions as a filter')}
|
||||
placeholder={I18n.t('Give this filter a name')}
|
||||
value={stagedFilter.name}
|
||||
value={stagedFilterName}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
useStore.setState({
|
||||
stagedFilter: {
|
||||
...stagedFilter,
|
||||
name: event.target.value
|
||||
}
|
||||
})
|
||||
setStagedFilterName(event.target.value)
|
||||
}
|
||||
/>
|
||||
</FlexItem>
|
||||
|
@ -278,8 +240,8 @@ export default function FilterNav({
|
|||
color="secondary"
|
||||
data-testid="save-filter-button"
|
||||
margin="small 0 0 0"
|
||||
onClick={saveStagedFilter}
|
||||
interaction={stagedFilter.name.trim().length > 0 ? 'enabled' : 'disabled'}
|
||||
onClick={handleSaveStagedFilter}
|
||||
interaction={stagedFilterName.trim().length > 0 ? 'enabled' : 'disabled'}
|
||||
>
|
||||
{I18n.t('Save')}
|
||||
</Button>
|
||||
|
@ -291,7 +253,18 @@ export default function FilterNav({
|
|||
<Button
|
||||
renderIcon={IconFilterLine}
|
||||
color="secondary"
|
||||
onClick={() => useStore.setState({stagedFilter: newFilter()})}
|
||||
onClick={() =>
|
||||
useStore.setState({
|
||||
stagedFilterConditions: [
|
||||
{
|
||||
id: uuid(),
|
||||
type: undefined,
|
||||
value: undefined,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
margin="small 0 0 0"
|
||||
data-testid="new-filter-button"
|
||||
>
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
*/
|
||||
|
||||
import React, {useRef} from 'react'
|
||||
// @ts-ignore
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {SimpleSelect} from '@instructure/ui-simple-select'
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
import React, {useState, useRef, useEffect} from 'react'
|
||||
import uuid from 'uuid'
|
||||
// @ts-ignore
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import {Button, IconButton} from '@instructure/ui-buttons'
|
||||
|
@ -51,9 +50,11 @@ const I18n = useI18nScope('gradebook')
|
|||
const {Item} = Flex as any
|
||||
|
||||
export type FilterNavFilterProps = {
|
||||
applyConditions: (conditions: PartialFilter['conditions']) => void
|
||||
assignmentGroups: AssignmentGroup[]
|
||||
filter: PartialFilter | Filter
|
||||
gradingPeriods: GradingPeriod[]
|
||||
isApplied: boolean
|
||||
modules: Module[]
|
||||
onChange: any
|
||||
onDelete: any
|
||||
|
@ -62,12 +63,14 @@ export type FilterNavFilterProps = {
|
|||
}
|
||||
|
||||
export default function FilterNavFilter({
|
||||
filter,
|
||||
onDelete,
|
||||
onChange,
|
||||
modules,
|
||||
gradingPeriods,
|
||||
applyConditions,
|
||||
assignmentGroups,
|
||||
filter,
|
||||
gradingPeriods,
|
||||
isApplied,
|
||||
modules,
|
||||
onChange,
|
||||
onDelete,
|
||||
sections,
|
||||
studentGroupCategories
|
||||
}: FilterNavFilterProps) {
|
||||
|
@ -127,10 +130,7 @@ export default function FilterNavFilter({
|
|||
}
|
||||
|
||||
const toggleApply = () => {
|
||||
onChange({
|
||||
...filter,
|
||||
is_applied: !filter.is_applied
|
||||
})
|
||||
applyConditions(isApplied ? [] : filter.conditions)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -229,8 +229,8 @@ export default function FilterNavFilter({
|
|||
<Flex>
|
||||
<Item>
|
||||
<Checkbox
|
||||
checked={filter.is_applied}
|
||||
label={filter.id ? I18n.t('Apply filter') : I18n.t('Apply conditions')}
|
||||
checked={isApplied}
|
||||
label={I18n.t('Apply conditions')}
|
||||
labelPlacement="start"
|
||||
onChange={toggleApply}
|
||||
size="small"
|
||||
|
|
|
@ -21,7 +21,7 @@ import FilterNav from '../FilterNav'
|
|||
import fetchMock from 'fetch-mock'
|
||||
import store from '../../stores/index'
|
||||
import type {FilterNavProps} from '../FilterNav'
|
||||
import type {Filter} from '../../gradebook.d'
|
||||
import type {Filter, FilterCondition} from '../../gradebook.d'
|
||||
import {render, waitFor} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
|
@ -61,6 +61,15 @@ const defaultProps: FilterNavProps = {
|
|||
}
|
||||
}
|
||||
|
||||
const defaultAppliedFilterConditions: FilterCondition[] = [
|
||||
{
|
||||
id: '2',
|
||||
type: 'module',
|
||||
value: '1',
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
|
||||
const defaultFilters: Filter[] = [
|
||||
{
|
||||
id: '1',
|
||||
|
@ -69,10 +78,10 @@ const defaultFilters: Filter[] = [
|
|||
{
|
||||
id: '2',
|
||||
type: 'module',
|
||||
created_at: new Date().toISOString()
|
||||
value: '1',
|
||||
created_at: '2022-02-05T10:18:34-07:00'
|
||||
}
|
||||
],
|
||||
is_applied: true,
|
||||
created_at: '2022-02-05T10:18:34-07:00'
|
||||
},
|
||||
{
|
||||
|
@ -82,10 +91,10 @@ const defaultFilters: Filter[] = [
|
|||
{
|
||||
id: '3',
|
||||
type: 'section',
|
||||
value: '7',
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
],
|
||||
is_applied: true,
|
||||
created_at: '2022-02-06T10:18:34-07:00'
|
||||
}
|
||||
]
|
||||
|
@ -97,7 +106,6 @@ const mockPostResponse = {
|
|||
user_id: '1',
|
||||
name: 'test',
|
||||
payload: {
|
||||
is_applied: false,
|
||||
conditions: [
|
||||
{
|
||||
id: 'f783e528-dbb5-4474-972a-0f1a19c29551',
|
||||
|
@ -114,7 +122,10 @@ const mockPostResponse = {
|
|||
|
||||
describe('FilterNav', () => {
|
||||
beforeEach(() => {
|
||||
store.setState({filters: defaultFilters})
|
||||
store.setState({
|
||||
filters: defaultFilters,
|
||||
appliedFilterConditions: defaultAppliedFilterConditions
|
||||
})
|
||||
fetchMock.mock('*', 200)
|
||||
})
|
||||
afterEach(() => {
|
||||
|
@ -132,45 +143,22 @@ describe('FilterNav', () => {
|
|||
await findByText(/Applied Filters:/)
|
||||
})
|
||||
|
||||
it('render filter tag for saved filter', async () => {
|
||||
const {getByTestId} = render(<FilterNav {...defaultProps} />)
|
||||
expect(await getByTestId('filter-tag-1')).toHaveTextContent('Filter 1')
|
||||
})
|
||||
|
||||
it('clicking filter tag for saved filter removes the tag', async () => {
|
||||
const {getByTestId, queryByTestId} = render(<FilterNav {...defaultProps} />)
|
||||
userEvent.click(await getByTestId('filter-tag-1'))
|
||||
expect(await queryByTestId('filter-tag-1')).toBeNull()
|
||||
})
|
||||
|
||||
it('clicking filter tag for saved filter does not remove the filter', async () => {
|
||||
const {getByTestId, getByText} = render(<FilterNav {...defaultProps} />)
|
||||
userEvent.click(await getByTestId('filter-tag-1'))
|
||||
userEvent.click(getByText('Filters'))
|
||||
expect(getByTestId('filter-name-1')).toHaveTextContent('Filter 1')
|
||||
})
|
||||
|
||||
it('render condition tag for applied staged filter', async () => {
|
||||
store.setState({
|
||||
stagedFilter: {
|
||||
name: '',
|
||||
conditions: [
|
||||
{
|
||||
id: '4',
|
||||
type: 'module',
|
||||
value: '1',
|
||||
created_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: undefined,
|
||||
value: undefined,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
],
|
||||
is_applied: true,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
stagedFilterConditions: [
|
||||
{
|
||||
id: '4',
|
||||
type: 'module',
|
||||
value: '1',
|
||||
created_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: undefined,
|
||||
value: undefined,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
})
|
||||
const {getAllByTestId} = render(<FilterNav {...defaultProps} />)
|
||||
expect(await getAllByTestId('staged-filter-condition-tag')[0]).toHaveTextContent('Module 1')
|
||||
|
@ -183,7 +171,7 @@ describe('FilterNav', () => {
|
|||
})
|
||||
|
||||
it('shows friendly panda image when there are no filters', async () => {
|
||||
store.setState({filters: [], stagedFilter: null})
|
||||
store.setState({filters: [], stagedFilterConditions: []})
|
||||
const {getByAltText, getByText} = render(<FilterNav {...defaultProps} />)
|
||||
userEvent.click(getByText('Filters'))
|
||||
expect(await getByAltText('Friendly panda')).toBeInTheDocument()
|
||||
|
@ -245,18 +233,18 @@ describe('FilterNav', () => {
|
|||
)
|
||||
userEvent.click(getByText('Filters'))
|
||||
expect(getAllByPlaceholderText(/Select condition type/)[0]).toBeInTheDocument()
|
||||
const checkbox = getAllByRole('checkbox', {name: /Apply filter/})[0]
|
||||
const checkbox = getAllByRole('checkbox', {name: /Apply conditions/})[0]
|
||||
expect(checkbox).toBeChecked()
|
||||
userEvent.click(checkbox)
|
||||
expect(checkbox).not.toBeChecked()
|
||||
})
|
||||
|
||||
it('Enables filter', () => {
|
||||
store.setState({filters: [{...defaultFilters[0], is_applied: false}]})
|
||||
store.setState({filters: [{...defaultFilters[0]}], appliedFilterConditions: []})
|
||||
const {getByText, getByRole, getByPlaceholderText} = render(<FilterNav {...defaultProps} />)
|
||||
userEvent.click(getByText('Filters'))
|
||||
expect(getByPlaceholderText(/Select condition type/)).toBeInTheDocument()
|
||||
const checkbox = getByRole('checkbox', {name: /Apply filter/})
|
||||
const checkbox = getByRole('checkbox', {name: /Apply conditions/})
|
||||
expect(checkbox).not.toBeChecked()
|
||||
userEvent.click(checkbox)
|
||||
expect(checkbox).toBeChecked()
|
||||
|
@ -265,7 +253,10 @@ describe('FilterNav', () => {
|
|||
|
||||
describe('FilterNav (save)', () => {
|
||||
beforeEach(() => {
|
||||
store.setState({filters: defaultFilters})
|
||||
store.setState({
|
||||
filters: defaultFilters,
|
||||
appliedFilterConditions: defaultAppliedFilterConditions
|
||||
})
|
||||
fetchMock.post('/api/v1/courses/0/gradebook_filters', mockPostResponse)
|
||||
})
|
||||
afterEach(() => {
|
||||
|
|
|
@ -41,12 +41,13 @@ const defaultFilter: Filter = {
|
|||
type: 'section',
|
||||
value: undefined
|
||||
}
|
||||
],
|
||||
is_applied: true
|
||||
]
|
||||
}
|
||||
|
||||
const defaultProps: FilterNavFilterProps = {
|
||||
filter: defaultFilter,
|
||||
applyConditions: () => ({}),
|
||||
isApplied: false,
|
||||
onChange: () => {},
|
||||
onDelete: () => {},
|
||||
modules: [
|
||||
|
|
|
@ -25,6 +25,7 @@ export type CourseSettingsType = {
|
|||
}
|
||||
|
||||
export type GradebookSettings = {
|
||||
hide_assignment_group_totals: string
|
||||
show_separate_first_last_names: string
|
||||
show_unpublished_assignments: string
|
||||
show_concluded_enrollments: string
|
||||
|
@ -327,18 +328,16 @@ export type Filter = {
|
|||
id: string
|
||||
name: string
|
||||
conditions: FilterCondition[]
|
||||
is_applied: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type PartialFilter = Omit<Filter, 'id'> & {id?: string}
|
||||
|
||||
export type AppliedFilter = Omit<Filter, 'id'> & {is_applied: true}
|
||||
// export type AppliedFilter = Omit<Filter, 'id'>
|
||||
|
||||
export type GradebookFilterApiRequest = {
|
||||
name: string
|
||||
payload: {
|
||||
is_applied: boolean
|
||||
conditions: FilterCondition[]
|
||||
}
|
||||
}
|
||||
|
@ -354,7 +353,6 @@ export type GradebookFilterApiResponseFilter = {
|
|||
name: string
|
||||
payload: {
|
||||
conditions: FilterCondition[]
|
||||
is_applied: boolean
|
||||
}
|
||||
created_at: string
|
||||
updated_at: string
|
||||
|
|
|
@ -72,4 +72,5 @@ export type GridDisplaySettings = {
|
|||
viewUngradedAsZero: boolean
|
||||
showUnpublishedAssignments: boolean
|
||||
showSeparateFirstLastNames: boolean
|
||||
hideAssignmentGroupTotals: boolean
|
||||
}
|
||||
|
|
|
@ -30,8 +30,7 @@ const mockResponse: GradebookFilterApiResponse[] = [
|
|||
user_id: '1',
|
||||
name: 'filter 1',
|
||||
payload: {
|
||||
conditions: [],
|
||||
is_applied: true
|
||||
conditions: []
|
||||
},
|
||||
created_at: '2020-01-01T00:00:00Z',
|
||||
updated_at: '2020-01-01T00:00:00Z'
|
||||
|
@ -44,8 +43,7 @@ const mockResponse: GradebookFilterApiResponse[] = [
|
|||
user_id: '1',
|
||||
name: 'filter 2',
|
||||
payload: {
|
||||
conditions: [],
|
||||
is_applied: false
|
||||
conditions: []
|
||||
},
|
||||
created_at: '2020-01-01T00:00:00Z',
|
||||
updated_at: '2020-01-01T00:00:00Z'
|
||||
|
@ -71,7 +69,6 @@ describe('filterState', () => {
|
|||
id: '321',
|
||||
name: 'filter 1',
|
||||
conditions: [],
|
||||
is_applied: true,
|
||||
created_at: '2020-01-01T00:00:00Z'
|
||||
}
|
||||
])
|
||||
|
@ -79,31 +76,25 @@ describe('filterState', () => {
|
|||
|
||||
it('saves staged filter', async () => {
|
||||
store.setState({
|
||||
stagedFilter: {
|
||||
name: 'filter 1',
|
||||
conditions: [
|
||||
{
|
||||
id: '123',
|
||||
type: 'student-group',
|
||||
value: '1',
|
||||
created_at: '2022-01-01T00:00:00Z'
|
||||
}
|
||||
],
|
||||
created_at: '2022-01-01T00:00:00Z',
|
||||
is_applied: true
|
||||
}
|
||||
stagedFilterConditions: [
|
||||
{
|
||||
id: '123',
|
||||
type: 'student-group',
|
||||
value: '1',
|
||||
created_at: '2022-01-01T00:00:00Z'
|
||||
}
|
||||
]
|
||||
})
|
||||
const url = `/api/v1/courses/${courseId}/gradebook_filters`
|
||||
fetchMock.post(url, mockResponse[0])
|
||||
await store.getState().saveStagedFilter()
|
||||
await store.getState().saveStagedFilter('filter 1')
|
||||
expect(fetchMock.called(url, 'POST')).toBe(true)
|
||||
expect(store.getState().stagedFilter).toBeNull()
|
||||
expect(store.getState().stagedFilterConditions.length).toStrictEqual(0)
|
||||
expect(store.getState().filters).toMatchObject([
|
||||
{
|
||||
id: '321',
|
||||
name: 'filter 1',
|
||||
conditions: [],
|
||||
is_applied: true,
|
||||
created_at: '2020-01-01T00:00:00Z'
|
||||
}
|
||||
])
|
||||
|
@ -116,7 +107,6 @@ describe('filterState', () => {
|
|||
id: '321',
|
||||
name: 'filter 1',
|
||||
conditions: [],
|
||||
is_applied: true,
|
||||
created_at: '2020-01-01T00:00:00Z'
|
||||
}
|
||||
]
|
||||
|
@ -132,7 +122,6 @@ describe('filterState', () => {
|
|||
id: '321',
|
||||
name: 'filter 1 (renamed)',
|
||||
conditions: [],
|
||||
is_applied: true,
|
||||
created_at: '2020-01-01T00:00:00Z'
|
||||
})
|
||||
expect(fetchMock.called(url, 'PUT')).toBe(true)
|
||||
|
@ -141,7 +130,6 @@ describe('filterState', () => {
|
|||
id: '321',
|
||||
name: 'filter 1 (renamed)',
|
||||
conditions: [],
|
||||
is_applied: true,
|
||||
created_at: '2020-01-01T00:00:00Z'
|
||||
}
|
||||
])
|
||||
|
@ -154,7 +142,6 @@ describe('filterState', () => {
|
|||
id: '321',
|
||||
name: 'filter 1',
|
||||
conditions: [],
|
||||
is_applied: true,
|
||||
created_at: '2020-01-01T00:00:00Z'
|
||||
}
|
||||
]
|
||||
|
@ -165,7 +152,6 @@ describe('filterState', () => {
|
|||
id: '321',
|
||||
name: 'filter 1 (renamed)',
|
||||
conditions: [],
|
||||
is_applied: true,
|
||||
created_at: '2020-01-01T00:00:00Z'
|
||||
})
|
||||
expect(fetchMock.called(url, 'DELETE')).toBe(true)
|
||||
|
@ -185,7 +171,7 @@ describe('filterState', () => {
|
|||
grading_period_id: null
|
||||
}
|
||||
store.getState().initializeStagedFilter(initialRowFilterSettings, initialColumnFilterSettings)
|
||||
expect(store.getState().stagedFilter).toBeNull()
|
||||
expect(store.getState().stagedFilterConditions.length).toStrictEqual(0)
|
||||
})
|
||||
|
||||
it('derive staged section filter from gradebook settings', async () => {
|
||||
|
@ -201,19 +187,14 @@ describe('filterState', () => {
|
|||
grading_period_id: null
|
||||
}
|
||||
store.getState().initializeStagedFilter(initialRowFilterSettings, initialColumnFilterSettings)
|
||||
expect(store.getState().stagedFilter).not.toBeNull()
|
||||
expect(store.getState().stagedFilter).toMatchObject({
|
||||
name: '',
|
||||
conditions: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
type: 'section',
|
||||
value: '1'
|
||||
}
|
||||
],
|
||||
is_applied: true,
|
||||
created_at: expect.any(String)
|
||||
})
|
||||
expect(store.getState().stagedFilterConditions).not.toBeNull()
|
||||
expect(store.getState().stagedFilterConditions).toMatchObject([
|
||||
{
|
||||
id: expect.any(String),
|
||||
type: 'section',
|
||||
value: '1'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('derive staged student group filter from gradebook settings', async () => {
|
||||
|
@ -229,19 +210,14 @@ describe('filterState', () => {
|
|||
grading_period_id: null
|
||||
}
|
||||
store.getState().initializeStagedFilter(initialRowFilterSettings, initialColumnFilterSettings)
|
||||
expect(store.getState().stagedFilter).not.toBeNull()
|
||||
expect(store.getState().stagedFilter).toMatchObject({
|
||||
name: '',
|
||||
conditions: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
type: 'student-group',
|
||||
value: '1'
|
||||
}
|
||||
],
|
||||
is_applied: true,
|
||||
created_at: expect.any(String)
|
||||
})
|
||||
expect(store.getState().stagedFilterConditions).not.toBeNull()
|
||||
expect(store.getState().stagedFilterConditions).toMatchObject([
|
||||
{
|
||||
id: expect.any(String),
|
||||
type: 'student-group',
|
||||
value: '1'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('derive staged assignment group filter from gradebook settings', async () => {
|
||||
|
@ -257,19 +233,14 @@ describe('filterState', () => {
|
|||
grading_period_id: null
|
||||
}
|
||||
store.getState().initializeStagedFilter(initialRowFilterSettings, initialColumnFilterSettings)
|
||||
expect(store.getState().stagedFilter).not.toBeNull()
|
||||
expect(store.getState().stagedFilter).toMatchObject({
|
||||
name: '',
|
||||
conditions: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
type: 'assignment-group',
|
||||
value: '1'
|
||||
}
|
||||
],
|
||||
is_applied: true,
|
||||
created_at: expect.any(String)
|
||||
})
|
||||
expect(store.getState().stagedFilterConditions).not.toBeNull()
|
||||
expect(store.getState().stagedFilterConditions).toMatchObject([
|
||||
{
|
||||
id: expect.any(String),
|
||||
type: 'assignment-group',
|
||||
value: '1'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it(`does not derive staged assignment group filter from '0'`, async () => {
|
||||
|
@ -285,7 +256,7 @@ describe('filterState', () => {
|
|||
grading_period_id: null
|
||||
}
|
||||
store.getState().initializeStagedFilter(initialRowFilterSettings, initialColumnFilterSettings)
|
||||
expect(store.getState().stagedFilter).toBeNull()
|
||||
expect(store.getState().stagedFilterConditions.length).toStrictEqual(0)
|
||||
})
|
||||
|
||||
it('derive staged grading period filter from gradebook settings', async () => {
|
||||
|
@ -301,19 +272,14 @@ describe('filterState', () => {
|
|||
grading_period_id: '1'
|
||||
}
|
||||
store.getState().initializeStagedFilter(initialRowFilterSettings, initialColumnFilterSettings)
|
||||
expect(store.getState().stagedFilter).not.toBeNull()
|
||||
expect(store.getState().stagedFilter).toMatchObject({
|
||||
name: '',
|
||||
conditions: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
type: 'grading-period',
|
||||
value: '1'
|
||||
}
|
||||
],
|
||||
is_applied: true,
|
||||
created_at: expect.any(String)
|
||||
})
|
||||
expect(store.getState().stagedFilterConditions).not.toBeNull()
|
||||
expect(store.getState().stagedFilterConditions).toMatchObject([
|
||||
{
|
||||
id: expect.any(String),
|
||||
type: 'grading-period',
|
||||
value: '1'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('derive staged module filter from gradebook settings', async () => {
|
||||
|
@ -329,19 +295,13 @@ describe('filterState', () => {
|
|||
grading_period_id: null
|
||||
}
|
||||
store.getState().initializeStagedFilter(initialRowFilterSettings, initialColumnFilterSettings)
|
||||
expect(store.getState().stagedFilter).not.toBeNull()
|
||||
expect(store.getState().stagedFilter).toMatchObject({
|
||||
name: '',
|
||||
conditions: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
type: 'module',
|
||||
value: '1'
|
||||
}
|
||||
],
|
||||
is_applied: true,
|
||||
created_at: expect.any(String)
|
||||
})
|
||||
expect(store.getState().stagedFilterConditions).toMatchObject([
|
||||
{
|
||||
id: expect.any(String),
|
||||
type: 'module',
|
||||
value: '1'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it(`does not derive staged module filter from '0'`, async () => {
|
||||
|
@ -357,7 +317,7 @@ describe('filterState', () => {
|
|||
grading_period_id: null
|
||||
}
|
||||
store.getState().initializeStagedFilter(initialRowFilterSettings, initialColumnFilterSettings)
|
||||
expect(store.getState().stagedFilter).toBeNull()
|
||||
expect(store.getState().stagedFilterConditions.length).toStrictEqual(0)
|
||||
})
|
||||
|
||||
it('disallows multiple filters from being applied', async () => {
|
||||
|
@ -367,14 +327,12 @@ describe('filterState', () => {
|
|||
id: '321',
|
||||
name: 'filter 1',
|
||||
conditions: [],
|
||||
is_applied: false,
|
||||
created_at: '2020-01-01T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '432',
|
||||
name: 'filter 2',
|
||||
conditions: [],
|
||||
is_applied: true,
|
||||
created_at: '2020-01-02T00:00:00Z'
|
||||
}
|
||||
]
|
||||
|
@ -388,21 +346,18 @@ describe('filterState', () => {
|
|||
id: '321',
|
||||
name: 'filter 1',
|
||||
conditions: [],
|
||||
is_applied: true,
|
||||
created_at: '2020-01-01T00:00:00Z'
|
||||
})
|
||||
expect(store.getState().filters[0]).toMatchObject({
|
||||
id: '321',
|
||||
name: 'filter 1',
|
||||
conditions: [],
|
||||
is_applied: true,
|
||||
created_at: '2020-01-01T00:00:00Z'
|
||||
})
|
||||
expect(store.getState().filters[1]).toMatchObject({
|
||||
id: '432',
|
||||
name: 'filter 2',
|
||||
conditions: [],
|
||||
is_applied: false,
|
||||
created_at: '2020-01-02T00:00:00Z'
|
||||
})
|
||||
})
|
||||
|
|
|
@ -19,13 +19,13 @@
|
|||
import {SetState, GetState} from 'zustand'
|
||||
import uuid from 'uuid'
|
||||
import doFetchApi from '@canvas/do-fetch-api-effect'
|
||||
// @ts-ignore
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import type {PartialFilter, AppliedFilter, FilterCondition, Filter} from '../gradebook.d'
|
||||
import type {FilterCondition, Filter} from '../gradebook.d'
|
||||
import {
|
||||
deserializeFilter,
|
||||
compareFilterByDate,
|
||||
findAllAppliedFilterValuesOfType
|
||||
deserializeFilter,
|
||||
doFilterConditionsMatch,
|
||||
findConditionValuesOfType
|
||||
} from '../Gradebook.utils'
|
||||
import GradebookApi from '../apis/GradebookApi'
|
||||
import type {GradebookStore} from './index'
|
||||
|
@ -33,14 +33,17 @@ import type {GradebookStore} from './index'
|
|||
const I18n = useI18nScope('gradebook')
|
||||
|
||||
export type FiltersState = {
|
||||
appliedFilterConditions: FilterCondition[]
|
||||
filters: Filter[]
|
||||
stagedFilter: null | PartialFilter
|
||||
stagedFilterConditions: FilterCondition[]
|
||||
isFiltersLoading: boolean
|
||||
|
||||
appliedFilters: () => AppliedFilter[]
|
||||
applyConditions: (appliedFilterConditions: FilterCondition[]) => Promise<void>
|
||||
initializeStagedFilter: (InitialColumnFilterSettings, InitialRowFilterSettings) => void
|
||||
fetchFilters: () => Promise<void>
|
||||
saveStagedFilter: () => Promise<void>
|
||||
saveStagedFilter: (name: string) => Promise<void>
|
||||
updateStagedFilter: (filter: FilterCondition[]) => void
|
||||
deleteStagedFilter: () => void
|
||||
updateFilter: (filter: Filter) => Promise<void>
|
||||
deleteFilter: (filter: Filter) => Promise<any>
|
||||
}
|
||||
|
@ -57,31 +60,32 @@ type InitialRowFilterSettings = {
|
|||
}
|
||||
|
||||
export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): FiltersState => ({
|
||||
appliedFilterConditions: [],
|
||||
|
||||
filters: [],
|
||||
|
||||
stagedFilter: null,
|
||||
stagedFilterConditions: [],
|
||||
|
||||
isFiltersLoading: false,
|
||||
|
||||
appliedFilters: () => {
|
||||
const allFilters = get().stagedFilter ? [...get().filters, get().stagedFilter!] : get().filters
|
||||
return allFilters.filter(filter => filter.is_applied) as AppliedFilter[]
|
||||
applyConditions: async (appliedFilterConditions: FilterCondition[]) => {
|
||||
set({appliedFilterConditions})
|
||||
},
|
||||
|
||||
initializeStagedFilter: (
|
||||
initialRowFilterSettings: InitialRowFilterSettings,
|
||||
initialColumnFilterSettings: InitialColumnFilterSettings
|
||||
) => {
|
||||
const conditions: FilterCondition[] = []
|
||||
const appliedFilterConditions: FilterCondition[] = []
|
||||
|
||||
// Is section filter not represented?
|
||||
if (
|
||||
initialRowFilterSettings.section_id &&
|
||||
!findAllAppliedFilterValuesOfType('section', get().filters).includes(
|
||||
!findConditionValuesOfType('section', get().filters).includes(
|
||||
initialRowFilterSettings.section_id
|
||||
)
|
||||
) {
|
||||
conditions.push({
|
||||
appliedFilterConditions.push({
|
||||
id: uuid.v4(),
|
||||
value: initialRowFilterSettings.section_id,
|
||||
type: 'section',
|
||||
|
@ -92,11 +96,11 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
// Is student group filter not represented?
|
||||
if (
|
||||
initialRowFilterSettings.student_group_id &&
|
||||
!findAllAppliedFilterValuesOfType('student-group', get().filters).includes(
|
||||
!findConditionValuesOfType('student-group', get().filters).includes(
|
||||
initialRowFilterSettings.student_group_id
|
||||
)
|
||||
) {
|
||||
conditions.push({
|
||||
appliedFilterConditions.push({
|
||||
id: uuid.v4(),
|
||||
value: initialRowFilterSettings.student_group_id,
|
||||
type: 'student-group',
|
||||
|
@ -108,11 +112,11 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
if (
|
||||
initialColumnFilterSettings.assignment_group_id &&
|
||||
initialColumnFilterSettings.assignment_group_id !== '0' &&
|
||||
!findAllAppliedFilterValuesOfType('assignment-group', get().filters).includes(
|
||||
!findConditionValuesOfType('assignment-group', get().filters).includes(
|
||||
initialColumnFilterSettings.assignment_group_id
|
||||
)
|
||||
) {
|
||||
conditions.push({
|
||||
appliedFilterConditions.push({
|
||||
id: uuid.v4(),
|
||||
value: initialColumnFilterSettings.assignment_group_id,
|
||||
type: 'assignment-group',
|
||||
|
@ -124,11 +128,11 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
if (
|
||||
initialColumnFilterSettings.context_module_id &&
|
||||
initialColumnFilterSettings.context_module_id !== '0' &&
|
||||
!findAllAppliedFilterValuesOfType('module', get().filters).includes(
|
||||
!findConditionValuesOfType('module', get().filters).includes(
|
||||
initialColumnFilterSettings.context_module_id
|
||||
)
|
||||
) {
|
||||
conditions.push({
|
||||
appliedFilterConditions.push({
|
||||
id: uuid.v4(),
|
||||
value: initialColumnFilterSettings.context_module_id,
|
||||
type: 'module',
|
||||
|
@ -139,11 +143,11 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
// Is grading period filter not represented?
|
||||
if (
|
||||
initialColumnFilterSettings.grading_period_id &&
|
||||
!findAllAppliedFilterValuesOfType('grading-period', get().filters).includes(
|
||||
!findConditionValuesOfType('grading-period', get().filters).includes(
|
||||
initialColumnFilterSettings.grading_period_id
|
||||
)
|
||||
) {
|
||||
conditions.push({
|
||||
appliedFilterConditions.push({
|
||||
id: uuid.v4(),
|
||||
value: initialColumnFilterSettings.grading_period_id,
|
||||
type: 'grading-period',
|
||||
|
@ -151,14 +155,12 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
})
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
const stagedFilter: PartialFilter = {
|
||||
name: '',
|
||||
conditions,
|
||||
is_applied: true,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
set({stagedFilter})
|
||||
const savedFilterAlreadyMatches = get().filters.some(filter =>
|
||||
doFilterConditionsMatch(filter.conditions, appliedFilterConditions)
|
||||
)
|
||||
|
||||
if (!savedFilterAlreadyMatches) {
|
||||
set({stagedFilterConditions: appliedFilterConditions})
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -187,52 +189,58 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
})
|
||||
},
|
||||
|
||||
saveStagedFilter: async () => {
|
||||
const stagedFilter = get().stagedFilter
|
||||
if (!stagedFilter) throw new Error('staged filter is null')
|
||||
deleteStagedFilter: () => {
|
||||
const appliedFilterConditions = get().appliedFilterConditions
|
||||
const isFilterApplied = doFilterConditionsMatch(
|
||||
get().stagedFilterConditions,
|
||||
appliedFilterConditions
|
||||
)
|
||||
set({
|
||||
stagedFilterConditions: [],
|
||||
appliedFilterConditions: isFilterApplied ? [] : appliedFilterConditions
|
||||
})
|
||||
},
|
||||
|
||||
const otherFilters = get().filters.filter(f => f.id !== stagedFilter.id)
|
||||
const previouslyAppliedFilters = otherFilters.filter(f => f.is_applied)
|
||||
updateStagedFilter: (stagedFilterConditions: FilterCondition[]) => {
|
||||
const appliedFilterConditions = get().appliedFilterConditions
|
||||
const isFilterApplied = doFilterConditionsMatch(stagedFilterConditions, appliedFilterConditions)
|
||||
|
||||
if (stagedFilter.is_applied && previouslyAppliedFilters.length > 0) {
|
||||
try {
|
||||
await Promise.all(
|
||||
previouslyAppliedFilters.map(applidFilter =>
|
||||
GradebookApi.updateGradebookFilter(get().courseId, {
|
||||
...applidFilter,
|
||||
is_applied: false
|
||||
})
|
||||
)
|
||||
)
|
||||
} catch (err) {
|
||||
set({
|
||||
flashMessages: get().flashMessages.concat([
|
||||
{
|
||||
key: `filters-create-error-${Date.now()}`,
|
||||
message: I18n.t('There was an error updating a filter.'),
|
||||
variant: 'error'
|
||||
}
|
||||
])
|
||||
})
|
||||
}
|
||||
set({
|
||||
stagedFilterConditions,
|
||||
appliedFilterConditions: isFilterApplied
|
||||
? stagedFilterConditions
|
||||
: get().appliedFilterConditions
|
||||
})
|
||||
},
|
||||
|
||||
saveStagedFilter: async (name: string) => {
|
||||
const stagedFilterConditions = get().stagedFilterConditions
|
||||
if (!stagedFilterConditions.length) return
|
||||
const originalFilters = get().filters
|
||||
const stagedFilter = {
|
||||
id: uuid.v4(),
|
||||
name,
|
||||
conditions: stagedFilterConditions,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
|
||||
// optimistic update
|
||||
set({
|
||||
filters: get().filters.concat([stagedFilter]).sort(compareFilterByDate),
|
||||
stagedFilterConditions: []
|
||||
})
|
||||
|
||||
return GradebookApi.createGradebookFilter(get().courseId, stagedFilter)
|
||||
.then(response => {
|
||||
const newFilter = deserializeFilter(response.json)
|
||||
set({
|
||||
stagedFilter: null,
|
||||
filters: get()
|
||||
.filters.map(f => ({
|
||||
...f,
|
||||
is_applied: stagedFilter.is_applied ? false : f.is_applied
|
||||
}))
|
||||
.concat([newFilter])
|
||||
.sort(compareFilterByDate)
|
||||
stagedFilterConditions: [],
|
||||
filters: originalFilters.concat([newFilter]).sort(compareFilterByDate)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
set({
|
||||
stagedFilterConditions,
|
||||
flashMessages: get().flashMessages.concat([
|
||||
{
|
||||
key: `filters-create-error-${Date.now()}`,
|
||||
|
@ -247,42 +255,19 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
updateFilter: async (filter: Filter) => {
|
||||
const originalFilter = get().filters.find(f => f.id === filter.id)
|
||||
const otherFilters = get().filters.filter(f => f.id !== filter.id)
|
||||
const previouslyAppliedFilters = otherFilters.filter(f => f.is_applied)
|
||||
const appliedFilterConditions = get().appliedFilterConditions
|
||||
|
||||
const isFilterApplied = doFilterConditionsMatch(
|
||||
originalFilter?.conditions || [],
|
||||
appliedFilterConditions
|
||||
)
|
||||
|
||||
// optimistic update
|
||||
set({
|
||||
filters: otherFilters
|
||||
.map(f => ({
|
||||
...f,
|
||||
is_applied: filter.is_applied ? false : f.is_applied
|
||||
}))
|
||||
.concat([filter])
|
||||
.sort(compareFilterByDate)
|
||||
filters: otherFilters.concat([filter]).sort(compareFilterByDate),
|
||||
appliedFilterConditions: isFilterApplied ? filter.conditions : appliedFilterConditions
|
||||
})
|
||||
|
||||
if (filter.is_applied && previouslyAppliedFilters.length > 0) {
|
||||
try {
|
||||
await Promise.all(
|
||||
previouslyAppliedFilters.map(applidFilter =>
|
||||
GradebookApi.updateGradebookFilter(get().courseId, {
|
||||
...applidFilter,
|
||||
is_applied: false
|
||||
})
|
||||
)
|
||||
)
|
||||
} catch (err) {
|
||||
set({
|
||||
flashMessages: get().flashMessages.concat([
|
||||
{
|
||||
key: `filters-create-error-${Date.now()}`,
|
||||
message: I18n.t('There was an error updating a filter.'),
|
||||
variant: 'error'
|
||||
}
|
||||
])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await GradebookApi.updateGradebookFilter(get().courseId, filter)
|
||||
const updatedFilter = deserializeFilter(response.json)
|
||||
|
@ -290,7 +275,10 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
filters: get()
|
||||
.filters.filter(f => f.id !== filter.id)
|
||||
.concat([updatedFilter])
|
||||
.sort(compareFilterByDate)
|
||||
.sort(compareFilterByDate),
|
||||
appliedFilterConditions: isFilterApplied
|
||||
? updatedFilter.conditions
|
||||
: appliedFilterConditions
|
||||
})
|
||||
} catch (err) {
|
||||
// rewind
|
||||
|
@ -299,7 +287,8 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
filters: get()
|
||||
.filters.filter(f => f.id !== filter.id)
|
||||
.concat([originalFilter])
|
||||
.sort(compareFilterByDate)
|
||||
.sort(compareFilterByDate),
|
||||
appliedFilterConditions
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -316,8 +305,17 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
},
|
||||
|
||||
deleteFilter: (filter: Filter) => {
|
||||
const appliedFilterConditions = get().appliedFilterConditions
|
||||
const isFilterApplied = doFilterConditionsMatch(
|
||||
filter.conditions,
|
||||
get().appliedFilterConditions
|
||||
)
|
||||
|
||||
// Optimistic update
|
||||
set({filters: get().filters.filter(f => f.id !== filter.id)})
|
||||
set({
|
||||
filters: get().filters.filter(f => f.id !== filter.id),
|
||||
appliedFilterConditions: isFilterApplied ? [] : appliedFilterConditions
|
||||
})
|
||||
|
||||
return GradebookApi.deleteGradebookFilter(get().courseId, filter.id).catch(() => {
|
||||
// rewind
|
||||
|
@ -333,7 +331,8 @@ export default (set: SetState<GradebookStore>, get: GetState<GradebookStore>): F
|
|||
message: I18n.t('There was an error deleting "%{name}".', {name: filter.name}),
|
||||
variant: 'error'
|
||||
}
|
||||
])
|
||||
]),
|
||||
appliedFilterConditions
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue