Skip to content

Commit c8f30c4

Browse files
author
waleed
committed
feat(kb-types): added number, boolean, date type to kb tags
1 parent 888fddf commit c8f30c4

11 files changed

Lines changed: 835 additions & 152 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { getSession } from '@/lib/auth'
3+
import { FIELD_TYPE_METADATA, SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/consts'
4+
import { createLogger } from '@/lib/logs/console/logger'
5+
6+
export const dynamic = 'force-dynamic'
7+
8+
const logger = createLogger('FieldTypesAPI')
9+
10+
// GET /api/knowledge/field-types - Get metadata for all supported field types
11+
export async function GET(req: NextRequest) {
12+
try {
13+
logger.info('Getting field type metadata')
14+
15+
const session = await getSession()
16+
if (!session?.user?.id) {
17+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
18+
}
19+
20+
const fieldTypes = SUPPORTED_FIELD_TYPES.map((fieldType) => ({
21+
value: fieldType,
22+
...FIELD_TYPE_METADATA[fieldType],
23+
}))
24+
25+
return NextResponse.json({
26+
success: true,
27+
data: {
28+
fieldTypes,
29+
supportedTypes: SUPPORTED_FIELD_TYPES,
30+
},
31+
})
32+
} catch (error) {
33+
logger.error('Error getting field type metadata', error)
34+
return NextResponse.json({ error: 'Failed to get field type metadata' }, { status: 500 })
35+
}
36+
}

apps/sim/app/workspace/[workspaceId]/knowledge/components/document-tag-entry/document-tag-entry.tsx

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useState } from 'react'
3+
import { useEffect, useState } from 'react'
44
import { ChevronDown, Info, Plus, X } from 'lucide-react'
55
import {
66
Badge,
@@ -30,6 +30,7 @@ import { createLogger } from '@/lib/logs/console/logger'
3030
import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions'
3131
import { useNextAvailableSlot } from '@/hooks/use-next-available-slot'
3232
import { type TagDefinitionInput, useTagDefinitions } from '@/hooks/use-tag-definitions'
33+
import { TypedTagInput } from '../tag-input/typed-tag-input'
3334

3435
const logger = createLogger('DocumentTagEntry')
3536

@@ -66,6 +67,43 @@ export function DocumentTagEntry({
6667
const { saveTagDefinitions } = documentTagHook
6768
const { tagDefinitions: kbTagDefinitions, fetchTagDefinitions: refreshTagDefinitions } = kbTagHook
6869

70+
// State for field types
71+
const [fieldTypes, setFieldTypes] = useState<
72+
Array<{
73+
value: string
74+
label: string
75+
description: string
76+
placeholder: string
77+
}>
78+
>([
79+
{
80+
value: 'text',
81+
label: 'Text',
82+
description: 'Free-form text content',
83+
placeholder: 'Enter text',
84+
},
85+
])
86+
87+
// Fetch field types on component mount
88+
useEffect(() => {
89+
const fetchFieldTypes = async () => {
90+
try {
91+
const response = await fetch('/api/knowledge/field-types')
92+
if (response.ok) {
93+
const result = await response.json()
94+
if (result.success) {
95+
setFieldTypes(result.data.fieldTypes)
96+
}
97+
}
98+
} catch (error) {
99+
logger.error('Error fetching field types:', error)
100+
// Keep the default fallback
101+
}
102+
}
103+
104+
fetchFieldTypes()
105+
}, [])
106+
69107
// Modal state for tag editing
70108
const [editingTagIndex, setEditingTagIndex] = useState<number | null>(null)
71109
const [modalOpen, setModalOpen] = useState(false)
@@ -363,16 +401,23 @@ export function DocumentTagEntry({
363401
value={editForm.displayName}
364402
onChange={(e) => setEditForm({ ...editForm, displayName: e.target.value })}
365403
placeholder='Enter tag name'
366-
className='flex-1'
404+
className='h-8 w-full rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
367405
/>
368406
{editingTagIndex === null && availableDefinitions.length > 0 && (
369407
<DropdownMenu>
370408
<DropdownMenuTrigger asChild>
371-
<Button variant='outline' size='sm'>
409+
<Button
410+
variant='outline'
411+
size='sm'
412+
className='h-8 rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
413+
>
372414
<ChevronDown className='h-4 w-4' />
373415
</Button>
374416
</DropdownMenuTrigger>
375-
<DropdownMenuContent align='end'>
417+
<DropdownMenuContent
418+
align='end'
419+
className='w-[240px] rounded-lg border-[#E5E5E5] bg-[#FFFFFF] p-0 shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
420+
>
376421
{availableDefinitions.map((def) => (
377422
<DropdownMenuItem
378423
key={def.id}
@@ -383,8 +428,13 @@ export function DocumentTagEntry({
383428
fieldType: def.fieldType,
384429
})
385430
}
431+
className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
386432
>
387-
{def.displayName}
433+
<span className='truncate'>{def.displayName}</span>
434+
<span className='ml-2 shrink-0 text-muted-foreground text-xs'>
435+
{fieldTypes.find((ft) => ft.value === def.fieldType)?.label ||
436+
def.fieldType}
437+
</span>
388438
</DropdownMenuItem>
389439
))}
390440
</DropdownMenuContent>
@@ -399,25 +449,48 @@ export function DocumentTagEntry({
399449
<Select
400450
value={editForm.fieldType}
401451
onValueChange={(value) => setEditForm({ ...editForm, fieldType: value })}
402-
disabled={editingTagIndex !== null} // Disable in edit mode
452+
disabled={
453+
editingTagIndex !== null || // Disable in edit mode
454+
(editingTagIndex === null &&
455+
kbTagDefinitions.some(
456+
(def) => def.displayName.toLowerCase() === editForm.displayName.toLowerCase()
457+
))
458+
} // Also disable when using existing definition in create mode
403459
>
404-
<SelectTrigger>
405-
<SelectValue />
460+
<SelectTrigger className='h-8 w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[#FFFFFF] text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'>
461+
<SelectValue placeholder='Select type' />
406462
</SelectTrigger>
407-
<SelectContent>
408-
<SelectItem value='text'>Text</SelectItem>
463+
<SelectContent className='rounded-lg border-[#E5E5E5] bg-[#FFFFFF] dark:border-[#414141] dark:bg-[var(--surface-elevated)]'>
464+
{fieldTypes.map((fieldType) => (
465+
<SelectItem key={fieldType.value} value={fieldType.value} className='text-sm'>
466+
{fieldType.label}
467+
</SelectItem>
468+
))}
409469
</SelectContent>
410470
</Select>
411471
</div>
412472

413473
{/* Tag Value */}
414474
<div className='space-y-2'>
415475
<Label htmlFor='tag-value'>Value</Label>
416-
<Input
417-
id='tag-value'
476+
<TypedTagInput
477+
fieldType={editForm.fieldType}
418478
value={editForm.value}
419-
onChange={(e) => setEditForm({ ...editForm, value: e.target.value })}
420-
placeholder='Enter tag value'
479+
onChange={(value) => setEditForm({ ...editForm, value })}
480+
placeholder={
481+
fieldTypes.find((ft) => ft.value === editForm.fieldType)?.placeholder ||
482+
'Enter tag value'
483+
}
484+
showInlineError={true}
485+
onValidityChange={(valid) => {
486+
// Disable save when invalid
487+
// We just store validity in state via a refactor-free approach by toggling a hidden flag on form
488+
// Use a no-op set to trigger re-render when invalid so button disabled reflects it
489+
if (!valid) {
490+
// noop – re-render by updating same state value
491+
setEditForm((prev) => ({ ...prev }))
492+
}
493+
}}
421494
/>
422495
</div>
423496
</div>

apps/sim/app/workspace/[workspaceId]/knowledge/components/tag-input/tag-input.tsx

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { useState } from 'react'
44
import { ChevronDown, ChevronRight, Plus, Settings, X } from 'lucide-react'
55
import { Button } from '@/components/ui/button'
66
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
7-
import { Input } from '@/components/ui/input'
87
import { Label } from '@/components/ui/label'
98
import { TAG_SLOTS, type TagSlot } from '@/lib/knowledge/consts'
109
import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions'
10+
import { TypedTagInput } from './typed-tag-input'
1111

1212
export type TagData = {
1313
[K in TagSlot]?: string
@@ -40,7 +40,7 @@ export function TagInput({
4040
const [showAllTags, setShowAllTags] = useState(false)
4141

4242
// Use custom tag definitions if available
43-
const { getTagLabel } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
43+
const { getTagLabel, getTagDefinition } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
4444

4545
const handleTagChange = (tagKey: keyof TagData, value: string) => {
4646
onTagsChange({
@@ -113,36 +113,40 @@ export function TagInput({
113113
</div>
114114

115115
<div className='grid grid-cols-1 gap-3 sm:grid-cols-2'>
116-
{visibleTags.map(({ key, label, placeholder }) => (
117-
<div key={key} className='space-y-1'>
118-
<Label htmlFor={key} className='text-muted-foreground text-xs'>
119-
{label}
120-
</Label>
121-
<div className='relative'>
122-
<Input
123-
id={key}
124-
type='text'
125-
value={tags[key] || ''}
126-
onChange={(e) => handleTagChange(key, e.target.value)}
127-
placeholder={placeholder}
128-
disabled={disabled}
129-
className='pr-8 text-sm'
130-
/>
131-
{tags[key] && (
132-
<Button
133-
type='button'
134-
variant='ghost'
135-
size='sm'
136-
onClick={() => clearTag(key)}
116+
{visibleTags.map(({ key, label, placeholder }) => {
117+
const tagDefinition = getTagDefinition(key)
118+
const fieldType = tagDefinition?.fieldType || 'text'
119+
120+
return (
121+
<div key={key} className='space-y-1'>
122+
<Label htmlFor={key} className='text-muted-foreground text-xs'>
123+
{label}
124+
</Label>
125+
<div className='relative'>
126+
<TypedTagInput
127+
fieldType={fieldType}
128+
value={tags[key] || ''}
129+
onChange={(value) => handleTagChange(key, value)}
130+
placeholder={placeholder}
137131
disabled={disabled}
138-
className='-translate-y-1/2 absolute top-1/2 right-1 h-6 w-6 p-0 hover:bg-muted'
139-
>
140-
<X className='h-3 w-3' />
141-
</Button>
142-
)}
132+
className='pr-8'
133+
/>
134+
{tags[key] && (
135+
<Button
136+
type='button'
137+
variant='ghost'
138+
size='sm'
139+
onClick={() => clearTag(key)}
140+
disabled={disabled}
141+
className='-translate-y-1/2 absolute top-1/2 right-1 h-6 w-6 p-0 hover:bg-muted'
142+
>
143+
<X className='h-3 w-3' />
144+
</Button>
145+
)}
146+
</div>
143147
</div>
144-
</div>
145-
))}
148+
)
149+
})}
146150
</div>
147151

148152
{showAllTags && (

0 commit comments

Comments
 (0)