d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
682 lines
19 KiB
TypeScript
682 lines
19 KiB
TypeScript
import type { Suggestion, SuggestionGroup } from '@/app/workspace/[workspaceId]/logs/types'
|
|
|
|
export interface FilterDefinition {
|
|
key: string
|
|
label: string
|
|
description: string
|
|
options: Array<{
|
|
value: string
|
|
label: string
|
|
description?: string
|
|
}>
|
|
acceptsCustomValue?: boolean
|
|
customValueHint?: string
|
|
}
|
|
|
|
export interface WorkflowData {
|
|
id: string
|
|
name: string
|
|
description?: string
|
|
}
|
|
|
|
export interface FolderData {
|
|
id: string
|
|
name: string
|
|
}
|
|
|
|
export interface TriggerData {
|
|
value: string
|
|
label: string
|
|
color: string
|
|
}
|
|
|
|
/**
|
|
* Generates current date examples for the date filter options.
|
|
*/
|
|
function getDateExamples() {
|
|
const now = new Date()
|
|
const year = now.getFullYear()
|
|
const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
const day = String(now.getDate()).padStart(2, '0')
|
|
const firstOfMonth = `${year}-${month}-01`
|
|
const today = `${year}-${month}-${day}`
|
|
const yearMonth = `${year}-${month}`
|
|
return { today, firstOfMonth, year: String(year), yearMonth }
|
|
}
|
|
|
|
export const FILTER_DEFINITIONS: FilterDefinition[] = [
|
|
{
|
|
key: 'level',
|
|
label: 'Status',
|
|
description: 'Filter by log level',
|
|
options: [
|
|
{ value: 'error', label: 'Error', description: 'Error logs only' },
|
|
{ value: 'info', label: 'Info', description: 'Info logs only' },
|
|
],
|
|
},
|
|
{
|
|
key: 'cost',
|
|
label: 'Cost',
|
|
description: 'Filter by run cost',
|
|
options: [
|
|
{
|
|
value: '>0.01',
|
|
label: 'Over 2 credits',
|
|
description: 'Runs costing more than 2 credits',
|
|
},
|
|
{
|
|
value: '<0.005',
|
|
label: 'Under 1 credit',
|
|
description: 'Runs costing less than 1 credit',
|
|
},
|
|
{
|
|
value: '>0.05',
|
|
label: 'Over 10 credits',
|
|
description: 'Runs costing more than 10 credits',
|
|
},
|
|
{ value: '=0', label: 'Free', description: 'Free runs' },
|
|
{ value: '>0', label: 'Paid', description: 'Runs with cost' },
|
|
],
|
|
},
|
|
{
|
|
key: 'date',
|
|
label: 'Date',
|
|
description: 'Filter by date range',
|
|
options: (() => {
|
|
const { today, firstOfMonth, year, yearMonth } = getDateExamples()
|
|
return [
|
|
{ value: 'today', label: 'Today', description: "Today's logs" },
|
|
{ value: 'yesterday', label: 'Yesterday', description: "Yesterday's logs" },
|
|
{ value: 'this-week', label: 'This week', description: "This week's logs" },
|
|
{ value: 'last-week', label: 'Last week', description: "Last week's logs" },
|
|
{ value: 'this-month', label: 'This month', description: "This month's logs" },
|
|
{ value: today, label: 'Specific date', description: 'YYYY-MM-DD' },
|
|
{ value: yearMonth, label: 'Specific month', description: 'YYYY-MM' },
|
|
{ value: year, label: 'Specific year', description: 'YYYY' },
|
|
{
|
|
value: `${firstOfMonth}..${today}`,
|
|
label: 'Date range',
|
|
description: 'YYYY-MM-DD..YYYY-MM-DD',
|
|
},
|
|
]
|
|
})(),
|
|
},
|
|
{
|
|
key: 'duration',
|
|
label: 'Duration',
|
|
description: 'Filter by run duration',
|
|
options: [
|
|
{ value: '>5s', label: 'Over 5s', description: 'Runs longer than 5 seconds' },
|
|
{ value: '<1s', label: 'Under 1s', description: 'Runs shorter than 1 second' },
|
|
{ value: '>10s', label: 'Over 10s', description: 'Runs longer than 10 seconds' },
|
|
{ value: '>30s', label: 'Over 30s', description: 'Runs longer than 30 seconds' },
|
|
{ value: '<500ms', label: 'Under 0.5s', description: 'Very fast runs' },
|
|
],
|
|
},
|
|
]
|
|
|
|
export class SearchSuggestions {
|
|
private workflowsData: WorkflowData[]
|
|
private foldersData: FolderData[]
|
|
private triggersData: TriggerData[]
|
|
|
|
constructor(
|
|
workflowsData: WorkflowData[] = [],
|
|
foldersData: FolderData[] = [],
|
|
triggersData: TriggerData[] = []
|
|
) {
|
|
this.workflowsData = workflowsData
|
|
this.foldersData = foldersData
|
|
this.triggersData = triggersData
|
|
}
|
|
|
|
updateData(
|
|
workflowsData: WorkflowData[] = [],
|
|
foldersData: FolderData[] = [],
|
|
triggersData: TriggerData[] = []
|
|
) {
|
|
this.workflowsData = workflowsData
|
|
this.foldersData = foldersData
|
|
this.triggersData = triggersData
|
|
}
|
|
|
|
/**
|
|
* Get all triggers from registry data
|
|
*/
|
|
private getAllTriggers(): TriggerData[] {
|
|
return this.triggersData
|
|
}
|
|
|
|
/**
|
|
* Get suggestions based ONLY on current input (no cursor position!)
|
|
*/
|
|
getSuggestions(input: string): SuggestionGroup | null {
|
|
const trimmed = input.trim()
|
|
|
|
if (!trimmed) {
|
|
return this.getFilterKeysList()
|
|
}
|
|
|
|
if (trimmed.endsWith(':')) {
|
|
const key = trimmed.slice(0, -1)
|
|
return this.getFilterValues(key)
|
|
}
|
|
|
|
if (trimmed.includes(':')) {
|
|
const [key, partial] = trimmed.split(':')
|
|
return this.getFilterValues(key, partial)
|
|
}
|
|
|
|
return this.getMultiSectionResults(trimmed)
|
|
}
|
|
|
|
/**
|
|
* Get filter keys list (empty input state)
|
|
*/
|
|
private getFilterKeysList(): SuggestionGroup {
|
|
const suggestions: Suggestion[] = []
|
|
|
|
for (const filter of FILTER_DEFINITIONS) {
|
|
suggestions.push({
|
|
id: `filter-key-${filter.key}`,
|
|
value: `${filter.key}:`,
|
|
label: filter.label,
|
|
description: filter.description,
|
|
category: 'filters',
|
|
})
|
|
}
|
|
|
|
suggestions.push({
|
|
id: 'filter-key-trigger',
|
|
value: 'trigger:',
|
|
label: 'Trigger',
|
|
description: 'Filter by trigger type',
|
|
category: 'filters',
|
|
})
|
|
|
|
if (this.workflowsData.length > 0) {
|
|
suggestions.push({
|
|
id: 'filter-key-workflow',
|
|
value: 'workflow:',
|
|
label: 'Workflow',
|
|
description: 'Filter by workflow name',
|
|
category: 'filters',
|
|
})
|
|
}
|
|
|
|
if (this.foldersData.length > 0) {
|
|
suggestions.push({
|
|
id: 'filter-key-folder',
|
|
value: 'folder:',
|
|
label: 'Folder',
|
|
description: 'Filter by folder name',
|
|
category: 'filters',
|
|
})
|
|
}
|
|
|
|
suggestions.push({
|
|
id: 'filter-key-workflowId',
|
|
value: 'workflowId:',
|
|
label: 'Workflow ID',
|
|
description: 'Filter by workflow ID',
|
|
category: 'filters',
|
|
})
|
|
|
|
suggestions.push({
|
|
id: 'filter-key-executionId',
|
|
value: 'executionId:',
|
|
label: 'Run ID',
|
|
description: 'Filter by run ID',
|
|
category: 'filters',
|
|
})
|
|
|
|
return {
|
|
type: 'filter-keys',
|
|
suggestions,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get filter values for a specific key
|
|
*/
|
|
private getFilterValues(key: string, partial = ''): SuggestionGroup | null {
|
|
const filterDef = FILTER_DEFINITIONS.find((f) => f.key === key)
|
|
|
|
if (filterDef) {
|
|
const suggestions: Suggestion[] = filterDef.options
|
|
.filter(
|
|
(opt) =>
|
|
!partial ||
|
|
opt.value.toLowerCase().includes(partial.toLowerCase()) ||
|
|
opt.label.toLowerCase().includes(partial.toLowerCase())
|
|
)
|
|
.map((opt) => ({
|
|
id: `filter-value-${key}-${opt.value}`,
|
|
value: `${key}:${opt.value}`,
|
|
label: opt.label,
|
|
description: opt.description,
|
|
category: key as Suggestion['category'],
|
|
}))
|
|
|
|
// Handle custom date input
|
|
if (key === 'date' && partial) {
|
|
const dateSuggestions = this.getDateSuggestions(partial)
|
|
if (dateSuggestions.length > 0) {
|
|
suggestions.unshift(...dateSuggestions)
|
|
}
|
|
}
|
|
|
|
return suggestions.length > 0
|
|
? {
|
|
type: 'filter-values',
|
|
filterKey: key,
|
|
suggestions,
|
|
}
|
|
: null
|
|
}
|
|
|
|
if (key === 'trigger') {
|
|
const allTriggers = this.getAllTriggers()
|
|
const suggestions = allTriggers
|
|
.filter((t) => !partial || t.label.toLowerCase().includes(partial.toLowerCase()))
|
|
.map((t) => ({
|
|
id: `filter-value-trigger-${t.value}`,
|
|
value: `trigger:${t.value}`,
|
|
label: t.label,
|
|
description: `${t.label}-triggered runs`,
|
|
category: 'trigger' as const,
|
|
color: t.color,
|
|
}))
|
|
|
|
return suggestions.length > 0
|
|
? {
|
|
type: 'filter-values',
|
|
filterKey: 'trigger',
|
|
suggestions,
|
|
}
|
|
: null
|
|
}
|
|
|
|
if (key === 'workflow') {
|
|
const suggestions = this.workflowsData
|
|
.filter((w) => !partial || w.name.toLowerCase().includes(partial.toLowerCase()))
|
|
.map((w) => ({
|
|
id: `filter-value-workflow-${w.id}`,
|
|
value: `workflow:"${w.name}"`,
|
|
label: w.name,
|
|
description: w.description,
|
|
category: 'workflow' as const,
|
|
}))
|
|
|
|
return suggestions.length > 0
|
|
? {
|
|
type: 'filter-values',
|
|
filterKey: 'workflow',
|
|
suggestions,
|
|
}
|
|
: null
|
|
}
|
|
|
|
if (key === 'folder') {
|
|
const suggestions = this.foldersData
|
|
.filter((f) => !partial || f.name.toLowerCase().includes(partial.toLowerCase()))
|
|
.map((f) => ({
|
|
id: `filter-value-folder-${f.id}`,
|
|
value: `folder:"${f.name}"`,
|
|
label: f.name,
|
|
category: 'folder' as const,
|
|
}))
|
|
|
|
return suggestions.length > 0
|
|
? {
|
|
type: 'filter-values',
|
|
filterKey: 'folder',
|
|
suggestions,
|
|
}
|
|
: null
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Get multi-section results for plain text
|
|
*/
|
|
private getMultiSectionResults(query: string): SuggestionGroup | null {
|
|
const sections: Array<{ title: string; suggestions: Suggestion[] }> = []
|
|
const allSuggestions: Suggestion[] = []
|
|
|
|
const showAllSuggestion: Suggestion = {
|
|
id: 'show-all',
|
|
value: query,
|
|
label: `Show all results for "${query}"`,
|
|
category: 'show-all',
|
|
}
|
|
allSuggestions.push(showAllSuggestion)
|
|
|
|
const matchingFilterValues = this.getMatchingFilterValues(query)
|
|
if (matchingFilterValues.length > 0) {
|
|
sections.push({
|
|
title: 'SUGGESTED FILTERS',
|
|
suggestions: matchingFilterValues,
|
|
})
|
|
allSuggestions.push(...matchingFilterValues)
|
|
}
|
|
|
|
const matchingTriggers = this.getMatchingTriggers(query)
|
|
if (matchingTriggers.length > 0) {
|
|
sections.push({
|
|
title: 'TRIGGERS',
|
|
suggestions: matchingTriggers,
|
|
})
|
|
allSuggestions.push(...matchingTriggers)
|
|
}
|
|
|
|
const matchingWorkflows = this.getMatchingWorkflows(query)
|
|
if (matchingWorkflows.length > 0) {
|
|
sections.push({
|
|
title: 'WORKFLOWS',
|
|
suggestions: matchingWorkflows,
|
|
})
|
|
allSuggestions.push(...matchingWorkflows)
|
|
}
|
|
|
|
const matchingFolders = this.getMatchingFolders(query)
|
|
if (matchingFolders.length > 0) {
|
|
sections.push({
|
|
title: 'FOLDERS',
|
|
suggestions: matchingFolders,
|
|
})
|
|
allSuggestions.push(...matchingFolders)
|
|
}
|
|
|
|
if (
|
|
matchingFilterValues.length === 0 &&
|
|
matchingTriggers.length === 0 &&
|
|
matchingWorkflows.length === 0 &&
|
|
matchingFolders.length === 0
|
|
) {
|
|
const filterKeys = this.getFilterKeysList()
|
|
if (filterKeys.suggestions.length > 0) {
|
|
sections.push({
|
|
title: 'SUGGESTED FILTERS',
|
|
suggestions: filterKeys.suggestions.slice(0, 5),
|
|
})
|
|
allSuggestions.push(...filterKeys.suggestions.slice(0, 5))
|
|
}
|
|
}
|
|
|
|
return allSuggestions.length > 0
|
|
? {
|
|
type: 'multi-section',
|
|
suggestions: allSuggestions,
|
|
sections,
|
|
}
|
|
: null
|
|
}
|
|
|
|
/**
|
|
* Get suggestions for custom date input
|
|
*/
|
|
private getDateSuggestions(partial: string): Suggestion[] {
|
|
const suggestions: Suggestion[] = []
|
|
|
|
// Pattern for year only: YYYY
|
|
const yearPattern = /^\d{4}$/
|
|
// Pattern for month only: YYYY-MM
|
|
const monthPattern = /^\d{4}-\d{2}$/
|
|
// Pattern for full date: YYYY-MM-DD
|
|
const fullDatePattern = /^\d{4}-\d{2}-\d{2}$/
|
|
// Pattern for partial date being typed
|
|
const partialDatePattern = /^\d{4}(-\d{0,2})?(-\d{0,2})?$/
|
|
// Pattern for date range: YYYY-MM-DD..YYYY-MM-DD (complete or partial)
|
|
const rangePattern = /^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/
|
|
const partialRangePattern = /^(\d{4}-\d{2}-\d{2})\.\.?$/
|
|
|
|
// Check if it's a complete date range
|
|
if (rangePattern.test(partial)) {
|
|
const [startDate, endDate] = partial.split('..')
|
|
suggestions.push({
|
|
id: `date-range-${partial}`,
|
|
value: `date:${partial}`,
|
|
label: `${this.formatDateLabel(startDate)} to ${this.formatDateLabel(endDate)}`,
|
|
description: 'Custom date range',
|
|
category: 'date',
|
|
})
|
|
return suggestions
|
|
}
|
|
|
|
// Check if it's a partial date range (has ..)
|
|
if (partialRangePattern.test(partial)) {
|
|
const startDate = partial.replace(/\.+$/, '')
|
|
suggestions.push({
|
|
id: `date-range-hint-${partial}`,
|
|
value: `date:${startDate}..`,
|
|
label: `${this.formatDateLabel(startDate)} to ...`,
|
|
description: 'Type end date (YYYY-MM-DD)',
|
|
category: 'date',
|
|
})
|
|
return suggestions
|
|
}
|
|
|
|
// Check if it's a year only (YYYY)
|
|
if (yearPattern.test(partial)) {
|
|
suggestions.push({
|
|
id: `date-year-${partial}`,
|
|
value: `date:${partial}`,
|
|
label: `Year ${partial}`,
|
|
description: 'All logs from this year',
|
|
category: 'date',
|
|
})
|
|
return suggestions
|
|
}
|
|
|
|
// Check if it's a month only (YYYY-MM)
|
|
if (monthPattern.test(partial)) {
|
|
const [year, month] = partial.split('-')
|
|
const monthNames = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'May',
|
|
'Jun',
|
|
'Jul',
|
|
'Aug',
|
|
'Sep',
|
|
'Oct',
|
|
'Nov',
|
|
'Dec',
|
|
]
|
|
const monthName = monthNames[Number.parseInt(month, 10) - 1] || month
|
|
suggestions.push({
|
|
id: `date-month-${partial}`,
|
|
value: `date:${partial}`,
|
|
label: `${monthName} ${year}`,
|
|
description: 'All logs from this month',
|
|
category: 'date',
|
|
})
|
|
return suggestions
|
|
}
|
|
|
|
// Check if it's a complete single date
|
|
if (fullDatePattern.test(partial)) {
|
|
const date = new Date(partial)
|
|
if (!Number.isNaN(date.getTime())) {
|
|
suggestions.push({
|
|
id: `date-single-${partial}`,
|
|
value: `date:${partial}`,
|
|
label: this.formatDateLabel(partial),
|
|
description: 'Single date',
|
|
category: 'date',
|
|
})
|
|
// Also suggest starting a range
|
|
suggestions.push({
|
|
id: `date-range-start-${partial}`,
|
|
value: `date:${partial}..`,
|
|
label: `${this.formatDateLabel(partial)} to ...`,
|
|
description: 'Start a date range',
|
|
category: 'date',
|
|
})
|
|
}
|
|
return suggestions
|
|
}
|
|
|
|
// Check if user is typing a date pattern
|
|
if (partialDatePattern.test(partial) && partial.length >= 4) {
|
|
suggestions.push({
|
|
id: 'date-custom-hint',
|
|
value: `date:${partial}`,
|
|
label: partial,
|
|
description: 'Continue typing: YYYY, YYYY-MM, or YYYY-MM-DD',
|
|
category: 'date',
|
|
})
|
|
}
|
|
|
|
return suggestions
|
|
}
|
|
|
|
/**
|
|
* Format a date string for display
|
|
*/
|
|
private formatDateLabel(dateStr: string): string {
|
|
const date = new Date(dateStr)
|
|
if (Number.isNaN(date.getTime())) return dateStr
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Match filter values across all definitions
|
|
*/
|
|
private getMatchingFilterValues(query: string): Suggestion[] {
|
|
if (!query.trim()) return []
|
|
|
|
const matches: Suggestion[] = []
|
|
const lowerQuery = query.toLowerCase()
|
|
|
|
for (const filterDef of FILTER_DEFINITIONS) {
|
|
for (const option of filterDef.options) {
|
|
if (
|
|
option.value.toLowerCase().includes(lowerQuery) ||
|
|
option.label.toLowerCase().includes(lowerQuery)
|
|
) {
|
|
matches.push({
|
|
id: `filter-match-${filterDef.key}-${option.value}`,
|
|
value: `${filterDef.key}:${option.value}`,
|
|
label: `${filterDef.label}: ${option.label}`,
|
|
description: option.description,
|
|
category: filterDef.key as any,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return matches.slice(0, 5)
|
|
}
|
|
|
|
/**
|
|
* Match triggers by label (core + integrations)
|
|
*/
|
|
private getMatchingTriggers(query: string): Suggestion[] {
|
|
if (!query.trim()) return []
|
|
|
|
const lowerQuery = query.toLowerCase()
|
|
const allTriggers = this.getAllTriggers()
|
|
|
|
const matches = allTriggers
|
|
.filter((trigger) => trigger.label.toLowerCase().includes(lowerQuery))
|
|
.sort((a, b) => {
|
|
const aLabel = a.label.toLowerCase()
|
|
const bLabel = b.label.toLowerCase()
|
|
|
|
if (aLabel === lowerQuery) return -1
|
|
if (bLabel === lowerQuery) return 1
|
|
if (aLabel.startsWith(lowerQuery) && !bLabel.startsWith(lowerQuery)) return -1
|
|
if (bLabel.startsWith(lowerQuery) && !aLabel.startsWith(lowerQuery)) return 1
|
|
return aLabel.localeCompare(bLabel)
|
|
})
|
|
.slice(0, 8)
|
|
.map((trigger) => ({
|
|
id: `trigger-match-${trigger.value}`,
|
|
value: `trigger:${trigger.value}`,
|
|
label: trigger.label,
|
|
description: `${trigger.label}-triggered runs`,
|
|
category: 'trigger' as const,
|
|
color: trigger.color,
|
|
}))
|
|
|
|
return matches
|
|
}
|
|
|
|
/**
|
|
* Match workflows by name/description
|
|
*/
|
|
private getMatchingWorkflows(query: string): Suggestion[] {
|
|
if (!query.trim() || this.workflowsData.length === 0) return []
|
|
|
|
const lowerQuery = query.toLowerCase()
|
|
|
|
const matches = this.workflowsData
|
|
.filter(
|
|
(workflow) =>
|
|
workflow.name.toLowerCase().includes(lowerQuery) ||
|
|
workflow.description?.toLowerCase().includes(lowerQuery)
|
|
)
|
|
.sort((a, b) => {
|
|
const aName = a.name.toLowerCase()
|
|
const bName = b.name.toLowerCase()
|
|
|
|
if (aName === lowerQuery) return -1
|
|
if (bName === lowerQuery) return 1
|
|
if (aName.startsWith(lowerQuery) && !bName.startsWith(lowerQuery)) return -1
|
|
if (bName.startsWith(lowerQuery) && !aName.startsWith(lowerQuery)) return 1
|
|
return aName.localeCompare(bName)
|
|
})
|
|
.slice(0, 8)
|
|
.map((workflow) => ({
|
|
id: `workflow-match-${workflow.id}`,
|
|
value: `workflow:"${workflow.name}"`,
|
|
label: workflow.name,
|
|
description: workflow.description,
|
|
category: 'workflow' as const,
|
|
}))
|
|
|
|
return matches
|
|
}
|
|
|
|
/**
|
|
* Match folders by name
|
|
*/
|
|
private getMatchingFolders(query: string): Suggestion[] {
|
|
if (!query.trim() || this.foldersData.length === 0) return []
|
|
|
|
const lowerQuery = query.toLowerCase()
|
|
|
|
const matches = this.foldersData
|
|
.filter((folder) => folder.name.toLowerCase().includes(lowerQuery))
|
|
.sort((a, b) => {
|
|
const aName = a.name.toLowerCase()
|
|
const bName = b.name.toLowerCase()
|
|
|
|
if (aName === lowerQuery) return -1
|
|
if (bName === lowerQuery) return 1
|
|
if (aName.startsWith(lowerQuery) && !bName.startsWith(lowerQuery)) return -1
|
|
if (bName.startsWith(lowerQuery) && !aName.startsWith(lowerQuery)) return 1
|
|
return aName.localeCompare(bName)
|
|
})
|
|
.slice(0, 8)
|
|
.map((folder) => ({
|
|
id: `folder-match-${folder.id}`,
|
|
value: `folder:"${folder.name}"`,
|
|
label: folder.name,
|
|
category: 'folder' as const,
|
|
}))
|
|
|
|
return matches
|
|
}
|
|
}
|