mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(files): unstick monaco find widget tooltips and surface logs in mothership add-resource (#4395)
This commit is contained in:
+2
-1
@@ -9,7 +9,7 @@ import {
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/emcn'
|
||||
import { Clipboard, Copy, Search } from '@/components/emcn/icons'
|
||||
import { Clipboard, Copy, Search, SelectAll } from '@/components/emcn/icons'
|
||||
|
||||
interface EditorContextMenuProps {
|
||||
isOpen: boolean
|
||||
@@ -88,6 +88,7 @@ export function EditorContextMenu({
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={onSelectAll}>
|
||||
<SelectAll />
|
||||
Select all
|
||||
<DropdownMenuShortcut>⌘A</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -219,6 +219,27 @@ const SPLIT_MIN_PCT = 20
|
||||
const SPLIT_MAX_PCT = 80
|
||||
const SPLIT_DEFAULT_PCT = 50
|
||||
|
||||
/**
|
||||
* Monaco's find-widget button hover tooltips render above the button by default. Because the find
|
||||
* widget sits at the very top of the editor, the tooltip overlaps the button itself —
|
||||
* `document.elementFromPoint` at the button's center returns `.hover-contents` instead of the
|
||||
* button, which fires `mouseleave` on the button. Monaco then disposes the tooltip, the cursor
|
||||
* lands back on the button, and the cycle repeats every ~210ms, making the buttons unclickable.
|
||||
*
|
||||
* Translate the tooltip's `.context-view` below the button so the cursor stays on the button
|
||||
* while the tooltip is shown, and flip the carat to point up at the button.
|
||||
*/
|
||||
const FIND_TOOLTIP_FIX_CSS = `
|
||||
[data-find-tooltip-fix] .context-view.top.left {
|
||||
transform: translateY(56px);
|
||||
}
|
||||
[data-find-tooltip-fix] .context-view.top.left .workbench-hover-pointer.bottom {
|
||||
top: -3px;
|
||||
bottom: auto;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
`
|
||||
|
||||
/** Maps file extensions to Monaco editor language IDs. */
|
||||
const MONACO_LANGUAGE_BY_EXTENSION: Partial<Record<string, string>> = {
|
||||
js: 'javascript',
|
||||
@@ -663,7 +684,8 @@ export const TextEditor = memo(function TextEditor({
|
||||
const closeContextMenu = () => setContextMenu(null)
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='relative flex flex-1 overflow-hidden'>
|
||||
<div ref={containerRef} data-find-tooltip-fix className='relative flex flex-1 overflow-hidden'>
|
||||
<style>{FIND_TOOLTIP_FIX_CSS}</style>
|
||||
{showEditor && (
|
||||
<div
|
||||
style={showPreviewPane ? { width: `${splitPct}%`, flexShrink: 0 } : undefined}
|
||||
|
||||
+52
-1
@@ -24,8 +24,10 @@ import type {
|
||||
MothershipResource,
|
||||
MothershipResourceType,
|
||||
} from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils'
|
||||
import { useFolders } from '@/hooks/queries/folders'
|
||||
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
|
||||
import { useLogsList } from '@/hooks/queries/logs'
|
||||
import { useTablesList } from '@/hooks/queries/tables'
|
||||
import { useTasks } from '@/hooks/queries/tasks'
|
||||
import { useWorkflows } from '@/hooks/queries/workflows'
|
||||
@@ -47,6 +49,18 @@ interface AvailableItemsByType {
|
||||
items: AvailableItem[]
|
||||
}
|
||||
|
||||
const LOG_DROPDOWN_LIMIT = 50
|
||||
|
||||
const LOG_DROPDOWN_FILTERS = {
|
||||
timeRange: 'All time' as const,
|
||||
level: 'all',
|
||||
workflowIds: [] as string[],
|
||||
folderIds: [] as string[],
|
||||
triggers: [] as string[],
|
||||
searchQuery: '',
|
||||
limit: LOG_DROPDOWN_LIMIT,
|
||||
}
|
||||
|
||||
export function useAvailableResources(
|
||||
workspaceId: string,
|
||||
existingKeys: Set<string>,
|
||||
@@ -58,6 +72,13 @@ export function useAvailableResources(
|
||||
const { data: knowledgeBases } = useKnowledgeBasesQuery(workspaceId)
|
||||
const { data: folders = [] } = useFolders(workspaceId)
|
||||
const { data: tasks = [] } = useTasks(workspaceId)
|
||||
const { data: logsData } = useLogsList(workspaceId, LOG_DROPDOWN_FILTERS)
|
||||
const logs = useMemo(() => (logsData?.pages ?? []).flatMap((page) => page.logs), [logsData])
|
||||
const workflowColorById = useMemo(() => {
|
||||
const map = new Map<string, string>()
|
||||
for (const w of workflows) map.set(w.id, w.color)
|
||||
return map
|
||||
}, [workflows])
|
||||
|
||||
return useMemo(() => {
|
||||
const excluded = new Set<MothershipResourceType>(excludeTypes ?? [])
|
||||
@@ -115,9 +136,39 @@ export function useAvailableResources(
|
||||
isOpen: existingKeys.has(`task:${t.id}`),
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'log' as const,
|
||||
items: logs.map((log) => {
|
||||
const workflowName = log.workflow?.name ?? log.workflowId ?? 'Unknown'
|
||||
const color =
|
||||
log.workflow?.color ??
|
||||
(log.workflowId ? workflowColorById.get(log.workflowId) : undefined) ??
|
||||
'#888'
|
||||
const time = formatDate(log.createdAt).compact
|
||||
return {
|
||||
id: log.id,
|
||||
name: `${workflowName} · ${time}`,
|
||||
workflowName,
|
||||
color,
|
||||
time,
|
||||
isOpen: existingKeys.has(`log:${log.id}`),
|
||||
}
|
||||
}),
|
||||
},
|
||||
]
|
||||
return groups.filter((g) => !excluded.has(g.type))
|
||||
}, [workflows, folders, tables, files, knowledgeBases, tasks, existingKeys, excludeTypes])
|
||||
}, [
|
||||
workflows,
|
||||
folders,
|
||||
tables,
|
||||
files,
|
||||
knowledgeBases,
|
||||
tasks,
|
||||
logs,
|
||||
workflowColorById,
|
||||
existingKeys,
|
||||
excludeTypes,
|
||||
])
|
||||
}
|
||||
|
||||
export type WorkflowTreeNode =
|
||||
|
||||
+25
-1
@@ -100,6 +100,30 @@ function IconDropdownItem({ item, icon: Icon }: DropdownItemRenderProps & { icon
|
||||
)
|
||||
}
|
||||
|
||||
function LogDropdownItem({ item }: DropdownItemRenderProps) {
|
||||
const color = (item.color as string) ?? '#888'
|
||||
const workflowName = (item.workflowName as string) ?? item.name
|
||||
const time = (item.time as string) ?? ''
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px] border-[2px]'
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderColor: workflowBorderColor(color),
|
||||
backgroundClip: 'padding-box',
|
||||
}}
|
||||
/>
|
||||
<span className='truncate'>{workflowName}</span>
|
||||
{time && (
|
||||
<span className='ml-auto flex-shrink-0 text-[var(--text-tertiary)] text-caption'>
|
||||
{time}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const RESOURCE_REGISTRY: Record<MothershipResourceType, ResourceTypeConfig> = {
|
||||
generic: {
|
||||
type: 'generic',
|
||||
@@ -172,7 +196,7 @@ export const RESOURCE_REGISTRY: Record<MothershipResourceType, ResourceTypeConfi
|
||||
renderTabIcon: (_resource, className) => (
|
||||
<Library className={cn(className, 'text-[var(--text-icon)]')} />
|
||||
),
|
||||
renderDropdownItem: (props) => <IconDropdownItem {...props} icon={Library} />,
|
||||
renderDropdownItem: (props) => <LogDropdownItem {...props} />,
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
+1
-5
@@ -41,11 +41,7 @@ import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
|
||||
const EDGE_ZONE = 40
|
||||
const SCROLL_SPEED = 8
|
||||
|
||||
const ADD_RESOURCE_EXCLUDED_TYPES: readonly MothershipResourceType[] = [
|
||||
'folder',
|
||||
'task',
|
||||
'log',
|
||||
] as const
|
||||
const ADD_RESOURCE_EXCLUDED_TYPES: readonly MothershipResourceType[] = ['folder', 'task'] as const
|
||||
|
||||
/**
|
||||
* Returns the id of the nearest resource to `idx` that is in `filter`
|
||||
|
||||
@@ -65,6 +65,7 @@ export { RefreshCw } from './refresh-cw'
|
||||
export { Rocket } from './rocket'
|
||||
export { Rows3 } from './rows3'
|
||||
export { Search } from './search'
|
||||
export { SelectAll } from './select-all'
|
||||
export { Send } from './send'
|
||||
export { Server } from './server'
|
||||
export { Settings } from './settings'
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
/**
|
||||
* SelectAll icon component - four L-shaped corner brackets indicating a selection region
|
||||
* @param props - SVG properties including className, fill, etc.
|
||||
*/
|
||||
export function SelectAll(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
aria-hidden='true'
|
||||
{...props}
|
||||
>
|
||||
<path d='M8 4H4V8' />
|
||||
<path d='M16 4H20V8' />
|
||||
<path d='M20 16V20H16' />
|
||||
<path d='M8 20H4V16' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user