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
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import { PageContainer } from '@/components/PageContainer'

import { useMemo, useState } from 'react'
import { Loader2, Search, List, Columns } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Search, List, Columns } from 'lucide-react'
import {
Select,
SelectContent,
Expand All @@ -26,7 +25,7 @@ import { pickDefaultCycleId, useCycles } from '@/lib/queries/cycles'
import { useQuestionsByCycleRoles } from '@/lib/queries/questions'
import { useCurrentUser } from '@/lib/queries/users'
import { ROLE_COLUMNS, ROLE_LABEL } from '@/lib/roles'
import { ORDERED_STAGES, stageLabel } from './constants'
import { BulkActionBar } from './BulkActionBar'
import {
AVAILABILITY_DAY_OPTIONS,
availabilityOptionsFor,
Expand Down Expand Up @@ -218,6 +217,14 @@ export function ApplicationsClient() {
})
}

// Dropping the selection also drops the bar's transient state, so a failure
// notice from a previous batch can't linger into the next one.
function clearSelection() {
setSelectedIds(new Set())
setBulkStage('')
setBulkFailed(0)
}

async function applyBulkStage() {
if (!bulkStage || selectedIds.size === 0) return
setApplyingBulk(true)
Expand Down Expand Up @@ -352,49 +359,6 @@ export function ApplicationsClient() {
</div>
</div>

{view === 'table' && isChief && (
<div className="flex flex-wrap items-center gap-3 rounded-xl border border-gray-100 bg-white p-4">
<span className="text-text-faint text-sm">
{selectedIds.size} selected
</span>
{bulkFailed > 0 && (
<span className="text-destructive text-sm">
{bulkFailed} update{bulkFailed === 1 ? '' : 's'} failed
</span>
)}
<div className="ml-auto flex items-center gap-2">
<Select
value={bulkStage}
onValueChange={(val) => setBulkStage(val as ApplicationStage)}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="Move to stage…" />
</SelectTrigger>
<SelectContent>
{ORDERED_STAGES.map((stage) => (
<SelectItem key={stage} value={stage}>
{stageLabel[stage]}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
onClick={applyBulkStage}
disabled={!bulkStage || selectedIds.size === 0 || applyingBulk}
>
{applyingBulk ? (
<>
<Loader2 className="animate-spin" size={14} />
Updating…
</>
) : (
`Move ${selectedIds.size || ''} selected`
)}
</Button>
</div>
</div>
)}

<div className="flex min-h-0 flex-1 flex-col">
{view === 'table' ? (
<TableView
Expand Down Expand Up @@ -422,6 +386,19 @@ export function ApplicationsClient() {
)
}
}}
bulkBar={
isChief && selectedIds.size > 0 ? (
<BulkActionBar
selectedCount={selectedIds.size}
stage={bulkStage}
onStageChange={setBulkStage}
onApply={applyBulkStage}
onClear={clearSelection}
applying={applyingBulk}
failedCount={bulkFailed}
/>
) : undefined
}
/>
) : (
<KanbanView
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use client'

import { Loader2, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import type { ApplicationStage } from '@/lib/api/types'
import { ORDERED_STAGES, stageLabel } from './constants'

// Takes over the table's filter row while rows are selected, so bulk actions
// appear where the selection was made instead of as a permanently docked bar
// that reads "0 selected" most of the time. Sized to match the filter chips it
// replaces so the row doesn't jump on the swap.
export function BulkActionBar({
selectedCount,
stage,
onStageChange,
onApply,
onClear,
applying,
failedCount,
}: {
selectedCount: number
stage: ApplicationStage | ''
onStageChange: (stage: ApplicationStage) => void
onApply: () => void
onClear: () => void
applying: boolean
failedCount: number
}) {
return (
<div className="flex min-h-7 flex-wrap items-center gap-2">
<span className="text-text-default text-sm font-medium">
{selectedCount} selected
</span>
<button
onClick={onClear}
className="text-text-muted hover:text-text-default inline-flex h-7 items-center gap-1 rounded-md px-1.5 text-sm transition-colors hover:bg-gray-100"
>
<X className="h-3.5 w-3.5" />
Clear
</button>
{failedCount > 0 && (
<span className="text-destructive text-sm">
{failedCount} update{failedCount === 1 ? '' : 's'} failed
</span>
)}
<div className="ml-auto flex items-center gap-2">
<Select
value={stage}
onValueChange={(val) => onStageChange(val as ApplicationStage)}
>
<SelectTrigger className="h-7 w-44 rounded-md px-2 text-sm">
<SelectValue placeholder="Move to stage…" />
</SelectTrigger>
<SelectContent>
{ORDERED_STAGES.map((s) => (
<SelectItem key={s} value={s}>
{stageLabel[s]}
</SelectItem>
))}
</SelectContent>
</Select>
<Button size="sm" onClick={onApply} disabled={!stage || applying}>
{applying ? (
<>
<Loader2 className="animate-spin" size={14} />
Updating…
</>
) : (
`Move ${selectedCount}`
)}
</Button>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function TableView({
onSelectApplication,
filters,
onFilterChange,
bulkBar,
}: {
applicants: ApplicantApplication[]
allApplicants: ApplicantApplication[]
Expand All @@ -48,6 +49,9 @@ export function TableView({
filter: AnswerFilter | null,
action: 'add' | 'remove'
) => void
// Rendered in the filter row's place while a selection is active. Owned by
// the parent, which holds the selection and the bulk mutation.
bulkBar?: React.ReactNode
}) {
const countByStage = (stage: ApplicationStage | 'all') =>
stage === 'all'
Expand All @@ -72,11 +76,13 @@ export function TableView({
return (
<div className="flex min-h-0 flex-1 flex-col rounded-lg border border-gray-200 bg-white">
<div className="space-y-3 border-b border-gray-100 px-4 py-3">
<FilterChips
filters={filters}
columns={columns}
onFilterChange={onFilterChange}
/>
{bulkBar ?? (
<FilterChips
filters={filters}
columns={columns}
onFilterChange={onFilterChange}
/>
)}
<div className="flex shrink-0 items-center gap-1 overflow-x-auto">
{FILTER_STAGES.map(({ label, value }) => (
<button
Expand Down
Loading