mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
improvement(ui): updated kb tag component to match existing table (#2498)
* improvement(ui): updated kb tag component to match existing table * fix selection * fix more ui --------- Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
This commit is contained in:
@@ -1,18 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useMemo, useRef, useState } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import { Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react'
|
||||||
import {
|
import { Button, Combobox, type ComboboxOption, Label, Trash } from '@/components/emcn'
|
||||||
Button,
|
import { Input } from '@/components/ui/input'
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Popover,
|
|
||||||
PopoverAnchor,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverItem,
|
|
||||||
PopoverScrollArea,
|
|
||||||
Trash,
|
|
||||||
} from '@/components/emcn'
|
|
||||||
import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants'
|
import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants'
|
||||||
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
||||||
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||||
@@ -74,9 +65,6 @@ export function DocumentTagEntry({
|
|||||||
|
|
||||||
const emitTagSelection = useTagSelection(blockId, subBlock.id)
|
const emitTagSelection = useTagSelection(blockId, subBlock.id)
|
||||||
|
|
||||||
// State for tag name dropdown visibility - one for each row
|
|
||||||
const [dropdownStates, setDropdownStates] = useState<Record<number, boolean>>({})
|
|
||||||
|
|
||||||
// Use preview value when in preview mode, otherwise use store value
|
// Use preview value when in preview mode, otherwise use store value
|
||||||
const currentValue = isPreview ? previewValue : storeValue
|
const currentValue = isPreview ? previewValue : storeValue
|
||||||
|
|
||||||
@@ -194,7 +182,13 @@ export function DocumentTagEntry({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteRow = (rowIndex: number) => {
|
const handleDeleteRow = (rowIndex: number) => {
|
||||||
if (isPreview || disabled || rows.length <= 1) return
|
if (isPreview || disabled) return
|
||||||
|
|
||||||
|
if (rows.length <= 1) {
|
||||||
|
// Clear the single row instead of deleting
|
||||||
|
setStoreValue('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const updatedRows = rows.filter((_, idx) => idx !== rowIndex)
|
const updatedRows = rows.filter((_, idx) => idx !== rowIndex)
|
||||||
const tableDataForStorage = updatedRows.map((row) => ({
|
const tableDataForStorage = updatedRows.map((row) => ({
|
||||||
@@ -227,88 +221,55 @@ export function DocumentTagEntry({
|
|||||||
|
|
||||||
if (tagDefinitions.length === 0) {
|
if (tagDefinitions.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className='rounded-md border p-4 text-center text-muted-foreground text-sm'>
|
<div className='flex h-32 items-center justify-center rounded-lg border border-muted-foreground/25 bg-muted/20'>
|
||||||
No tags defined for this knowledge base.
|
<div className='text-center'>
|
||||||
<br />
|
<p className='font-medium text-[var(--text-secondary)] text-sm'>
|
||||||
Define tags at the knowledge base level first.
|
No tags defined for this knowledge base
|
||||||
|
</p>
|
||||||
|
<p className='mt-1 text-[var(--text-muted)] text-xs'>
|
||||||
|
Define tags at the knowledge base level first
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<thead>
|
<thead className='bg-transparent'>
|
||||||
<tr className='border-b'>
|
<tr className='border-[var(--border-strong)] border-b bg-transparent'>
|
||||||
<th className='w-2/5 border-r px-4 py-2 text-center font-medium text-sm'>Tag</th>
|
<th className='w-[50%] min-w-0 border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
<th className='border-r px-4 py-2 text-center font-medium text-sm'>Value</th>
|
Tag
|
||||||
<th className='w-10' />
|
</th>
|
||||||
|
<th className='w-[50%] min-w-0 bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderTagNameCell = (row: DocumentTagRow, rowIndex: number) => {
|
const renderTagNameCell = (row: DocumentTagRow, rowIndex: number) => {
|
||||||
const cellValue = row.cells.tagName || ''
|
const cellValue = row.cells.tagName || ''
|
||||||
const isOpen = dropdownStates[rowIndex] || false
|
|
||||||
|
|
||||||
const setIsOpen = (open: boolean) => {
|
|
||||||
setDropdownStates((prev) => ({ ...prev, [rowIndex]: open }))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show tags that are either available OR currently selected for this row
|
// Show tags that are either available OR currently selected for this row
|
||||||
const selectableTags = tagDefinitions.filter(
|
const selectableTags = tagDefinitions.filter(
|
||||||
(def) => def.displayName === cellValue || !usedTagNames.has(def.displayName.toLowerCase())
|
(def) => def.displayName === cellValue || !usedTagNames.has(def.displayName.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const tagOptions: ComboboxOption[] = selectableTags.map((tag) => ({
|
||||||
|
value: tag.displayName,
|
||||||
|
label: `${tag.displayName} (${FIELD_TYPE_LABELS[tag.fieldType] || 'Text'})`,
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className='relative border-r p-1'>
|
<td className='relative min-w-0 overflow-hidden border-[var(--border-strong)] border-r bg-transparent p-0'>
|
||||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
<Combobox
|
||||||
<PopoverAnchor asChild>
|
options={tagOptions}
|
||||||
<div
|
value={cellValue}
|
||||||
className='relative w-full cursor-pointer'
|
onChange={(value) => handleTagSelection(rowIndex, value)}
|
||||||
onClick={() => !disabled && setIsOpen(true)}
|
disabled={disabled || isLoading}
|
||||||
>
|
placeholder='Select tag'
|
||||||
<Input
|
className='!border-0 !bg-transparent hover:!bg-transparent px-[10px] py-[8px] font-medium text-sm leading-[21px] focus-visible:ring-0 focus-visible:ring-offset-0 [&>span]:truncate'
|
||||||
value={cellValue}
|
/>
|
||||||
readOnly
|
|
||||||
disabled={disabled}
|
|
||||||
autoComplete='off'
|
|
||||||
placeholder='Select tag'
|
|
||||||
className='w-full cursor-pointer border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
|
||||||
/>
|
|
||||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-[8px] font-medium font-sans text-sm'>
|
|
||||||
<span className='truncate'>
|
|
||||||
{cellValue || <span className='text-muted-foreground/50'>Select tag</span>}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverAnchor>
|
|
||||||
{selectableTags.length > 0 && (
|
|
||||||
<PopoverContent
|
|
||||||
side='bottom'
|
|
||||||
align='start'
|
|
||||||
sideOffset={4}
|
|
||||||
maxHeight={192}
|
|
||||||
className='w-[200px]'
|
|
||||||
>
|
|
||||||
<PopoverScrollArea>
|
|
||||||
{selectableTags.map((tagDef) => (
|
|
||||||
<PopoverItem
|
|
||||||
key={tagDef.id}
|
|
||||||
active={tagDef.displayName === cellValue}
|
|
||||||
onClick={() => {
|
|
||||||
handleTagSelection(rowIndex, tagDef.displayName)
|
|
||||||
setIsOpen(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className='flex-1 truncate'>{tagDef.displayName}</span>
|
|
||||||
<span className='flex-shrink-0 rounded bg-muted px-1 py-0.5 text-[10px] text-muted-foreground'>
|
|
||||||
{FIELD_TYPE_LABELS[tagDef.fieldType] || 'Text'}
|
|
||||||
</span>
|
|
||||||
</PopoverItem>
|
|
||||||
))}
|
|
||||||
</PopoverScrollArea>
|
|
||||||
</PopoverContent>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -333,7 +294,7 @@ export function DocumentTagEntry({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className='p-1'>
|
<td className='relative min-w-0 overflow-hidden bg-transparent p-0'>
|
||||||
<div className='relative w-full'>
|
<div className='relative w-full'>
|
||||||
<Input
|
<Input
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@@ -347,10 +308,10 @@ export function DocumentTagEntry({
|
|||||||
disabled={disabled || !isTagSelected}
|
disabled={disabled || !isTagSelected}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
placeholder={isTagSelected ? placeholder : 'Select a tag first'}
|
placeholder={isTagSelected ? placeholder : 'Select a tag first'}
|
||||||
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
className='w-full border-0 bg-transparent px-[10px] py-[8px] font-medium text-sm text-transparent leading-[21px] caret-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||||
/>
|
/>
|
||||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-[8px] font-medium font-sans text-sm'>
|
<div className='scrollbar-hide pointer-events-none absolute top-0 right-[10px] bottom-0 left-[10px] overflow-x-auto overflow-y-hidden bg-transparent'>
|
||||||
<div className='whitespace-pre'>
|
<div className='whitespace-pre py-[8px] font-medium text-[var(--text-primary)] text-sm leading-[21px]'>
|
||||||
{formatDisplayText(cellValue, {
|
{formatDisplayText(cellValue, {
|
||||||
accessiblePrefixes,
|
accessiblePrefixes,
|
||||||
highlightAll: !accessiblePrefixes,
|
highlightAll: !accessiblePrefixes,
|
||||||
@@ -379,29 +340,32 @@ export function DocumentTagEntry({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderDeleteButton = (rowIndex: number) => {
|
const renderDeleteButton = (rowIndex: number) => {
|
||||||
const canDelete = !isPreview && !disabled
|
if (isPreview || disabled) return null
|
||||||
|
|
||||||
return canDelete ? (
|
return (
|
||||||
<td className='w-10 p-1'>
|
<td className='w-0 p-0'>
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
className='h-8 w-8 p-0 opacity-0 group-hover:opacity-100'
|
className='-translate-y-1/2 absolute top-1/2 right-[8px] transition-opacity'
|
||||||
onClick={() => handleDeleteRow(rowIndex)}
|
onClick={() => handleDeleteRow(rowIndex)}
|
||||||
>
|
>
|
||||||
<Trash className='h-4 w-4 text-muted-foreground' />
|
<Trash className='h-[14px] w-[14px]' />
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
) : null
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative w-full'>
|
||||||
<div className='overflow-visible rounded-md border'>
|
<div className='overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-2)] dark:bg-[#1F1F1F]'>
|
||||||
<table className='w-full'>
|
<table className='w-full table-fixed bg-transparent'>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
<tbody>
|
<tbody className='bg-transparent'>
|
||||||
{rows.map((row, rowIndex) => (
|
{rows.map((row, rowIndex) => (
|
||||||
<tr key={row.id} className='group relative border-t'>
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className='group relative border-[var(--border-strong)] border-t bg-transparent'
|
||||||
|
>
|
||||||
{renderTagNameCell(row, rowIndex)}
|
{renderTagNameCell(row, rowIndex)}
|
||||||
{renderValueCell(row, rowIndex)}
|
{renderValueCell(row, rowIndex)}
|
||||||
{renderDeleteButton(rowIndex)}
|
{renderDeleteButton(rowIndex)}
|
||||||
@@ -411,15 +375,10 @@ export function DocumentTagEntry({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Row Button */}
|
{/* Add Tag Button */}
|
||||||
{!isPreview && !disabled && (
|
{!isPreview && !disabled && (
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
<Button
|
<Button onClick={handleAddRow} disabled={!canAddMoreTags} className='h-7 px-2 text-xs'>
|
||||||
variant='outline'
|
|
||||||
onClick={handleAddRow}
|
|
||||||
disabled={!canAddMoreTags}
|
|
||||||
className='h-7 px-2 text-xs'
|
|
||||||
>
|
|
||||||
<Plus className='mr-1 h-2.5 w-2.5' />
|
<Plus className='mr-1 h-2.5 w-2.5' />
|
||||||
Add Tag
|
Add Tag
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,19 +2,8 @@
|
|||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react'
|
||||||
import {
|
import { Button, Combobox, type ComboboxOption, Label, Trash } from '@/components/emcn'
|
||||||
Button,
|
import { Input } from '@/components/ui/input'
|
||||||
Combobox,
|
|
||||||
type ComboboxOption,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Popover,
|
|
||||||
PopoverAnchor,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverItem,
|
|
||||||
PopoverScrollArea,
|
|
||||||
Trash,
|
|
||||||
} from '@/components/emcn'
|
|
||||||
import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants'
|
import { FIELD_TYPE_LABELS, getPlaceholderForFieldType } from '@/lib/knowledge/constants'
|
||||||
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
|
import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/filters/types'
|
||||||
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
||||||
@@ -66,21 +55,15 @@ export function KnowledgeTagFilters({
|
|||||||
previewValue,
|
previewValue,
|
||||||
}: KnowledgeTagFiltersProps) {
|
}: KnowledgeTagFiltersProps) {
|
||||||
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
|
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
|
||||||
|
|
||||||
// Hook for immediate tag/dropdown selections
|
|
||||||
const emitTagSelection = useTagSelection(blockId, subBlock.id)
|
const emitTagSelection = useTagSelection(blockId, subBlock.id)
|
||||||
|
|
||||||
// Get the knowledge base ID from other sub-blocks
|
|
||||||
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||||
const knowledgeBaseId = knowledgeBaseIdValue || null
|
const knowledgeBaseId = knowledgeBaseIdValue || null
|
||||||
|
|
||||||
// Use KB tag definitions hook to get available tags
|
|
||||||
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
|
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
|
||||||
|
|
||||||
// Get accessible prefixes for variable highlighting
|
|
||||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||||
|
|
||||||
// State for managing tag dropdown
|
|
||||||
const [activeTagDropdown, setActiveTagDropdown] = useState<{
|
const [activeTagDropdown, setActiveTagDropdown] = useState<{
|
||||||
rowIndex: number
|
rowIndex: number
|
||||||
showTags: boolean
|
showTags: boolean
|
||||||
@@ -89,15 +72,10 @@ export function KnowledgeTagFilters({
|
|||||||
element?: HTMLElement | null
|
element?: HTMLElement | null
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
// State for dropdown visibility - one for each row
|
|
||||||
const [dropdownStates, setDropdownStates] = useState<Record<number, boolean>>({})
|
|
||||||
|
|
||||||
// Parse the current value to extract filters
|
|
||||||
const parseFilters = (filterValue: string | null): TagFilter[] => {
|
const parseFilters = (filterValue: string | null): TagFilter[] => {
|
||||||
if (!filterValue) return []
|
if (!filterValue) return []
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(filterValue)
|
const parsed = JSON.parse(filterValue)
|
||||||
// Handle legacy format (without fieldType/operator)
|
|
||||||
return parsed.map((f: TagFilter) => ({
|
return parsed.map((f: TagFilter) => ({
|
||||||
...f,
|
...f,
|
||||||
fieldType: f.fieldType || 'text',
|
fieldType: f.fieldType || 'text',
|
||||||
@@ -111,7 +89,6 @@ export function KnowledgeTagFilters({
|
|||||||
const currentValue = isPreview ? previewValue : storeValue
|
const currentValue = isPreview ? previewValue : storeValue
|
||||||
const filters = parseFilters(currentValue || null)
|
const filters = parseFilters(currentValue || null)
|
||||||
|
|
||||||
// Transform filters to table format for display
|
|
||||||
const rows: TagFilterRow[] =
|
const rows: TagFilterRow[] =
|
||||||
filters.length > 0
|
filters.length > 0
|
||||||
? filters.map((filter) => ({
|
? filters.map((filter) => ({
|
||||||
@@ -128,7 +105,7 @@ export function KnowledgeTagFilters({
|
|||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: 'empty-row-0',
|
id: 'empty-row-0',
|
||||||
cells: { tagName: '', fieldType: 'text', operator: 'eq', value: '' },
|
cells: { tagName: '', fieldType: 'text', operator: '', value: '' },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -138,17 +115,18 @@ export function KnowledgeTagFilters({
|
|||||||
setStoreValue(value)
|
setStoreValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert rows back to TagFilter format */
|
|
||||||
const rowsToFilters = (rowsToConvert: TagFilterRow[]): TagFilter[] => {
|
const rowsToFilters = (rowsToConvert: TagFilterRow[]): TagFilter[] => {
|
||||||
return rowsToConvert.map((row) => ({
|
return rowsToConvert
|
||||||
id: row.id,
|
.filter((row) => row.cells.tagName?.trim())
|
||||||
tagName: row.cells.tagName || '',
|
.map((row) => ({
|
||||||
tagSlot: row.cells.tagSlot,
|
id: row.id,
|
||||||
fieldType: row.cells.fieldType || 'text',
|
tagName: row.cells.tagName || '',
|
||||||
operator: row.cells.operator || 'eq',
|
tagSlot: row.cells.tagSlot,
|
||||||
tagValue: row.cells.value || '',
|
fieldType: row.cells.fieldType || 'text',
|
||||||
valueTo: row.cells.valueTo,
|
operator: row.cells.operator || 'eq',
|
||||||
}))
|
tagValue: row.cells.value || '',
|
||||||
|
valueTo: row.cells.valueTo,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCellChange = (rowIndex: number, column: string, value: string | FilterFieldType) => {
|
const handleCellChange = (rowIndex: number, column: string, value: string | FilterFieldType) => {
|
||||||
@@ -158,15 +136,13 @@ export function KnowledgeTagFilters({
|
|||||||
if (idx === rowIndex) {
|
if (idx === rowIndex) {
|
||||||
const newCells = { ...row.cells, [column]: value }
|
const newCells = { ...row.cells, [column]: value }
|
||||||
|
|
||||||
// Reset operator when field type changes
|
|
||||||
if (column === 'fieldType') {
|
if (column === 'fieldType') {
|
||||||
const operators = getOperatorsForFieldType(value as FilterFieldType)
|
const operators = getOperatorsForFieldType(value as FilterFieldType)
|
||||||
newCells.operator = operators[0]?.value || 'eq'
|
newCells.operator = operators[0]?.value || 'eq'
|
||||||
newCells.value = '' // Reset value when type changes
|
newCells.value = ''
|
||||||
newCells.valueTo = undefined
|
newCells.valueTo = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset valueTo if operator is not 'between'
|
|
||||||
if (column === 'operator' && value !== 'between') {
|
if (column === 'operator' && value !== 'between') {
|
||||||
newCells.valueTo = undefined
|
newCells.valueTo = undefined
|
||||||
}
|
}
|
||||||
@@ -179,11 +155,9 @@ export function KnowledgeTagFilters({
|
|||||||
updateFilters(rowsToFilters(updatedRows))
|
updateFilters(rowsToFilters(updatedRows))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handle tag name selection from dropdown */
|
|
||||||
const handleTagNameSelection = (rowIndex: number, tagName: string) => {
|
const handleTagNameSelection = (rowIndex: number, tagName: string) => {
|
||||||
if (isPreview || disabled) return
|
if (isPreview || disabled) return
|
||||||
|
|
||||||
// Find the tag definition to get fieldType and tagSlot
|
|
||||||
const tagDef = tagDefinitions.find((t) => t.displayName === tagName)
|
const tagDef = tagDefinitions.find((t) => t.displayName === tagName)
|
||||||
const fieldType = (tagDef?.fieldType || 'text') as FilterFieldType
|
const fieldType = (tagDef?.fieldType || 'text') as FilterFieldType
|
||||||
const operators = getOperatorsForFieldType(fieldType)
|
const operators = getOperatorsForFieldType(fieldType)
|
||||||
@@ -198,7 +172,7 @@ export function KnowledgeTagFilters({
|
|||||||
tagSlot: tagDef?.tagSlot,
|
tagSlot: tagDef?.tagSlot,
|
||||||
fieldType,
|
fieldType,
|
||||||
operator: operators[0]?.value || 'eq',
|
operator: operators[0]?.value || 'eq',
|
||||||
value: '', // Reset value when tag changes
|
value: '',
|
||||||
valueTo: undefined,
|
valueTo: undefined,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -242,7 +216,14 @@ export function KnowledgeTagFilters({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteRow = (rowIndex: number) => {
|
const handleDeleteRow = (rowIndex: number) => {
|
||||||
if (isPreview || disabled || rows.length <= 1) return
|
if (isPreview || disabled) return
|
||||||
|
|
||||||
|
if (rows.length <= 1) {
|
||||||
|
// Clear the single row instead of deleting
|
||||||
|
setStoreValue(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const updatedRows = rows.filter((_, idx) => idx !== rowIndex)
|
const updatedRows = rows.filter((_, idx) => idx !== rowIndex)
|
||||||
updateFilters(rowsToFilters(updatedRows))
|
updateFilters(rowsToFilters(updatedRows))
|
||||||
}
|
}
|
||||||
@@ -261,80 +242,48 @@ export function KnowledgeTagFilters({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<thead>
|
<thead className='bg-transparent'>
|
||||||
<tr className='border-b'>
|
<tr className='border-[var(--border-strong)] border-b bg-transparent'>
|
||||||
<th className='w-[35%] border-r px-2 py-2 text-center font-medium text-sm'>Tag</th>
|
<th className='w-[35%] min-w-0 border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
<th className='w-[25%] border-r px-2 py-2 text-center font-medium text-sm'>Operator</th>
|
Tag
|
||||||
<th className='border-r px-2 py-2 text-center font-medium text-sm'>Value</th>
|
</th>
|
||||||
<th className='w-10' />
|
<th className='w-[35%] min-w-0 border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
|
Operator
|
||||||
|
</th>
|
||||||
|
<th className='w-[30%] min-w-0 bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderTagNameCell = (row: TagFilterRow, rowIndex: number) => {
|
const renderTagNameCell = (row: TagFilterRow, rowIndex: number) => {
|
||||||
const cellValue = row.cells.tagName || ''
|
const cellValue = row.cells.tagName || ''
|
||||||
const isOpen = dropdownStates[rowIndex] || false
|
|
||||||
|
|
||||||
const setIsOpen = (open: boolean) => {
|
const tagOptions: ComboboxOption[] = tagDefinitions.map((tag) => ({
|
||||||
setDropdownStates((prev) => ({ ...prev, [rowIndex]: open }))
|
value: tag.displayName,
|
||||||
}
|
label: `${tag.displayName} (${FIELD_TYPE_LABELS[tag.fieldType] || 'Text'})`,
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className='relative border-r p-1'>
|
<td className='relative min-w-0 overflow-hidden border-[var(--border-strong)] border-r bg-transparent p-0'>
|
||||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
<Combobox
|
||||||
<PopoverAnchor asChild>
|
options={tagOptions}
|
||||||
<div
|
value={cellValue}
|
||||||
className='relative w-full cursor-pointer'
|
onChange={(value) => handleTagNameSelection(rowIndex, value)}
|
||||||
onClick={() => !disabled && !isLoading && setIsOpen(true)}
|
disabled={disabled || isLoading}
|
||||||
>
|
placeholder='Select tag'
|
||||||
<Input
|
className='!border-0 !bg-transparent hover:!bg-transparent px-[10px] py-[8px] font-medium text-sm leading-[21px] focus-visible:ring-0 focus-visible:ring-offset-0 [&>span]:truncate'
|
||||||
value={cellValue}
|
/>
|
||||||
readOnly
|
|
||||||
disabled={disabled || isLoading}
|
|
||||||
autoComplete='off'
|
|
||||||
className='w-full cursor-pointer border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
|
||||||
/>
|
|
||||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-[8px] font-medium font-sans text-sm'>
|
|
||||||
<span className='truncate'>{cellValue || 'Select tag'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverAnchor>
|
|
||||||
{tagDefinitions.length > 0 && (
|
|
||||||
<PopoverContent
|
|
||||||
side='bottom'
|
|
||||||
align='start'
|
|
||||||
sideOffset={4}
|
|
||||||
maxHeight={192}
|
|
||||||
className='w-[200px]'
|
|
||||||
>
|
|
||||||
<PopoverScrollArea>
|
|
||||||
{tagDefinitions.map((tag) => (
|
|
||||||
<PopoverItem
|
|
||||||
key={tag.id}
|
|
||||||
onClick={() => {
|
|
||||||
handleTagNameSelection(rowIndex, tag.displayName)
|
|
||||||
setIsOpen(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className='flex-1 truncate'>{tag.displayName}</span>
|
|
||||||
<span className='flex-shrink-0 rounded bg-muted px-1 py-0.5 text-[10px] text-muted-foreground'>
|
|
||||||
{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}
|
|
||||||
</span>
|
|
||||||
</PopoverItem>
|
|
||||||
))}
|
|
||||||
</PopoverScrollArea>
|
|
||||||
</PopoverContent>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Render operator cell */
|
|
||||||
const renderOperatorCell = (row: TagFilterRow, rowIndex: number) => {
|
const renderOperatorCell = (row: TagFilterRow, rowIndex: number) => {
|
||||||
const fieldType = row.cells.fieldType || 'text'
|
const fieldType = row.cells.fieldType || 'text'
|
||||||
const operator = row.cells.operator || 'eq'
|
const operator = row.cells.operator || ''
|
||||||
const operators = getOperatorsForFieldType(fieldType)
|
const operators = getOperatorsForFieldType(fieldType)
|
||||||
|
const isOperatorDisabled = disabled || !row.cells.tagName
|
||||||
|
|
||||||
const operatorOptions: ComboboxOption[] = operators.map((op) => ({
|
const operatorOptions: ComboboxOption[] = operators.map((op) => ({
|
||||||
value: op.value,
|
value: op.value,
|
||||||
@@ -342,14 +291,14 @@ export function KnowledgeTagFilters({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className='border-r p-1'>
|
<td className='relative min-w-0 overflow-hidden border-[var(--border-strong)] border-r bg-transparent p-0'>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={operatorOptions}
|
options={operatorOptions}
|
||||||
value={operator}
|
value={operator}
|
||||||
onChange={(value) => handleCellChange(rowIndex, 'operator', value)}
|
onChange={(value) => handleCellChange(rowIndex, 'operator', value)}
|
||||||
disabled={disabled || !row.cells.tagName}
|
disabled={isOperatorDisabled}
|
||||||
placeholder='Operator'
|
placeholder='Select operator'
|
||||||
size='sm'
|
className='!border-0 !bg-transparent hover:!bg-transparent px-[10px] py-[8px] font-medium text-sm leading-[21px] focus-visible:ring-0 focus-visible:ring-offset-0 [&>span]:truncate'
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
@@ -364,7 +313,6 @@ export function KnowledgeTagFilters({
|
|||||||
const isDisabled = disabled || !row.cells.tagName
|
const isDisabled = disabled || !row.cells.tagName
|
||||||
const placeholder = getPlaceholderForFieldType(fieldType)
|
const placeholder = getPlaceholderForFieldType(fieldType)
|
||||||
|
|
||||||
// Single text input for all field types with variable support
|
|
||||||
const renderInput = (value: string, column: 'value' | 'valueTo') => (
|
const renderInput = (value: string, column: 'value' | 'valueTo') => (
|
||||||
<div className='relative w-full'>
|
<div className='relative w-full'>
|
||||||
<Input
|
<Input
|
||||||
@@ -375,7 +323,6 @@ export function KnowledgeTagFilters({
|
|||||||
|
|
||||||
handleCellChange(rowIndex, column, newValue)
|
handleCellChange(rowIndex, column, newValue)
|
||||||
|
|
||||||
// Check for tag trigger (only for primary value input)
|
|
||||||
if (column === 'value') {
|
if (column === 'value') {
|
||||||
const tagTrigger = checkTagTrigger(newValue, cursorPosition)
|
const tagTrigger = checkTagTrigger(newValue, cursorPosition)
|
||||||
|
|
||||||
@@ -412,10 +359,10 @@ export function KnowledgeTagFilters({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
className='w-full border-0 bg-transparent px-[10px] py-[8px] font-medium text-sm text-transparent leading-[21px] caret-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||||
/>
|
/>
|
||||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-[8px] font-medium font-sans text-sm'>
|
<div className='scrollbar-hide pointer-events-none absolute top-0 right-[10px] bottom-0 left-[10px] overflow-x-auto overflow-y-hidden bg-transparent'>
|
||||||
<div className='whitespace-pre'>
|
<div className='whitespace-pre py-[8px] font-medium text-[var(--text-primary)] text-sm leading-[21px]'>
|
||||||
{formatDisplayText(value || '', {
|
{formatDisplayText(value || '', {
|
||||||
accessiblePrefixes,
|
accessiblePrefixes,
|
||||||
highlightAll: !accessiblePrefixes,
|
highlightAll: !accessiblePrefixes,
|
||||||
@@ -425,11 +372,10 @@ export function KnowledgeTagFilters({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
// Render with optional "between" second input
|
|
||||||
if (isBetween) {
|
if (isBetween) {
|
||||||
return (
|
return (
|
||||||
<td className='p-1'>
|
<td className='relative min-w-0 overflow-hidden bg-transparent p-0'>
|
||||||
<div className='flex items-center gap-1'>
|
<div className='flex items-center gap-1 px-[10px]'>
|
||||||
{renderInput(cellValue, 'value')}
|
{renderInput(cellValue, 'value')}
|
||||||
<span className='flex-shrink-0 text-muted-foreground text-xs'>to</span>
|
<span className='flex-shrink-0 text-muted-foreground text-xs'>to</span>
|
||||||
{renderInput(valueTo, 'valueTo')}
|
{renderInput(valueTo, 'valueTo')}
|
||||||
@@ -438,23 +384,27 @@ export function KnowledgeTagFilters({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <td className='p-1'>{renderInput(cellValue, 'value')}</td>
|
return (
|
||||||
|
<td className='relative min-w-0 overflow-hidden bg-transparent p-0'>
|
||||||
|
{renderInput(cellValue, 'value')}
|
||||||
|
</td>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderDeleteButton = (rowIndex: number) => {
|
const renderDeleteButton = (rowIndex: number) => {
|
||||||
const canDelete = !isPreview && !disabled
|
if (isPreview || disabled) return null
|
||||||
|
|
||||||
return canDelete ? (
|
return (
|
||||||
<td className='w-10 p-1'>
|
<td className='w-0 p-0'>
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
className='h-8 w-8 p-0 opacity-0 group-hover:opacity-100'
|
className='-translate-y-1/2 absolute top-1/2 right-[8px] transition-opacity'
|
||||||
onClick={() => handleDeleteRow(rowIndex)}
|
onClick={() => handleDeleteRow(rowIndex)}
|
||||||
>
|
>
|
||||||
<Trash className='h-4 w-4 text-muted-foreground' />
|
<Trash className='h-[14px] w-[14px]' />
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
) : null
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -462,13 +412,16 @@ export function KnowledgeTagFilters({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative w-full'>
|
||||||
<div className='overflow-visible rounded-md border'>
|
<div className='overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-2)] dark:bg-[#1F1F1F]'>
|
||||||
<table className='w-full'>
|
<table className='w-full table-fixed bg-transparent'>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
<tbody>
|
<tbody className='bg-transparent'>
|
||||||
{rows.map((row, rowIndex) => (
|
{rows.map((row, rowIndex) => (
|
||||||
<tr key={row.id} className='group relative border-t'>
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className='group relative border-[var(--border-strong)] border-t bg-transparent'
|
||||||
|
>
|
||||||
{renderTagNameCell(row, rowIndex)}
|
{renderTagNameCell(row, rowIndex)}
|
||||||
{renderOperatorCell(row, rowIndex)}
|
{renderOperatorCell(row, rowIndex)}
|
||||||
{renderValueCell(row, rowIndex)}
|
{renderValueCell(row, rowIndex)}
|
||||||
@@ -502,7 +455,7 @@ export function KnowledgeTagFilters({
|
|||||||
{/* Add Filter Button */}
|
{/* Add Filter Button */}
|
||||||
{!isPreview && !disabled && (
|
{!isPreview && !disabled && (
|
||||||
<div className='mt-3 flex items-center justify-between'>
|
<div className='mt-3 flex items-center justify-between'>
|
||||||
<Button variant='outline' onClick={handleAddRow} className='h-7 px-2 text-xs'>
|
<Button onClick={handleAddRow} className='h-7 px-2 text-xs'>
|
||||||
<Plus className='mr-1 h-2.5 w-2.5' />
|
<Plus className='mr-1 h-2.5 w-2.5' />
|
||||||
Add Filter
|
Add Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ export const KnowledgeBlock: BlockConfig = {
|
|||||||
type: 'knowledge-tag-filters',
|
type: 'knowledge-tag-filters',
|
||||||
placeholder: 'Add tag filters',
|
placeholder: 'Add tag filters',
|
||||||
condition: { field: 'operation', value: 'search' },
|
condition: { field: 'operation', value: 'search' },
|
||||||
mode: 'advanced',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'documentId',
|
id: 'documentId',
|
||||||
|
|||||||
@@ -18,8 +18,13 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'
|
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||||
|
import { Button } from '@/components/emcn/components/button/button'
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverContent,
|
||||||
|
} from '@/components/emcn/components/popover/popover'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { Popover, PopoverAnchor, PopoverContent } from '../popover/popover'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variant styles for the date picker trigger button.
|
* Variant styles for the date picker trigger button.
|
||||||
@@ -388,13 +393,9 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(
|
|||||||
|
|
||||||
{/* Today Button */}
|
{/* Today Button */}
|
||||||
<div className='border-[var(--surface-11)] border-t px-[8px] py-[8px]'>
|
<div className='border-[var(--surface-11)] border-t px-[8px] py-[8px]'>
|
||||||
<button
|
<Button variant='active' className='w-full' onClick={handleSelectToday}>
|
||||||
type='button'
|
|
||||||
className='w-full rounded-[4px] py-[6px] text-[12px] text-[var(--text-muted)] transition-colors hover:bg-[var(--surface-9)] hover:text-[var(--text-primary)]'
|
|
||||||
onClick={handleSelectToday}
|
|
||||||
>
|
|
||||||
Today
|
Today
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user