Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files. The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
255 lines
7.5 KiB
TypeScript
Vendored
255 lines
7.5 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import { useState } from 'react'
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
import { type Table } from '@tanstack/react-table'
|
|
import { Power, PowerOff, Tag, Trash2 } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip'
|
|
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
|
|
import {
|
|
handleBatchDelete,
|
|
handleBatchDisable,
|
|
handleBatchEnable,
|
|
handleBatchSetTag,
|
|
} from '../lib'
|
|
import type { Channel } from '../types'
|
|
|
|
interface DataTableBulkActionsProps<TData> {
|
|
table: Table<TData>
|
|
}
|
|
|
|
export function DataTableBulkActions<TData>({
|
|
table,
|
|
}: DataTableBulkActionsProps<TData>) {
|
|
const { t } = useTranslation()
|
|
const queryClient = useQueryClient()
|
|
const [showTagDialog, setShowTagDialog] = useState(false)
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
|
const [tagValue, setTagValue] = useState('')
|
|
|
|
const selectedRows = table.getFilteredSelectedRowModel().rows
|
|
const selectedIds = selectedRows.reduce<number[]>((ids, row) => {
|
|
const id = (row.original as Channel).id
|
|
|
|
if (typeof id === 'number') {
|
|
ids.push(id)
|
|
}
|
|
|
|
return ids
|
|
}, [])
|
|
|
|
const handleClearSelection = () => {
|
|
table.resetRowSelection()
|
|
}
|
|
|
|
const handleEnableAll = () => {
|
|
handleBatchEnable(selectedIds, queryClient, handleClearSelection)
|
|
}
|
|
|
|
const handleDisableAll = () => {
|
|
handleBatchDisable(selectedIds, queryClient, handleClearSelection)
|
|
}
|
|
|
|
const handleDeleteAll = () => {
|
|
handleBatchDelete(selectedIds, queryClient, () => {
|
|
setShowDeleteConfirm(false)
|
|
handleClearSelection()
|
|
})
|
|
}
|
|
|
|
const handleSetTag = () => {
|
|
handleBatchSetTag(selectedIds, tagValue || null, queryClient, () => {
|
|
setShowTagDialog(false)
|
|
setTagValue('')
|
|
handleClearSelection()
|
|
})
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<BulkActionsToolbar table={table} entityName='channel'>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<Button
|
|
variant='outline'
|
|
size='icon'
|
|
onClick={handleEnableAll}
|
|
className='size-8'
|
|
aria-label={t('Enable selected channels')}
|
|
title={t('Enable selected channels')}
|
|
/>
|
|
}
|
|
>
|
|
<Power />
|
|
<span className='sr-only'>{t('Enable selected channels')}</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>{t('Enable selected channels')}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<Button
|
|
variant='outline'
|
|
size='icon'
|
|
onClick={handleDisableAll}
|
|
className='size-8'
|
|
aria-label={t('Disable selected channels')}
|
|
title={t('Disable selected channels')}
|
|
/>
|
|
}
|
|
>
|
|
<PowerOff />
|
|
<span className='sr-only'>{t('Disable selected channels')}</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>{t('Disable selected channels')}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<Button
|
|
variant='outline'
|
|
size='icon'
|
|
onClick={() => setShowTagDialog(true)}
|
|
className='size-8'
|
|
aria-label={t('Set tag for selected channels')}
|
|
title={t('Set tag for selected channels')}
|
|
/>
|
|
}
|
|
>
|
|
<Tag />
|
|
<span className='sr-only'>
|
|
{t('Set tag for selected channels')}
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>{t('Set tag for selected channels')}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<Button
|
|
variant='destructive'
|
|
size='icon'
|
|
onClick={() => setShowDeleteConfirm(true)}
|
|
className='size-8'
|
|
aria-label={t('Delete selected channels')}
|
|
title={t('Delete selected channels')}
|
|
/>
|
|
}
|
|
>
|
|
<Trash2 />
|
|
<span className='sr-only'>{t('Delete selected channels')}</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>{t('Delete selected channels')}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</BulkActionsToolbar>
|
|
|
|
{/* Set Tag Dialog */}
|
|
<Dialog open={showTagDialog} onOpenChange={setShowTagDialog}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{t('Set Tag')}</DialogTitle>
|
|
<DialogDescription>
|
|
{t('Set a tag for')} {selectedIds.length}{' '}
|
|
{t('selected channel(s). Leave empty to remove tag.')}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className='grid gap-4 py-4'>
|
|
<div className='grid gap-2'>
|
|
<Label htmlFor='tag'>{t('Tag')}</Label>
|
|
<Input
|
|
id='tag'
|
|
placeholder={t('Enter tag name (optional)')}
|
|
value={tagValue}
|
|
onChange={(e) => setTagValue(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant='outline'
|
|
onClick={() => {
|
|
setShowTagDialog(false)
|
|
setTagValue('')
|
|
}}
|
|
>
|
|
{t('Cancel')}
|
|
</Button>
|
|
<Button onClick={handleSetTag}>{t('Set Tag')}</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{t('Delete Channels?')}</DialogTitle>
|
|
<DialogDescription>
|
|
{t('Are you sure you want to delete')} {selectedIds.length}{' '}
|
|
{t('channel(s)? This action cannot be undone.')}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant='outline'
|
|
onClick={() => setShowDeleteConfirm(false)}
|
|
>
|
|
{t('Cancel')}
|
|
</Button>
|
|
<Button variant='destructive' onClick={handleDeleteAll}>
|
|
{t('Delete')}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
}
|