feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
1 parent
aa6c3f50f4
commit
ceddfd3d6b
828 files changed
+122253
-13702
No files matched your search
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
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 {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnFiltersState,
|
||||
} from '@tanstack/react-table'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import { DataTableToolbar } from '../toolbar'
|
||||
|
||||
const rows = [{ name: 'Channel', model: 'model', status: 'enabled' }]
|
||||
const filters = [
|
||||
{
|
||||
columnId: 'status',
|
||||
title: 'Status',
|
||||
options: [{ value: 'enabled', label: 'Enabled' }],
|
||||
},
|
||||
]
|
||||
|
||||
function Fixture(props: { collapsibleOnMobile?: boolean }) {
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([
|
||||
{ id: 'status', value: ['enabled'] },
|
||||
])
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns: [
|
||||
{ accessorKey: 'name' },
|
||||
{ accessorKey: 'model' },
|
||||
{ accessorKey: 'status' },
|
||||
],
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
state: { columnFilters },
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
})
|
||||
return (
|
||||
<DataTableToolbar
|
||||
table={table}
|
||||
collapsibleOnMobile={props.collapsibleOnMobile}
|
||||
searchPlaceholder='Search channels'
|
||||
searchDebounceMs={50}
|
||||
filters={filters}
|
||||
additionalSearch={
|
||||
<Input
|
||||
aria-label='Model'
|
||||
value={String(table.getColumn('model')?.getFilterValue() ?? '')}
|
||||
onChange={(event) =>
|
||||
table.getColumn('model')?.setFilterValue(event.target.value)
|
||||
}
|
||||
/>
|
||||
}
|
||||
preActions={<Button>Refresh</Button>}
|
||||
viewToggle={<Button>Card view</Button>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function setMobileViewport(mobile: boolean) {
|
||||
const original = window.matchMedia
|
||||
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
|
||||
...original(query),
|
||||
matches: mobile && query === '(max-width: 640px)',
|
||||
}))
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('hides the mobile filter group while keeping reset and quick actions accessible', async () => {
|
||||
setMobileViewport(true)
|
||||
const user = userEvent.setup()
|
||||
render(<Fixture collapsibleOnMobile />)
|
||||
|
||||
const collapse = screen.getByRole('button', { name: 'Collapse' })
|
||||
expect(collapse).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByPlaceholderText('Search channels')).toBeVisible()
|
||||
expect(screen.getByRole('textbox', { name: 'Model' })).toBeVisible()
|
||||
expect(screen.getByRole('button', { name: /^Status/ })).toBeVisible()
|
||||
collapse.focus()
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Expand' })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'false'
|
||||
)
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /^Status/ })
|
||||
).not.toBeInTheDocument()
|
||||
for (const name of ['Reset', 'Refresh', 'Card view', 'View']) {
|
||||
expect(screen.getByRole('button', { name })).toBeVisible()
|
||||
expect(screen.getByRole('button', { name })).toBeEnabled()
|
||||
}
|
||||
})
|
||||
|
||||
it('retains mobile search and filter values after expanding and resets them while collapsed', async () => {
|
||||
setMobileViewport(true)
|
||||
const user = userEvent.setup()
|
||||
render(<Fixture collapsibleOnMobile />)
|
||||
|
||||
await user.type(screen.getByPlaceholderText('Search channels'), 'production')
|
||||
await user.type(screen.getByRole('textbox', { name: 'Model' }), 'gpt')
|
||||
await user.click(screen.getByRole('button', { name: 'Collapse' }))
|
||||
await user.click(screen.getByRole('button', { name: 'Expand' }))
|
||||
|
||||
expect(screen.getByPlaceholderText('Search channels')).toHaveValue(
|
||||
'production'
|
||||
)
|
||||
expect(screen.getByRole('textbox', { name: 'Model' })).toHaveValue('gpt')
|
||||
expect(screen.getByRole('button', { name: /^Status/ })).toHaveTextContent(
|
||||
'Enabled'
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'Collapse' }))
|
||||
await user.click(screen.getByRole('button', { name: 'Reset' }))
|
||||
await user.click(screen.getByRole('button', { name: 'Expand' }))
|
||||
|
||||
expect(screen.getByPlaceholderText('Search channels')).toHaveValue('')
|
||||
expect(screen.getByRole('textbox', { name: 'Model' })).toHaveValue('')
|
||||
expect(screen.getByRole('button', { name: 'Status' })).toBeVisible()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Reset' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ mobile: false, collapsibleOnMobile: true, condition: 'on desktop' },
|
||||
{ mobile: true, collapsibleOnMobile: undefined, condition: 'without opt-in' },
|
||||
])('keeps filters visible without a panel toggle $condition', (testCase) => {
|
||||
setMobileViewport(testCase.mobile)
|
||||
render(<Fixture collapsibleOnMobile={testCase.collapsibleOnMobile} />)
|
||||
|
||||
expect(screen.getByPlaceholderText('Search channels')).toBeVisible()
|
||||
expect(screen.getByRole('textbox', { name: 'Model' })).toBeVisible()
|
||||
expect(screen.getByRole('button', { name: /^Status/ })).toBeVisible()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /^(Collapse|Expand)$/ })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
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 { ChevronDown } from 'lucide-react'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type DataTableMobileFilterPanelProps = {
|
||||
children: ReactNode
|
||||
actions: ReactNode
|
||||
compact?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function DataTableMobileFilterPanel(
|
||||
props: DataTableMobileFilterPanelProps
|
||||
) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
className={cn(
|
||||
'bg-card/50 flex min-w-0 flex-col rounded-lg border p-2.5',
|
||||
open && (props.compact ? 'gap-2.5' : 'gap-2'),
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<CollapsibleContent>{props.children}</CollapsibleContent>
|
||||
<div
|
||||
className={cn(
|
||||
props.compact
|
||||
? 'grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-start gap-2'
|
||||
: 'flex items-center justify-end gap-1.5'
|
||||
)}
|
||||
>
|
||||
<CollapsibleTrigger
|
||||
aria-label={open ? t('Collapse') : t('Expand')}
|
||||
render={
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className={cn(
|
||||
'text-muted-foreground hover:text-foreground',
|
||||
props.compact ? 'size-9' : 'mr-auto size-7'
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ChevronDown
|
||||
aria-hidden='true'
|
||||
className={cn(
|
||||
'transition-transform duration-200',
|
||||
props.compact ? 'size-4' : 'size-3.5',
|
||||
open && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<div
|
||||
role='group'
|
||||
aria-label={t('Actions')}
|
||||
className={cn(
|
||||
'flex items-center justify-end gap-1.5',
|
||||
props.compact &&
|
||||
'min-w-0 flex-wrap [&>button]:h-auto [&>button]:min-h-9 [&>button]:max-w-full [&>button]:[overflow-wrap:anywhere] [&>button]:whitespace-normal'
|
||||
)}
|
||||
>
|
||||
{props.actions}
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
@@ -24,10 +24,11 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useDebounce } from '@/hooks'
|
||||
import { useDebounce, useMediaQuery } from '@/hooks'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { DataTableFacetedFilter } from './faceted-filter'
|
||||
import { DataTableMobileFilterPanel } from './mobile-filter-panel'
|
||||
import { DataTableViewOptions } from './view-options'
|
||||
|
||||
type FilterDef = {
|
||||
@@ -78,6 +79,10 @@ export type DataTableToolbarProps<TData> = {
|
||||
* search input and filter chips.
|
||||
*/
|
||||
additionalSearch?: ReactNode
|
||||
/**
|
||||
* Collapse the complete filter panel on mobile while keeping actions visible.
|
||||
*/
|
||||
collapsibleOnMobile?: boolean
|
||||
/**
|
||||
* Whether non-table filters (e.g. `additionalSearch` or `expandable`
|
||||
* inputs) are currently active. Controls Reset button visibility
|
||||
@@ -155,6 +160,7 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
|
||||
const { t } = useTranslation()
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [isSearchComposing, setIsSearchComposing] = useState(false)
|
||||
const isMobile = useMediaQuery('(max-width: 640px)')
|
||||
|
||||
const filters = props.filters ?? []
|
||||
const hasExpandable = props.expandable != null
|
||||
@@ -341,6 +347,33 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
|
||||
|
||||
const hasLeftActions = props.leftActions != null
|
||||
|
||||
if (isMobile && props.collapsibleOnMobile) {
|
||||
return (
|
||||
<DataTableMobileFilterPanel
|
||||
compact
|
||||
className={props.className}
|
||||
actions={
|
||||
<>
|
||||
{props.leftActions}
|
||||
{props.preActions}
|
||||
{resetButton}
|
||||
{searchButton}
|
||||
{viewToggleNode}
|
||||
{viewOptionsNode}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className='flex min-w-0 flex-wrap items-center gap-2'>
|
||||
{props.customSearch !== undefined ? props.customSearch : searchInput}
|
||||
{props.additionalSearch}
|
||||
{filterChips}
|
||||
{expanded && hasExpandable && props.expandable}
|
||||
{expandToggle}
|
||||
</div>
|
||||
</DataTableMobileFilterPanel>
|
||||
)
|
||||
}
|
||||
|
||||
if (hasLeftActions) {
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-2', props.className)}>
|
||||
|
||||
Reference in new issue
Block a user