feat: implement combobox for timezone, sound, and playlist selection in CreateTaskDialog
This commit is contained in:
@@ -1,4 +1,12 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from '@/components/ui/command'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -7,6 +15,11 @@ import {
|
|||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -26,7 +39,7 @@ import { soundsService } from '@/lib/api/services/sounds'
|
|||||||
import { playlistsService } from '@/lib/api/services/playlists'
|
import { playlistsService } from '@/lib/api/services/playlists'
|
||||||
import { getSupportedTimezones } from '@/utils/locale'
|
import { getSupportedTimezones } from '@/utils/locale'
|
||||||
import { useLocale } from '@/hooks/use-locale'
|
import { useLocale } from '@/hooks/use-locale'
|
||||||
import { CalendarPlus, Loader2, Music, PlayCircle } from 'lucide-react'
|
import { CalendarPlus, Check, ChevronsUpDown, Loader2, Music, PlayCircle } from 'lucide-react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
interface CreateTaskDialogProps {
|
interface CreateTaskDialogProps {
|
||||||
@@ -76,6 +89,11 @@ export function CreateTaskDialog({
|
|||||||
const [loadingSounds, setLoadingSounds] = useState(false)
|
const [loadingSounds, setLoadingSounds] = useState(false)
|
||||||
const [loadingPlaylists, setLoadingPlaylists] = useState(false)
|
const [loadingPlaylists, setLoadingPlaylists] = useState(false)
|
||||||
|
|
||||||
|
// Combobox state
|
||||||
|
const [soundComboOpen, setSoundComboOpen] = useState(false)
|
||||||
|
const [playlistComboOpen, setPlaylistComboOpen] = useState(false)
|
||||||
|
const [timezoneComboOpen, setTimezoneComboOpen] = useState(false)
|
||||||
|
|
||||||
// Load sounds and playlists when dialog opens
|
// Load sounds and playlists when dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -191,6 +209,9 @@ export function CreateTaskDialog({
|
|||||||
setSelectedPlaylistId('')
|
setSelectedPlaylistId('')
|
||||||
setPlayMode('continuous')
|
setPlayMode('continuous')
|
||||||
setShuffle(false)
|
setShuffle(false)
|
||||||
|
setSoundComboOpen(false)
|
||||||
|
setPlaylistComboOpen(false)
|
||||||
|
setTimezoneComboOpen(false)
|
||||||
onCancel()
|
onCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,22 +297,61 @@ export function CreateTaskDialog({
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="timezone">Timezone</Label>
|
<Label htmlFor="timezone">Timezone</Label>
|
||||||
<Select
|
<Popover open={timezoneComboOpen} onOpenChange={setTimezoneComboOpen}>
|
||||||
value={formData.timezone}
|
<PopoverTrigger asChild>
|
||||||
onValueChange={(value) => setFormData(prev => ({ ...prev, timezone: value }))}
|
<Button
|
||||||
>
|
variant="outline"
|
||||||
<SelectTrigger>
|
role="combobox"
|
||||||
<SelectValue />
|
aria-expanded={timezoneComboOpen}
|
||||||
</SelectTrigger>
|
className="w-full justify-between"
|
||||||
<SelectContent>
|
>
|
||||||
<SelectItem value="UTC">UTC</SelectItem>
|
{formData.timezone?.replace('_', ' ') || 'UTC'}
|
||||||
{getSupportedTimezones().map((tz) => (
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<SelectItem key={tz} value={tz}>
|
</Button>
|
||||||
{tz.replace('_', ' ')}
|
</PopoverTrigger>
|
||||||
</SelectItem>
|
<PopoverContent className="w-full p-0" style={{ WebkitOverflowScrolling: 'touch' } as React.CSSProperties}>
|
||||||
))}
|
<Command onWheel={(e) => e.stopPropagation()}>
|
||||||
</SelectContent>
|
<CommandInput placeholder="Search timezone..." />
|
||||||
</Select>
|
<CommandList className="max-h-[200px] overflow-y-auto overscroll-contain" style={{ touchAction: 'pan-y' }}>
|
||||||
|
<CommandEmpty>No timezone found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
key="UTC"
|
||||||
|
value="UTC"
|
||||||
|
onSelect={() => {
|
||||||
|
setFormData(prev => ({ ...prev, timezone: 'UTC' }))
|
||||||
|
setTimezoneComboOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>UTC</span>
|
||||||
|
<Check
|
||||||
|
className={`ml-auto h-4 w-4 ${
|
||||||
|
formData.timezone === 'UTC' ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
{getSupportedTimezones().map((tz) => (
|
||||||
|
<CommandItem
|
||||||
|
key={tz}
|
||||||
|
value={tz.replace('_', ' ')}
|
||||||
|
onSelect={() => {
|
||||||
|
setFormData(prev => ({ ...prev, timezone: tz }))
|
||||||
|
setTimezoneComboOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{tz.replace('_', ' ')}</span>
|
||||||
|
<Check
|
||||||
|
className={`ml-auto h-4 w-4 ${
|
||||||
|
formData.timezone === tz ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -358,24 +418,56 @@ export function CreateTaskDialog({
|
|||||||
{formData.task_type === 'play_sound' && (
|
{formData.task_type === 'play_sound' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="sound">Sound to Play</Label>
|
<Label htmlFor="sound">Sound to Play</Label>
|
||||||
<Select
|
<Popover open={soundComboOpen} onOpenChange={setSoundComboOpen}>
|
||||||
value={selectedSoundId}
|
<PopoverTrigger asChild>
|
||||||
onValueChange={setSelectedSoundId}
|
<Button
|
||||||
>
|
variant="outline"
|
||||||
<SelectTrigger>
|
role="combobox"
|
||||||
<SelectValue placeholder={loadingSounds ? "Loading sounds..." : "Select a sound"} />
|
aria-expanded={soundComboOpen}
|
||||||
</SelectTrigger>
|
className="w-full justify-between"
|
||||||
<SelectContent>
|
disabled={loadingSounds}
|
||||||
{sounds.map((sound) => (
|
>
|
||||||
<SelectItem key={sound.id} value={sound.id.toString()}>
|
{selectedSoundId
|
||||||
<div className="flex items-center gap-2">
|
? sounds.find((sound) => sound.id.toString() === selectedSoundId)?.name ||
|
||||||
<Music className="h-4 w-4" />
|
sounds.find((sound) => sound.id.toString() === selectedSoundId)?.filename ||
|
||||||
<span>{sound.name || sound.filename}</span>
|
"Selected sound"
|
||||||
</div>
|
: loadingSounds
|
||||||
</SelectItem>
|
? "Loading sounds..."
|
||||||
))}
|
: "Select a sound"}
|
||||||
</SelectContent>
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Select>
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0" style={{ WebkitOverflowScrolling: 'touch' } as React.CSSProperties}>
|
||||||
|
<Command onWheel={(e) => e.stopPropagation()}>
|
||||||
|
<CommandInput placeholder="Search sounds..." />
|
||||||
|
<CommandList className="max-h-[200px] overflow-y-auto overscroll-contain" style={{ touchAction: 'pan-y' }}>
|
||||||
|
<CommandEmpty>No sound found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{sounds.map((sound) => (
|
||||||
|
<CommandItem
|
||||||
|
key={sound.id}
|
||||||
|
value={`${sound.id}-${sound.name || sound.filename}`}
|
||||||
|
onSelect={() => {
|
||||||
|
setSelectedSoundId(sound.id.toString())
|
||||||
|
setSoundComboOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Music className="h-4 w-4" />
|
||||||
|
<span>{sound.name || sound.filename}</span>
|
||||||
|
</div>
|
||||||
|
<Check
|
||||||
|
className={`ml-auto h-4 w-4 ${
|
||||||
|
selectedSoundId === sound.id.toString() ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
{parametersError && (
|
{parametersError && (
|
||||||
<p className="text-sm text-destructive">{parametersError}</p>
|
<p className="text-sm text-destructive">{parametersError}</p>
|
||||||
)}
|
)}
|
||||||
@@ -386,24 +478,55 @@ export function CreateTaskDialog({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="playlist">Playlist to Play</Label>
|
<Label htmlFor="playlist">Playlist to Play</Label>
|
||||||
<Select
|
<Popover open={playlistComboOpen} onOpenChange={setPlaylistComboOpen}>
|
||||||
value={selectedPlaylistId}
|
<PopoverTrigger asChild>
|
||||||
onValueChange={setSelectedPlaylistId}
|
<Button
|
||||||
>
|
variant="outline"
|
||||||
<SelectTrigger>
|
role="combobox"
|
||||||
<SelectValue placeholder={loadingPlaylists ? "Loading playlists..." : "Select a playlist"} />
|
aria-expanded={playlistComboOpen}
|
||||||
</SelectTrigger>
|
className="w-full justify-between"
|
||||||
<SelectContent>
|
disabled={loadingPlaylists}
|
||||||
{playlists.map((playlist) => (
|
>
|
||||||
<SelectItem key={playlist.id} value={playlist.id.toString()}>
|
{selectedPlaylistId
|
||||||
<div className="flex items-center gap-2">
|
? playlists.find((playlist) => playlist.id.toString() === selectedPlaylistId)?.name ||
|
||||||
<PlayCircle className="h-4 w-4" />
|
"Selected playlist"
|
||||||
<span>{playlist.name}</span>
|
: loadingPlaylists
|
||||||
</div>
|
? "Loading playlists..."
|
||||||
</SelectItem>
|
: "Select a playlist"}
|
||||||
))}
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</SelectContent>
|
</Button>
|
||||||
</Select>
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0" style={{ WebkitOverflowScrolling: 'touch' } as React.CSSProperties}>
|
||||||
|
<Command onWheel={(e) => e.stopPropagation()}>
|
||||||
|
<CommandInput placeholder="Search playlists..." />
|
||||||
|
<CommandList className="max-h-[200px] overflow-y-auto overscroll-contain" style={{ touchAction: 'pan-y' }}>
|
||||||
|
<CommandEmpty>No playlist found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{playlists.map((playlist) => (
|
||||||
|
<CommandItem
|
||||||
|
key={playlist.id}
|
||||||
|
value={`${playlist.id}-${playlist.name}`}
|
||||||
|
onSelect={() => {
|
||||||
|
setSelectedPlaylistId(playlist.id.toString())
|
||||||
|
setPlaylistComboOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<PlayCircle className="h-4 w-4" />
|
||||||
|
<span>{playlist.name}</span>
|
||||||
|
</div>
|
||||||
|
<Check
|
||||||
|
className={`ml-auto h-4 w-4 ${
|
||||||
|
selectedPlaylistId === playlist.id.toString() ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user