Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { fireEvent, render, screen } from '@testing-library/react'

import { useOpenPLCStore } from '../../../../store'
import { ProjectTreeExpandableLeaf, ProjectTreeLeaf } from '../index'

// https://github.com/Autonomy-Logic/openplc-editor/issues/640
// The explorer used to pass a search-highlight HTML string as the leaf `label`,
// which ProjectTreeLeaf renders as plain text — after any project search, the
// matching POU's name appeared in the tree as literal markup
// (`<span class="bg-brand-light...`). The label must stay the element's real
// name; highlighting is applied safely inside the leaf via `highlightQuery`.
describe('ProjectTreeLeaf search highlight', () => {
it('renders the plain name and applies a safe highlight when highlightQuery matches', () => {
const { container } = render(
<ProjectTreeLeaf leafLang='fbd' leafType='function-block' label='Taktgeber' highlightQuery='Takt' />,
)

expect(screen.getByText(/Takt/).textContent).not.toContain('<span')
expect(container.textContent).toBe('Taktgeber')

const highlight = container.querySelector('span.bg-brand-light')
expect(highlight).not.toBeNull()
expect(highlight?.textContent).toBe('Takt')
})

it('renders the plain name without highlight markup when there is no query', () => {
const { container } = render(<ProjectTreeLeaf leafLang='fbd' leafType='function-block' label='Taktgeber' />)

expect(container.textContent).toBe('Taktgeber')
expect(container.querySelector('span.bg-brand-light')).toBeNull()
})

it('never shows literal markup for a non-matching query', () => {
const { container } = render(
<ProjectTreeLeaf leafLang='fbd' leafType='function-block' label='Taktgeber' highlightQuery='zzz' />,
)

expect(container.textContent).toBe('Taktgeber')
expect(container.textContent).not.toContain('<span')
})

it('keeps the plain name as the element identity while highlighted', () => {
const { container } = render(
<ProjectTreeLeaf leafLang='fbd' leafType='function-block' label='Taktgeber' highlightQuery='Takt' />,
)

const leaf = container.querySelector('li')
expect(leaf).not.toBeNull()
if (leaf) fireEvent.click(leaf)

// Selection must record the real POU name, never the decorated display string.
const { selectedProjectTreeLeaf } = useOpenPLCStore.getState().workspace
expect(selectedProjectTreeLeaf.label).toBe('Taktgeber')
})

it('applies the same safe highlighting on expandable leaves', () => {
const { container } = render(
<ProjectTreeExpandableLeaf
leafLang='remoteDevice'
leafType='remote-device'
label='EtherBus'
highlightQuery='Ether'
/>,
)

expect(container.textContent).toContain('EtherBus')
expect(container.textContent).not.toContain('<span')
const highlight = container.querySelector('span.bg-brand-light')
expect(highlight?.textContent).toBe('Ether')
})
})
19 changes: 17 additions & 2 deletions src/frontend/components/_molecules/project-tree/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { useOpenPLCStore } from '../../../store'
import { WorkspaceProjectTreeLeafType } from '../../../store/slices/workspace/types'
import { cn } from '../../../utils/cn'
import { isUnsaved, unsavedLabel } from '../../../utils/unsaved-label'
import { HighlightedText } from '../../_atoms/highlighted-text'
import { toast } from '../../_features/[app]/toast/use-toast'

const pousAllLanguages = ['il', 'st', 'python', 'cpp', 'ld', 'sfc', 'fbd'] as const
Expand Down Expand Up @@ -245,13 +246,20 @@ type IProjectTreeExpandableLeafProps = ComponentPropsWithoutRef<'li'> & {
leafLang: IProjectTreeLeafProps['leafLang']
leafType: WorkspaceProjectTreeLeafType
label?: string
/**
* Search query used only to highlight matches in the displayed label.
* Kept separate from `label`, which must stay the element's real name:
* rename/duplicate/delete and selection all use `label` as identity.
*/
highlightQuery?: string
children?: ReactNode
}

const ProjectTreeExpandableLeaf = ({
leafLang,
leafType,
label,
highlightQuery,
children,
onClick: handleLeafClick,
...res
Expand Down Expand Up @@ -373,7 +381,7 @@ const ProjectTreeExpandableLeaf = ({
)}
onDoubleClick={() => !isDebuggerVisible && setIsEditing(true)}
>
{handleLabel(label) || ''}
<HighlightedText text={handleLabel(label) || ''} searchQuery={highlightQuery} />
</span>
)}
</div>
Expand Down Expand Up @@ -455,6 +463,12 @@ type IProjectTreeLeafProps = ComponentPropsWithoutRef<'li'> & {
| 'userManagement'
leafType: WorkspaceProjectTreeLeafType
label?: string
/**
* Search query used only to highlight matches in the displayed label.
* Kept separate from `label`, which must stay the element's real name:
* rename/duplicate/delete and selection all use `label` as identity.
*/
highlightQuery?: string
busName?: string
deviceId?: string
}
Expand Down Expand Up @@ -492,6 +506,7 @@ const ProjectTreeLeaf = ({
leafLang,
leafType,
label,
highlightQuery,
busName,
deviceId,
onClick: handleLeafClick,
Expand Down Expand Up @@ -762,7 +777,7 @@ const ProjectTreeLeaf = ({
)}
onDoubleClick={() => !isDebuggerVisible && setIsEditing(true)}
>
{handleLabel(label) || ''}
<HighlightedText text={handleLabel(label) || ''} searchQuery={highlightQuery} />
</span>
)}

Expand Down
31 changes: 20 additions & 11 deletions src/frontend/components/_organisms/explorer/project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { projectCapabilities } from '../../../../middleware/shared/ports/types'
import { useCapabilities, useProject } from '../../../../middleware/shared/providers'
import { FolderIcon } from '../../../assets/icons/interface/Folder'
import { useOpenPLCStore } from '../../../store'
import { extractSearchQuery } from '../../../store/slices/search/utils'
import type { TabsProps } from '../../../store/slices/tabs'
import { CreateEditorObjectFromTab, LIBRARY_MANIFEST_TAB_NAME } from '../../../store/slices/tabs/utils'
import { isUserManagementCapableRuntime } from '../../../utils/device'
Expand Down Expand Up @@ -212,7 +211,8 @@ const Project = () => {
key={pou.name}
leafLang={pou.body.language as PouLeafLang}
leafType='function'
label={searchQuery ? extractSearchQuery(pou.name, searchQuery) : pou.name}
label={pou.name}
highlightQuery={searchQuery}
onClick={() =>
handleCreateTab({
name: pou.name,
Expand All @@ -234,7 +234,8 @@ const Project = () => {
key={pou.name}
leafLang={pou.body.language as PouLeafLang}
leafType='function-block'
label={searchQuery ? extractSearchQuery(pou.name, searchQuery) : pou.name}
label={pou.name}
highlightQuery={searchQuery}
onClick={() =>
handleCreateTab({
name: pou.name,
Expand All @@ -258,7 +259,8 @@ const Project = () => {
key={pou.name}
leafLang={pou.body.language as PouLeafLang}
leafType='program'
label={searchQuery ? extractSearchQuery(pou.name, searchQuery) : pou.name}
label={pou.name}
highlightQuery={searchQuery}
onClick={() =>
handleCreateTab({
name: pou.name,
Expand All @@ -281,7 +283,8 @@ const Project = () => {
key={name}
leafLang='arr'
leafType='data-type'
label={searchQuery ? extractSearchQuery(name, searchQuery) : name}
label={name}
highlightQuery={searchQuery}
onClick={() =>
handleCreateTab({
name,
Expand All @@ -299,7 +302,8 @@ const Project = () => {
key={name}
leafLang='enum'
leafType='data-type'
label={searchQuery ? extractSearchQuery(name, searchQuery) : name}
label={name}
highlightQuery={searchQuery}
/** Todo: Update the tab state */
onClick={() =>
handleCreateTab({
Expand All @@ -318,7 +322,8 @@ const Project = () => {
key={name}
leafLang='str'
leafType='data-type'
label={searchQuery ? extractSearchQuery(name, searchQuery) : name}
label={name}
highlightQuery={searchQuery}
/** Todo: Update the tab state */
onClick={() =>
handleCreateTab({
Expand Down Expand Up @@ -432,7 +437,8 @@ const Project = () => {
key={server.name}
leafLang='server'
leafType='server'
label={searchQuery ? extractSearchQuery(server.name, searchQuery) : server.name}
label={server.name}
highlightQuery={searchQuery}
onClick={() =>
handleCreateTab({
name: server.name,
Expand All @@ -456,7 +462,8 @@ const Project = () => {
key={device.name}
leafLang='remoteDevice'
leafType='remote-device'
label={searchQuery ? extractSearchQuery(device.name, searchQuery) : device.name}
label={device.name}
highlightQuery={searchQuery}
onClick={() =>
handleCreateTab({
name: device.name,
Expand All @@ -472,7 +479,8 @@ const Project = () => {
leafType='ethercat-device'
busName={device.name}
deviceId={child.id}
label={searchQuery ? extractSearchQuery(child.name, searchQuery) : child.name}
label={child.name}
highlightQuery={searchQuery}
onClick={() =>
handleCreateTab({
name: child.name,
Expand All @@ -488,7 +496,8 @@ const Project = () => {
key={device.name}
leafLang='remoteDevice'
leafType='remote-device'
label={searchQuery ? extractSearchQuery(device.name, searchQuery) : device.name}
label={device.name}
highlightQuery={searchQuery}
onClick={() =>
handleCreateTab({
name: device.name,
Expand Down
Loading