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:
Aaron Shafovaloff 2022-04-11 12:50:30 -05:00
parent c58f903dc0
commit 0362552610
16 changed files with 365 additions and 419 deletions

View File

@ -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) {

View File

@ -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)
)
)
}

View File

@ -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}

View File

@ -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')

View File

@ -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
)
})
})

View File

@ -92,6 +92,7 @@ export const defaultGradebookEnv = {
}
export const defaultGradebookProps = {
appliedFilterConditions: [],
filters: [],
isFiltersLoading: false,
onFiltersChange: () => {},

View File

@ -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'

View File

@ -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"
>

View File

@ -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'

View File

@ -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"

View File

@ -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(() => {

View File

@ -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: [

View File

@ -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

View File

@ -72,4 +72,5 @@ export type GridDisplaySettings = {
viewUngradedAsZero: boolean
showUnpublishedAssignments: boolean
showSeparateFirstLastNames: boolean
hideAssignmentGroupTotals: boolean
}

View File

@ -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'
})
})

View File

@ -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
})
})
}