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,195 @@
import { DimensionsModal } from '@root/renderer/components/_atoms/dimensions-modal'
import { toast } from '@root/renderer/components/_features/[app]/toast/use-toast'
import { useOpenPLCStore } from '@root/renderer/store'
import { arrayValidation } from '@root/renderer/store/slices/workspace/utils/variables'
import { BaseType, baseTypeSchema } from '@root/types/PLC/open-plc'
import { useEffect, useState } from 'react'

type ArrayModalProps = {
variableName: string
variableRow?: number
arrayModalIsOpen: boolean
setArrayModalIsOpen: (value: boolean) => void
closeContainer: () => void
}

type Pou = { type: string; name: string }
type UserLibWithPous = { pous: Pou[] }
type UserLibFunctionBlock = { type: string; name: string }

export const GlobalArrayModal = ({
arrayModalIsOpen,
closeContainer,
setArrayModalIsOpen,
variableName,
variableRow,
}: ArrayModalProps) => {
const {
project: {
data: {
dataTypes,
configuration: {
resource: { globalVariables },
},
},
},
projectActions: { updateVariable },
libraries: sliceLibraries,
} = useOpenPLCStore()

const baseTypes = baseTypeSchema.options.filter((type) => type.toUpperCase() !== 'ARRAY')

const userDataTypes = dataTypes.map((type) => type.name).filter((typeName) => typeName.toUpperCase() !== 'ARRAY')

const systemFunctionBlocks = sliceLibraries.system.flatMap((lib) =>
lib.pous.filter((pou) => pou.type === 'function-block').map((pou) => pou.name.toUpperCase()),
)

const userFunctionBlocks = sliceLibraries.user.flatMap((userLib: UserLibWithPous | UserLibFunctionBlock) =>
'pous' in userLib && Array.isArray(userLib.pous)
? userLib.pous.filter((pou) => pou.type === 'function-block').map((pou) => pou.name.toUpperCase())
: (userLib as UserLibFunctionBlock).type === 'function-block'
? [(userLib as UserLibFunctionBlock).name.toUpperCase()]
: [],
)

const VariableTypes = [
{ definition: 'base-type', values: baseTypes },
{ definition: 'user-data-type', values: userDataTypes },
]

const LibraryTypes = [
{ definition: 'system', values: systemFunctionBlocks },
{ definition: 'user', values: userFunctionBlocks },
]

const [selectedInput, setSelectedInput] = useState<string>('')
const [dimensions, setDimensions] = useState<string[]>([])
const [typeValue, setTypeValue] = useState<string>('dint')

useEffect(() => {
const variable = globalVariables.find((variable) => variable.name === variableName)
if (!variable) return

if (variable.type.definition === 'array') {
setDimensions(variable.type.data.dimensions.map((dimension) => dimension.dimension))
setTypeValue(variable.type.data.baseType.value)
} else {
setDimensions([])
setTypeValue('dint')
}
}, [variableName, globalVariables])

const handleAddDimension = () => {
setDimensions((prev) => [...prev, ''])
setSelectedInput(dimensions.length.toString())
}
Comment on lines +83 to +86
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Stale closure bug in handleAddDimension.

dimensions.length captures the value at the time the handler was created, not after the state update. Since setDimensions is asynchronous, selectedInput will be set to the old length rather than the new dimension's index.

πŸ› Proposed fix using functional update
 const handleAddDimension = () => {
-  setDimensions((prev) => [...prev, ''])
-  setSelectedInput(dimensions.length.toString())
+  setDimensions((prev) => {
+    setSelectedInput(prev.length.toString())
+    return [...prev, '']
+  })
 }

Alternatively, use a ref or compute the new length explicitly:

 const handleAddDimension = () => {
+  const newIndex = dimensions.length
   setDimensions((prev) => [...prev, ''])
-  setSelectedInput(dimensions.length.toString())
+  setSelectedInput(newIndex.toString())
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleAddDimension = () => {
setDimensions((prev) => [...prev, ''])
setSelectedInput(dimensions.length.toString())
}
const handleAddDimension = () => {
const newIndex = dimensions.length
setDimensions((prev) => [...prev, ''])
setSelectedInput(newIndex.toString())
}
πŸ€– Prompt for AI Agents
In
@src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx
around lines 83 - 86, handleAddDimension has a stale closure: it uses
dimensions.length (captured) immediately after calling setDimensions, so
selectedInput gets the old index; change handleAddDimension to use the
functional updater form for setDimensions (e.g., setDimensions(prev => { const
newIndex = prev.length; const next = [...prev, ''];
setSelectedInput(newIndex.toString()); return next; })) so selectedInput is
derived from prev.length and matches the newly added dimension.


const handleRemoveDimension = (index: string) => {
setDimensions((prev) => [...prev.slice(0, Number(index)), ...prev.slice(Number(index) + 1)])
setSelectedInput('')
}

const handleRearrangeDimensions = (index: number, direction: 'up' | 'down') => {
if (direction === 'up') {
if (index === 0) return
const newDimensions = [...dimensions]
const [removed] = newDimensions.splice(index, 1)
newDimensions.splice(index - 1, 0, removed)
setDimensions(newDimensions)
setSelectedInput((index - 1).toString())
return
}

if (index === dimensions.length - 1) return
const newDimensions = [...dimensions]
const [removed] = newDimensions.splice(index, 1)
newDimensions.splice(index + 1, 0, removed)
setDimensions(newDimensions)
setSelectedInput((index + 1).toString())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const handleUpdateType = (value: BaseType) => {
setTypeValue(value)
}

const handleUpdateDimension = (index: number, value: string): { ok: boolean } => {
const res = arrayValidation({ value: value })
if (!res.ok) {
toast({
title: res.title,
description: res.message,
variant: 'fail',
})
return { ok: false }
}
setDimensions((prev) => [...prev.slice(0, index), value, ...prev.slice(index + 1)])
return { ok: true }
}

const handleInputClick = (value: string) => {
setSelectedInput(value)
}

const handleSave = () => {
const dimensionToSave = dimensions.filter((value) => value !== '')
if (dimensionToSave.length === 0) {
toast({
title: 'Invalid array',
description: 'Array must have at least one not empty dimension',
variant: 'fail',
})
return
}
const formatArrayName = `ARRAY [${dimensionToSave.join(', ')}] OF ${typeValue?.toUpperCase()}`

const isBaseType = baseTypes.includes(typeValue)

updateVariable({
scope: 'global',
rowId: variableRow,
data: {
type: {
definition: 'array',
value: formatArrayName,
data: {
// @ts-expect-error - This is a valid operation. This is being fixed.
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ts-expect-error comment lacks specificity about what is being fixed and when. Consider adding more context such as a ticket number or a more detailed explanation of the type mismatch.

Suggested change
// @ts-expect-error - This is a valid operation. This is being fixed.
// @ts-expect-error - `type.data` in the variable schema does not currently declare a `baseType`
// field, but PLC array types expect this shape at runtime. This suppression can be removed once
// the TypeScript definitions for global variable `type.data` are updated to include `baseType`.

Copilot uses AI. Check for mistakes.
baseType: {
definition: isBaseType ? 'base-type' : 'user-data-type',
value: typeValue,
},
Comment on lines +146 to +160
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

# Search for baseType definition patterns to understand how function blocks are handled
rg -n "definition.*base-type|definition.*user-data-type|definition.*derived" --type ts -B2 -A2

Repository: Autonomy-Logic/openplc-editor

Length of output: 50386


🏁 Script executed:

# Look for how LibraryTypes or function block types are used in array handling
rg -n "LibraryTypes|userFunctionBlocks|systemFunctionBlocks" --type ts src/renderer/components/_molecules/global-variables-table/ -A3 -B3

Repository: Autonomy-Logic/openplc-editor

Length of output: 6674


🏁 Script executed:

# Find the baseTypes definition to understand what types are considered base types
rg -n "baseTypes|baseTypeSchema" --type ts src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx -B5 -A5

Repository: Autonomy-Logic/openplc-editor

Length of output: 1765


🏁 Script executed:

# Check for array variable type handling in POU context (if it exists)
fd "array" --type f src/renderer/components/_molecules/ | head -20

Repository: Autonomy-Logic/openplc-editor

Length of output: 318


🏁 Script executed:

# Search for how variable types are classified in the broader codebase
rg -n "typeValue.*definition|isBaseType" --type ts -C4

Repository: Autonomy-Logic/openplc-editor

Length of output: 8610


🏁 Script executed:

# Check if there are tests or other usages that show function blocks in arrays
rg -n "derived.*function-block|function-block.*derived" --type ts -B2 -A2 | head -30

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


🏁 Script executed:

# Verify the structure of LibraryTypes selection in array-modal
sed -n '180,220p' src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx

Repository: Autonomy-Logic/openplc-editor

Length of output: 563


Handle function block type classification separately.

The logic classifies typeValue as either 'base-type' or 'user-data-type' based solely on whether it exists in baseTypes. However, function blocks selected from LibraryTypes (which are uppercase) will never match baseTypes (lowercase), causing them to be incorrectly assigned definition: 'user-data-type'.

Function block types should use definition: 'derived' instead. Add a check: if typeValue matches any function block from systemFunctionBlocks or userFunctionBlocks, assign 'derived'. Only unmatched non-base types should fall back to 'user-data-type' (for user-defined structs).

πŸ€– Prompt for AI Agents
In
@src/renderer/components/_molecules/global-variables-table/elements/array-modal.tsx
around lines 146 - 160, The code currently sets the inner type definition based
only on baseTypes (isBaseType) which misclassifies function block types; update
the logic where updateVariable is called (the type.data.baseType block using
typeValue and isBaseType) to first check membership in systemFunctionBlocks or
userFunctionBlocks and, if matched, set definition to 'derived', else if
isBaseType set 'base-type', otherwise set 'user-data-type' (preserving value:
typeValue); keep existing surrounding structure (formatArrayName, variableRow,
scope: 'global') unchanged.

dimensions: dimensionToSave.map((dimension) => ({ dimension: dimension })),
},
},
},
})
setArrayModalIsOpen(false)
closeContainer()
}

const handleCancel = () => {
setArrayModalIsOpen(false)
closeContainer()
}

return (
<DimensionsModal
open={arrayModalIsOpen}
onOpenChange={setArrayModalIsOpen}
onCancel={handleCancel}
onSave={handleSave}
typeValue={typeValue}
onTypeChange={handleUpdateType}
dimensions={dimensions}
selectedInput={selectedInput}
onAddDimension={handleAddDimension}
onRemoveDimension={handleRemoveDimension}
onRearrangeDimensions={handleRearrangeDimensions}
onInputClick={handleInputClick}
onUpdateDimension={handleUpdateDimension}
variableTypes={VariableTypes}
libraryTypes={LibraryTypes}
hideTrigger
/>
)
}
Loading