diff --git a/ui/features/gradebook/react/default_gradebook/Gradebook.utils.ts b/ui/features/gradebook/react/default_gradebook/Gradebook.utils.ts index 804c75ab491..c9786786d6d 100644 --- a/ui/features/gradebook/react/default_gradebook/Gradebook.utils.ts +++ b/ui/features/gradebook/react/default_gradebook/Gradebook.utils.ts @@ -26,13 +26,20 @@ import _ from 'lodash' import htmlEscape from 'html-escape' import type { Assignment, + AssignmentGroup, Filter, + FilterCondition, FilterConditionType, - GradebookFilterApiResponse, GradebookFilterApiRequest, + GradebookFilterApiResponse, + GradingPeriod, + Module, PartialFilter, Section, SectionMap, + StudentGroup, + StudentGroupCategory, + StudentGroupCategoryMap, Submission } from './gradebook.d' @@ -195,20 +202,30 @@ export function getAllAppliedFilterValues(filters: Filter[]) { .map(c => c.value) } +export const conditionTypes: FilterConditionType[] = [ + 'section', + 'module', + 'assignment-group', + 'grading-period', + 'student-group', + 'start-date', + 'end-date', + 'submissions' +] + // Extra normalization; comes from jsonb payload export const deserializeFilter = (json: GradebookFilterApiResponse): Filter => { const filter = json.gradebook_filter if (!filter.id || typeof filter.id !== 'string') throw new Error('invalid filter id') if (!Array.isArray(filter.payload.conditions)) throw new Error('invalid filter conditions') - const conditions = filter.payload.conditions.map(c => { - if (!c || typeof c.id !== 'string') throw new Error('invalid condition id') - return { + const conditions = filter.payload.conditions + .filter(c => c && (typeof c.type === 'undefined' || conditionTypes.includes(c.type))) + .map(c => ({ id: c.id, type: c.type, value: c.value, created_at: String(c.created_at) - } - }) + })) return { id: filter.id, name: String(filter.name), @@ -230,3 +247,65 @@ export const serializeFilter = (filter: PartialFilter): GradebookFilterApiReques export const compareFilterByDate = (a: Filter, b: Filter) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + +export const getLabelForFilterCondition = ( + condition: FilterCondition, + assignmentGroups: Pick[], + gradingPeriods: Pick[], + modules: Pick[], + sections: Pick[], + studentGroupCategories: StudentGroupCategoryMap +) => { + if (!condition.type) throw new Error('missing condition type') + + if (condition.type === 'section') { + return sections.find(s => s.id === condition.value)?.name || I18n.t('Section') + } else if (condition.type === 'module') { + return modules.find(m => m.id === condition.value)?.name || I18n.t('Module') + } else if (condition.type === 'assignment-group') { + return assignmentGroups.find(a => a.id === condition.value)?.name || I18n.t('Assignment Group') + } else if (condition.type === 'grading-period') { + return gradingPeriods.find(g => g.id === condition.value)?.title || I18n.t('Grading Period') + } else if (condition.type === 'student-group') { + const studentGroups: StudentGroup[] = Object.values(studentGroupCategories) + .map((c: StudentGroupCategory) => c.groups) + .flat() + return ( + studentGroups.find((g: StudentGroup) => g.id === condition.value)?.name || + I18n.t('Student Group') + ) + } else if (condition.type === 'submissions') { + if (condition.value === 'has-ungraded-submissions') { + return I18n.t('Has ungraded submissions') + } else if (condition.value === 'has-submissions') { + return I18n.t('Has submissions') + } else { + throw new Error('invalid submissions condition value') + } + } else if (condition.type === 'start-date') { + const options: any = { + year: 'numeric', + month: 'numeric', + day: 'numeric' + } + if (typeof condition.value !== 'string') throw new Error('invalid start-date value') + const value = Intl.DateTimeFormat(I18n.currentLocale(), options).format( + new Date(condition.value) + ) + return I18n.t('Start Date %{value}', {value}) + } else if (condition.type === 'end-date') { + const options: any = { + year: 'numeric', + month: 'numeric', + day: 'numeric' + } + if (typeof condition.value !== 'string') throw new Error('invalid end-date value') + const value = Intl.DateTimeFormat(I18n.currentLocale(), options).format( + new Date(condition.value) + ) + return I18n.t('End Date %{value}', {value}) + } + + // unrecognized types should have been filtered out by deserializeFilter + throw new Error('invalid condition type') +} diff --git a/ui/features/gradebook/react/default_gradebook/components/FilterNav.tsx b/ui/features/gradebook/react/default_gradebook/components/FilterNav.tsx index 134b1cf4fa8..7e7db0e543a 100644 --- a/ui/features/gradebook/react/default_gradebook/components/FilterNav.tsx +++ b/ui/features/gradebook/react/default_gradebook/components/FilterNav.tsx @@ -22,6 +22,7 @@ import {AccessibleContent} from '@instructure/ui-a11y-content' import uuid from 'uuid' // @ts-ignore import I18n from 'i18n!gradebook' +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' @@ -40,6 +41,7 @@ import type { Section, StudentGroupCategoryMap } from '../gradebook.d' +import {getLabelForFilterCondition} from '../Gradebook.utils' import useStore from '../stores/index' const {Item: FlexItem} = Flex as any @@ -79,34 +81,69 @@ export default function FilterNav({ const saveStagedFilter = useStore(state => state.saveStagedFilter) const updateFilter = useStore(state => state.updateFilter) const deleteFilter = useStore(state => state.deleteFilter) + 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
{label}
+ }) + return ( - {filter.name}} - dismissible - onClick={() => updateFilter({...filter, is_applied: false})} - margin="0 xx-small 0 0" - /> + + {filter.name} + } + dismissible + onClick={() => updateFilter({...filter, is_applied: false})} + margin="0 xx-small 0 0" + /> + ) }) - if (stagedFilter) { - filterComponents.push( - - {stagedFilter.name || I18n.t('Unnamed Filter')} - - } - dismissible - onClick={() => useStore.setState({stagedFilter: null})} - 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( + {label}} + dismissible + onClick={() => + useStore.setState({ + stagedFilter: { + ...stagedFilter, + conditions: stagedFilter.conditions.filter(c => c.id !== condition.id) + } + }) + } + margin="0 xx-small 0 0" + /> + ) + }) } return ( diff --git a/ui/features/gradebook/react/default_gradebook/components/FilterNavCondition.tsx b/ui/features/gradebook/react/default_gradebook/components/FilterNavCondition.tsx index 6e528dabcd9..bf652b030be 100644 --- a/ui/features/gradebook/react/default_gradebook/components/FilterNavCondition.tsx +++ b/ui/features/gradebook/react/default_gradebook/components/FilterNavCondition.tsx @@ -42,7 +42,7 @@ const {Option, Group: OptionGroup} = SimpleSelect as any const formatDate = date => tz.format(date, 'date.formats.medium') const dateLabels = {'start-date': I18n.t('Start Date'), 'end-date': I18n.t('End Date')} -type SubmissionTypeOption = [string, string] +type SubmissionTypeOption = ['has-ungraded-submissions' | 'has-submissions', string] const submissionTypeOptions: SubmissionTypeOption[] = [ ['has-ungraded-submissions', I18n.t('Has ungraded submissions')], diff --git a/ui/features/gradebook/react/default_gradebook/components/FilterNavFilter.tsx b/ui/features/gradebook/react/default_gradebook/components/FilterNavFilter.tsx index ffe3fab0860..31b38e44039 100644 --- a/ui/features/gradebook/react/default_gradebook/components/FilterNavFilter.tsx +++ b/ui/features/gradebook/react/default_gradebook/components/FilterNavFilter.tsx @@ -221,7 +221,7 @@ export default function FilterNavFilter({ { 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() + } + }) + const {getAllByTestId} = render() + expect(await getAllByTestId('staged-filter-condition-tag')[0]).toHaveTextContent('Module 1') + }) + it('opens tray', () => { const {getByText, getByRole} = render() userEvent.click(getByText('Filters')) diff --git a/ui/global.d.ts b/ui/global.d.ts index 9235ad8c415..0ffdbb759e0 100644 --- a/ui/global.d.ts +++ b/ui/global.d.ts @@ -62,6 +62,7 @@ declare global { declare interface Array { flatMap: (callback: (value: T, index: number, array: T[]) => Y[]) => Y[] + flat: (depth?: number) => Y[] } declare interface Object {