Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion strr-examiner-web/app/components/Table/Header/Select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const props = defineProps<{
label: string
value: any
disabled?: boolean
childStatuses?: any[]
}>
sort?: TableSort
searchable?: boolean
Expand Down Expand Up @@ -40,6 +41,53 @@ const clearFilter = () => {
filterModel.value = props.default ?? []
}

// parent-child toggle logic for grouped options
const parentOptions = computed(() => props.options.filter(option => option.childStatuses?.length))
const isUpdatingFilter = ref(false)

// used to style the child statuses
const isChildStatus = (value: any) => parentOptions.value.some(p => p.childStatuses?.includes(value))

// Sync parent/child checkboxes: toggling a parent checks/unchecks all its children,
// and checking all children automatically checks the parent (and vice versa).
// isUpdatingFilter prevents the watcher from re-triggering on its own updates.
watch(filterModel, (newVal, oldVal) => {
const parents = parentOptions.value
if (isUpdatingFilter.value || !parents.length) { return } // skip re-entrant calls and flat option lists
isUpdatingFilter.value = true

const prev = new Set(oldVal) // previous selection, used to detect what changed
const result = new Set(newVal) // mutable working copy of the new selection

for (const { value: parentVal, childStatuses } of parents) {
const parentChecked = result.has(parentVal) && !prev.has(parentVal)
const parentUnchecked = !result.has(parentVal) && prev.has(parentVal)
const allChildrenSelected = childStatuses!.every(status => result.has(status))

if (parentChecked) {
childStatuses!.forEach(status => result.add(status)) // check all child statuses
} else if (parentUnchecked) {
childStatuses!.forEach(status => result.delete(status)) // uncheck all child statuses
} else if (allChildrenSelected) {
result.add(parentVal) // if all children checked - auto-check parent
} else {
result.delete(parentVal) // if some child unchecked - uncheck parent
}
}

filterModel.value = [...result]
nextTick(() => { isUpdatingFilter.value = false }) // wait for DOM updates before re-enabling the watcher
})

// used to check if any child statuses selected to set indeterminate state for ui checkbox
const isPartiallySelected = (option: { childStatuses?: any[] }) => {
if (!option.childStatuses?.length) { return false }
const selected = new Set(filterModel.value)
const someSelected = option.childStatuses.some(status => selected.has(status))
const allSelected = option.childStatuses.every(status => selected.has(status))
return someSelected && !allSelected
}

const filterColumnRef = ref<any>(null)
const initialWidth = ref<string>('auto')

Expand Down Expand Up @@ -82,7 +130,6 @@ onMounted(() => {
clear-search-on-close
>
<template #default="{ open }">
<!-- TODO: aria labels? -->
<UButton
ref="filterColumnRef"
variant="select_menu_trigger"
Expand Down Expand Up @@ -125,9 +172,11 @@ onMounted(() => {
<div
v-else
class="flex cursor-pointer items-center gap-1 p-1"
:class="{ 'pl-5': isChildStatus(option.value) }"
>
<UCheckbox
:model-value="selected"
:indeterminate="isPartiallySelected(option)"
class="pointer-events-none"
:ui="{
base: 'h-4 w-4',
Expand Down
40 changes: 26 additions & 14 deletions strr-examiner-web/app/pages/dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -536,23 +536,35 @@ function handleColumnSort (column: string) {
}

// Limit status options to the active table to prevent mixed-status filters.
const applicationStatusOptions: { label: string; value: any; disabled?: boolean }[] = [
{ label: 'Application Status', value: undefined, disabled: true },
const applicationStatusOptions: {
label: string,
value: any,
disabled?: boolean,
childStatuses?: any[]
}[] = [
{
label: 'Open',
value: 'OPEN',
childStatuses: [
ApplicationStatus.FULL_REVIEW,
ApplicationStatus.NOC_PENDING,
ApplicationStatus.NOC_EXPIRED
]
},
{ label: 'Full Review', value: ApplicationStatus.FULL_REVIEW },
{ label: 'Provisional Review', value: ApplicationStatus.PROVISIONAL_REVIEW },
{ label: 'Payment Due', value: ApplicationStatus.PAYMENT_DUE },
{ label: 'Provisional', value: ApplicationStatus.PROVISIONAL },
{ label: 'Paid', value: ApplicationStatus.PAID },
{ label: 'Additional Info Requested', value: ApplicationStatus.ADDITIONAL_INFO_REQUESTED },
{ label: 'Provisionally Approved', value: ApplicationStatus.PROVISIONALLY_APPROVED },
{ label: 'Declined', value: ApplicationStatus.DECLINED },
{ label: 'Provisionally Declined', value: ApplicationStatus.PROVISIONALLY_DECLINED },
{ label: 'Auto Approved', value: ApplicationStatus.AUTO_APPROVED },
{ label: 'Full Review Approved', value: ApplicationStatus.FULL_REVIEW_APPROVED },
{ label: 'NOC - Pending', value: ApplicationStatus.NOC_PENDING },
{ label: 'NOC - Expired', value: ApplicationStatus.NOC_EXPIRED },
{ label: 'NOC - Pending - Provisional', value: ApplicationStatus.PROVISIONAL_REVIEW_NOC_PENDING },
{ label: 'NOC - Expired - Provisional', value: ApplicationStatus.PROVISIONAL_REVIEW_NOC_EXPIRED }
{ label: 'Closed', value: ApplicationStatus.DECLINED },
{
label: 'Not Submitted',
value: 'NOT_SUBMITTED',
childStatuses: [
ApplicationStatus.PAYMENT_DUE,
ApplicationStatus.DRAFT
]
},
{ label: 'Payment Due', value: ApplicationStatus.PAYMENT_DUE },
{ label: 'Draft', value: ApplicationStatus.DRAFT }
]

const registrationStatusOptions: { label: string; value: any; disabled?: boolean }[] = [
Expand Down
5 changes: 3 additions & 2 deletions strr-examiner-web/app/stores/examiner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,14 @@ export const useExaminerStore = defineStore('strr/examiner-store', () => {
*/
const processStatusFilters = (statusFilters: any[]) => {
const regStatus: any[] = []
// Separate application and registration statuses
// Separate application and registration statuses, excluding UI grouping labels
const statusValue = statusFilters.filter((status) => {
if (Object.values(RegistrationStatus).includes(status as any)) {
regStatus.push(status)
return false
}
return true
// filter out any statuses that are not in ApplicationStatus (for grouping labels)
return Object.values(ApplicationStatus).includes(status as any)
})
// Start with default statuses list and these will be
// provided if not status selected in filter
Expand Down
2 changes: 1 addition & 1 deletion strr-examiner-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "strr-examiner-web",
"private": true,
"type": "module",
"version": "0.2.15",
"version": "0.2.16",
"scripts": {
"build-check": "nuxt build",
"build": "nuxt generate",
Expand Down
Loading