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
20 changes: 9 additions & 11 deletions src/frontend/components/_features/[workspace]/data-type/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { ComponentPropsWithoutRef, useEffect, useState } from 'react'

import type { PLCDataType } from '../../../../../middleware/shared/ports/types'
Expand All @@ -7,6 +7,7 @@
import { ArrayDataType } from '../../../_molecules/data-types/array'
import { EnumeratorDataType } from '../../../_molecules/data-types/enumerated'
import { StructureDataType } from '../../../_molecules/data-types/structure'
import { toast } from '../../[app]/toast/use-toast'

type DatatypeEditorProps = ComponentPropsWithoutRef<'div'> & {
dataTypeName: string
Expand All @@ -17,9 +18,7 @@
project: {
data: { dataTypes },
},
tabsActions: { updateTabName },
editorActions: { updateEditorModel },
projectActions: { updateDatatype },
datatypeActions: { rename },
searchQuery,
} = useOpenPLCStore()
const [editorContent, setEditorContent] = useState<PLCDataType>()
Expand Down Expand Up @@ -52,14 +51,13 @@
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const { value } = e.target
if (dataTypeName !== value) {
// `updateDatatype` is a full replace. Spread the current
// entry so the rename only changes `name` — without this the
// entry would lose every other field (variable / values /
// dimensions / baseType / initialValue).
if (!editorContent) return
updateDatatype(dataTypeName, { ...editorContent, name: value })
updateEditorModel(dataTypeName, value)
updateTabName(dataTypeName, value)
// `datatypeActions.rename` validates the new name and rekeys the
// editor model, tab, and file entry, then flags the file dirty.
const result = rename(dataTypeName, value)
if (!result.ok) {
setEditorContent((prevContent) => (prevContent ? { ...prevContent, name: dataTypeName } : prevContent))
toast({ title: 'Rename failed', description: result.message, variant: 'fail' })
}
setIsEditing(false)
}
}
Expand Down
41 changes: 31 additions & 10 deletions src/frontend/components/_molecules/data-types/array/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeEvent, ComponentPropsWithoutRef, useEffect, useState } from 'react'
import { ChangeEvent, ComponentPropsWithoutRef, useEffect, useRef, useState } from 'react'

import { baseTypeEnum } from '../../../../../middleware/shared/ports/plc-schemas'
import type { PLCDataType } from '../../../../../middleware/shared/ports/types'
Expand Down Expand Up @@ -30,6 +30,7 @@
data: { dataTypes },
},
libraries: sliceLibraries,
sharedWorkspaceActions: { handleFileAndWorkspaceSavedState },
} = useOpenPLCStore()

const { captureAndPush } = usePouSnapshot()
Expand Down Expand Up @@ -72,7 +73,7 @@
const ROWS_NOT_SELECTED = -1

const [arrayTable, setArrayTable] = useState<{ selectedRow: number }>({ selectedRow: ROWS_NOT_SELECTED })
const [initialValueData, setInitialValueData] = useState<string>('')
const [initialValueData, setInitialValueData] = useState<string>(data.initialValue || '')
const [baseType, setBaseType] = useState<string>(data.baseType.value)

const [tableData, setTableData] = useState<PLCArrayDatatype['dimensions']>([])
Expand All @@ -81,46 +82,63 @@
setTableData(data.dimensions)
}, [data.dimensions])

// One history entry per typing burst: armed on the first keystroke,
// rearmed on blur or when the store value changes under us (undo/redo).
const initialValueCaptured = useRef(false)
// `data` lags one render behind the store (the parent copies it via effect),
// so compare against our own last write to spot genuinely external changes.
const lastWrittenInitialValue = useRef(data.initialValue || '')

useEffect(() => {
setInitialValueData(data.initialValue || '')
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const storeValue = data.initialValue || ''
if (storeValue !== lastWrittenInitialValue.current) {
setInitialValueData(storeValue)
lastWrittenInitialValue.current = storeValue
initialValueCaptured.current = false
}
}, [data.initialValue])

useEffect(() => {
setBaseType(data.baseType.value)
}, [data.baseType])

const handleInitialValueChange = (e: ChangeEvent<HTMLInputElement>) => {
setInitialValueData(e.target.value)
lastWrittenInitialValue.current = e.target.value
if (!initialValueCaptured.current) {
captureAndPush(editor.meta.name)
initialValueCaptured.current = true
}
const updatedData = { ...data }
updatedData.initialValue = e.target.value
updateDatatype(data.name, updatedData as PLCArrayDatatype)
handleFileAndWorkspaceSavedState(editor.meta.name)
}

const handleSelect = (definition: string, value: string) => {
setBaseType(value)
captureAndPush(editor.meta.name)
updateDatatype(data.name, {
...data,
baseType: { value, definition },
} as PLCArrayDatatype)
handleFileAndWorkspaceSavedState(editor.meta.name)
}

// `updateDatatype` is a full replace — never pass a partial object,
// or the rest of the datatype (`name`, `derivation`, `baseType`, …)
// gets stripped and downstream selectors lose the entry.
const writeDimensions = (newRows: PLCArrayDatatype['dimensions']) => {
updateDatatype(data.name, { ...data, dimensions: newRows })
handleFileAndWorkspaceSavedState(editor.meta.name)
}

const addNewRow = () => {
captureAndPush(editor.meta.name)

setTableData((prevRows) => {
const isFirst = prevRows.length === 0
const newRows = [...prevRows, { dimension: '' }]

if (isFirst) {
captureAndPush(editor.meta.name)
}

setArrayTable({ selectedRow: newRows.length - 1 })
writeDimensions(newRows)
return newRows
Expand Down Expand Up @@ -211,6 +229,9 @@
</label>
<InputWithRef
onChange={handleInitialValueChange}
onBlur={() => {
initialValueCaptured.current = false
}}
value={initialValueData}
className='flex h-7 w-full max-w-44 items-center justify-between gap-2 rounded-lg border border-neutral-400 bg-white px-3 py-2 font-caption text-xs font-normal text-neutral-950 focus-within:border-brand focus:border-brand focus:outline-none dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-100'
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import React, { useEffect, useRef } from 'react'

Expand Down Expand Up @@ -36,6 +36,7 @@
data: { dataTypes },
},
projectActions: { updateDatatype },
sharedWorkspaceActions: { handleFileAndWorkspaceSavedState },
} = useOpenPLCStore()

const { captureAndPush } = usePouSnapshot()
Expand All @@ -48,6 +49,7 @@
const current = dataTypes.find((dt) => dt.name === name)
if (!current || current.derivation !== 'array') return
updateDatatype(name, { ...current, dimensions: newDimensions })
handleFileAndWorkspaceSavedState(editor.meta.name)
}

const columnHelper = createColumnHelper<{ dimension: string }>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { ComponentPropsWithoutRef, useEffect, useState } from 'react'

import type { PLCDataType } from '../../../../../middleware/shared/ports/types'
Expand All @@ -19,6 +19,7 @@
const {
editor,
projectActions: { updateDatatype },
sharedWorkspaceActions: { handleFileAndWorkspaceSavedState },
} = useOpenPLCStore()

const { captureAndPush } = usePouSnapshot()
Expand All @@ -32,8 +33,7 @@

useEffect(() => {
setInitialValueData(data.initialValue || '')
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, [data.initialValue])

useEffect(() => {
setTableData(data.values)
Expand All @@ -46,13 +46,15 @@
...data,
initialValue: value,
})
handleFileAndWorkspaceSavedState(editor.meta.name)
}

// `updateDatatype` is a full replace — spread `data` first so we
// don't strip `name` / `derivation` and corrupt the entry for
// downstream consumers.
const writeValues = (newValues: PLCEnumeratedDatatype['values']) => {
updateDatatype(data.name, { ...data, values: newValues })
handleFileAndWorkspaceSavedState(editor.meta.name)
}

const addNewRow = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import React, { useEffect, useRef } from 'react'

Expand Down Expand Up @@ -38,6 +38,7 @@
data: { dataTypes },
},
projectActions: { updateDatatype },
sharedWorkspaceActions: { handleFileAndWorkspaceSavedState },
} = useOpenPLCStore()

const { captureAndPush } = usePouSnapshot()
Expand All @@ -50,6 +51,7 @@
const current = dataTypes.find((dt) => dt.name === name)
if (!current || current.derivation !== 'enumerated') return
updateDatatype(name, { ...current, values: newValues })
handleFileAndWorkspaceSavedState(editor.meta.name)
}

const columnHelper = createColumnHelper<{ description: string }>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { useEffect, useState } from 'react'

import type { PLCStructureVariable } from '../../../../../middleware/shared/ports/types'
Expand All @@ -21,6 +21,7 @@
},
editorActions: { updateModelStructure },
projectActions: { updateDatatype, rearrangeStructureVariables },
sharedWorkspaceActions: { handleFileAndWorkspaceSavedState },
} = useOpenPLCStore()

const { captureAndPush } = usePouSnapshot()
Expand Down Expand Up @@ -65,6 +66,7 @@
const current = dataTypes.find((dt) => dt.name === editor.meta.name)
if (!current || current.derivation !== 'structure') return
updateDatatype(editor.meta.name, { ...current, variable: newVariables })
handleFileAndWorkspaceSavedState(editor.meta.name)
}

const handleCreateStructureVariable = () => {
Expand Down Expand Up @@ -178,6 +180,7 @@
rowId: row ?? parseInt(editorStructure.selectedRow),
newIndex: (row ?? parseInt(editorStructure.selectedRow)) + index,
})
handleFileAndWorkspaceSavedState(editor.meta.name)
updateModelStructure({
selectedRow: parseInt(editorStructure.selectedRow) + index,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { createColumnHelper } from '@tanstack/react-table'

import type { PLCStructureVariable } from '../../../../../../middleware/shared/ports/types'
Expand Down Expand Up @@ -51,6 +51,7 @@
data: { dataTypes },
},
projectActions: { updateDatatype },
sharedWorkspaceActions: { handleFileAndWorkspaceSavedState },
} = useOpenPLCStore()

const { captureAndPush } = usePouSnapshot()
Expand Down Expand Up @@ -85,6 +86,7 @@
return variable
}),
})
handleFileAndWorkspaceSavedState(editor.meta.name)
return { ok: true, message: 'Data updated successfully.' }
} catch (error) {
console.error('Failed to update data:', error)
Expand Down
66 changes: 66 additions & 0 deletions src/frontend/hooks/__tests__/use-pou-snapshot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { renderHook } from '@testing-library/react'

import { useOpenPLCStore } from '../../store'
import { usePouSnapshot } from '../use-pou-snapshot'

describe('usePouSnapshot', () => {
describe('captureAndPush', () => {
it('captures a data type snapshot keyed by the data type name', () => {
const created = useOpenPLCStore.getState().datatypeActions.create({
name: 'CaptureColors',
derivation: 'enumerated',
})
expect(created.ok).toBe(true)

const { result } = renderHook(() => usePouSnapshot())
result.current.captureAndPush('CaptureColors')

const bucket = useOpenPLCStore.getState().undoRedo['CaptureColors']
expect(bucket.past).toHaveLength(1)
expect(bucket.past[0].variables).toEqual([])
expect(bucket.past[0].body).toBeNull()
expect(bucket.past[0].dataTypes).toEqual([
expect.objectContaining({ name: 'CaptureColors', derivation: 'enumerated' }),
])
})

it('captures the current data type state, not the creation-time state', () => {
useOpenPLCStore.getState().datatypeActions.create({ name: 'CaptureDims', derivation: 'array' })
const current = useOpenPLCStore.getState().project.data.dataTypes.find((d) => d.name === 'CaptureDims')
if (!current || current.derivation !== 'array') throw new Error('CaptureDims array data type missing')
useOpenPLCStore.getState().projectActions.updateDatatype('CaptureDims', {
...current,
dimensions: [{ dimension: '0..7' }],
})

const { result } = renderHook(() => usePouSnapshot())
result.current.captureAndPush('CaptureDims')

const bucket = useOpenPLCStore.getState().undoRedo['CaptureDims']
expect(bucket.past[0].dataTypes).toEqual([
expect.objectContaining({ name: 'CaptureDims', dimensions: [{ dimension: '0..7' }] }),
])
})

it('captures a POU snapshot for POU names', () => {
useOpenPLCStore.getState().pouActions.create({ type: 'program', name: 'CaptureMain', language: 'st' })

const { result } = renderHook(() => usePouSnapshot())
result.current.captureAndPush('CaptureMain')

const bucket = useOpenPLCStore.getState().undoRedo['CaptureMain']
expect(bucket.past).toHaveLength(1)
expect(bucket.past[0].dataTypes).toBeUndefined()
const pou = useOpenPLCStore.getState().project.data.pous.find((p) => p.name === 'CaptureMain')
if (!pou) throw new Error('CaptureMain POU missing')
expect(bucket.past[0].body).toBe(pou.body.value)
})

it('is a no-op for names matching neither a POU nor a data type', () => {
const { result } = renderHook(() => usePouSnapshot())
result.current.captureAndPush('CaptureGhost')

expect(useOpenPLCStore.getState().undoRedo['CaptureGhost']).toBeUndefined()
})
})
})
9 changes: 8 additions & 1 deletion src/frontend/hooks/use-pou-snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { useCallback } from 'react'

import { useOpenPLCStore } from '../store'
Expand All @@ -7,6 +7,8 @@
* Convenience hook wrapping snapshotActions.pushToHistory().
* Captures the current POU state (variables, body, globalVariables, and
* graphical flow state for LD/FBD) and pushes it to the undo history.
* Names that resolve to a data type instead of a POU capture the data
* type entry (`dataTypes`) so datatype editors share the same history.
*
* State is read via getState() at capture time (not subscribed): the hook
* never re-renders its consumers and `captureAndPush` keeps a stable identity.
Expand All @@ -29,7 +31,12 @@
if (flushFlowWriteBacks(useOpenPLCStore.getState, pouName).length > 0) return
const { project, ladderFlows, fbdFlows } = useOpenPLCStore.getState()
const pou = project.data.pous.find((p) => p.name === pouName)
if (!pou) return
if (!pou) {
const dataType = project.data.dataTypes.find((d) => d.name === pouName)
if (!dataType) return
pushToHistory(pouName, { variables: [], body: null, dataTypes: [dataType] })
return
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

pushToHistory(pouName, {
variables: pou.interface?.variables ?? [],
Expand Down
Loading
Loading