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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "open-plc-editor",
"description": "OpenPLC Editor - IDE capable of creating programs for the OpenPLC Runtime",
"version": "4.2.9",
"version": "4.2.10",
"license": "GPL-3.0",
"author": {
"name": "Autonomy Logic"
Expand Down
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
@@ -1,3 +1,3 @@
import * as Popover from '@radix-ui/react-popover'
import { ComponentPropsWithoutRef, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'

Expand Down Expand Up @@ -35,6 +35,7 @@
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 @@
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 @@
)}
onDoubleClick={() => !isDebuggerVisible && setIsEditing(true)}
>
{handleLabel(label) || ''}
<HighlightedText text={handleLabel(label) || ''} searchQuery={highlightQuery} />
</span>
)}
</div>
Expand Down Expand Up @@ -455,6 +463,12 @@
| '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 @@
leafLang,
leafType,
label,
highlightQuery,
busName,
deviceId,
onClick: handleLeafClick,
Expand Down Expand Up @@ -762,7 +777,7 @@
)}
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
@@ -1,10 +1,9 @@
import { useEffect, useState } from 'react'

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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
2 changes: 1 addition & 1 deletion src/frontend/data/constants/app-version.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/**
* Single source of truth for the OpenPLC application version, SHARED
* byte-for-byte between openplc-web and openplc-editor.
Expand All @@ -18,4 +18,4 @@
* compares a per-deploy `BUILD_ID` (git commit SHA), not this version, so a
* stale tab is detected on every deploy even without a version bump.
*/
export const APP_VERSION = '4.2.9'
export const APP_VERSION = '4.2.10'
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,15 @@ describe('updateVariableValidation', () => {
expect(result.ok).toBe(true)
})

it.each(['%QX0.0', '%IX0.0', '%MX0.0', '%MX3.7'])(
'accepts every valid IEC area prefix for BOOL locations (%s)',
(location) => {
const boolVar = makeVariable('Test', 'BOOL', '')
const result = updateVariableValidation([], { location }, boolVar)
expect(result.ok).toBe(true)
},
)

it('returns ok: true when location is valid for WORD type', () => {
const wordVar = makeVariable('Test', 'WORD', '')
const result = updateVariableValidation([], { location: '%QW0' }, wordVar)
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/store/slices/project/validation/variables.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import type { PLCVariable } from '../../../../../middleware/shared/ports/types'
import { DISALLOWED_LOCATION_CLASSES } from '../../../../utils/generate-iec-string-to-variables'
import {
Expand Down Expand Up @@ -153,7 +153,7 @@
const variableLocationValidationErrorMessage = (variableType: string) => {
switch (variableType.toUpperCase()) {
case 'BOOL':
return 'Valid locations: %QX0.0..7, %IX0.0..7 (change the number to the desired location)'
return 'Valid locations: %QX0.0..7, %IX0.0..7, %MX0.0..7 (change the number to the desired location)'
case 'INT':
case 'UINT':
case 'WORD':
Expand Down
6 changes: 5 additions & 1 deletion src/frontend/utils/PLC/address-constants/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const PLC_ADDRESS_PREFIX = {
BOOL_OUTPUT: '%QX',
BOOL_INPUT: '%IX',
BOOL_MEMORY: '%MX',
WORD_OUTPUT: '%QW',
WORD_INPUT: '%IW',
WORD_MEMORY: '%MW',
Expand All @@ -12,7 +13,10 @@
LWORD_MEMORY: '%ML',
} as const

export const BOOL_LOCATION_REGEX = /^%[QI]X\d+\.\d$/
// Accept every valid IEC area prefix — input (I), output (Q) and memory (M).
// `%MX` memory bits are legitimate (e.g. Modbus coils) and were previously
// rejected here, unlike the word-width regexes below which already allow M.
export const BOOL_LOCATION_REGEX = /^%[QIM]X\d+\.\d$/
export const WORD_LOCATION_REGEX = /^%[QIM]W\d+$/
export const DWORD_LOCATION_REGEX = /^%[QIM]D\d+$/
export const LWORD_LOCATION_REGEX = /^%[QIM]L\d+$/
Loading