Skip to content

feat(data-types): add a form/code toggle to the data type editor (DOPE-535) - #995

Merged
JoaoGSP merged 2 commits into
developmentfrom
feature/DOPE-535-datatype-form-code-toggle
Aug 7, 2026
Merged

feat(data-types): add a form/code toggle to the data type editor (DOPE-535)#995
JoaoGSP merged 2 commits into
developmentfrom
feature/DOPE-535-datatype-form-code-toggle

Conversation

@JoaoGSP

@JoaoGSP JoaoGSP commented Aug 6, 2026

Copy link
Copy Markdown
Member

Adds the variables-style table/Monaco toggle to every data type tab, with the code side showing that type's .dt text. Part of DOPE-385 (per-type datatypes/<Name>.dt files).

Mirror of https://github.com/Autonomy-Logic/openplc-web/pull/654

What changed

ModelStructureTableType becomes the same discriminated union VariablesTable uses: { display: 'table', description, selectedRow } | { display: 'code', code? }. updateModelStructure grows display/code and gains updateModelStructureForName; every open data type is mounted at once (workspace-screen.tsx), so a background editor writing through the active-editor action would land on the wrong model. display is optional on purpose — a row selection must never flip the view.

Both model factories now seed display: 'table', and shared/utils.ts was corrected from selectedRow: '' to '-1' (the inconsistency the card called out: consumers do parseInt(selectedRow) === -1, and parseInt('') is NaN, so the disable-guards on the remove/move buttons never fired until the first row click).

Toggle + buffer lifecycle — owned by DataTypeEditor, which reads its own model (editor.meta.name === dataTypeName ? editor : editors.find(...)), never the active one. Table mode keeps the buffer serialized from the form so the switch is instant; code mode mirrors per keystroke into the model; commit fires on outside-mousedown and container focusout. Commit runs parseDataTypeFromText(buffer, tabName) — failure shows an inline error plus a toast and keeps the user in code mode (trapped-until-valid, variables precedent); success captures history, replaces the type, and flags the file dirty. Renaming inside the text stays rejected by the parser's existing name-match rule, whose message points at the project tree.

External-mutation guardsreconcileDatatypeText / regenerateDatatypeText in the project slice. datatypeActions.rename reconciles first, so a tree rename folds pending text edits in rather than regenerating over them, and refuses when the text is invalid instead of silently discarding the user's work; afterwards it regenerates so the buffer's TYPE line follows the new name. applyDatatypeSnapshot regenerates too, which is what makes undo, redo and a source-control revert visible in Monaco.

Unreadable .dt files — DOPE-533 preserved them raw but only logged a console warning, so a broken declaration was safe on disk and completely unreachable in the UI (raised by @Gustavo on #651). There is no PLCDataType for one, hence no tree leaf. Project open now registers a file entry and pre-opens a code-mode tab holding the raw bytes, without stealing focus from the auto-opened POU; the banner shows the live parse error, and committing valid text promotes it to a real type and drops it from unparsedDataTypeFiles. No toast — the store layer can't import components (validate:arch), so the existing console warning stands.

Flag

Everything user-visible is behind isDataTypeFilesEnabled(), which still ships false. DOPE-542 owns the flip. Validated locally with the constant flipped to true.

Verification

  • tsc -p tsconfig.app.json clean; editor tsc clean too — it type-checks test files and caught an assertion web's vitest does not check.
  • Store slice tests 1567 pass. Full web suite: 78 failures, an identical set on a stashed clean tree (known local jsdom/node env issue, green in CI and in the editor's jest run of the same code).
  • Coverage: every new line covered; editor per-file numbers came out slightly above baseline.
  • eslint 0 errors, prettier clean, compare-surfaces.pymatch: true, diffs: 0.
  • Manually validated by @joao with the flag on: toggle round-trip, commit on blur, invalid text trapped, rename-in-text rejected, tree rename while in code mode, undo after a bad commit, and a deliberately broken .dt pre-opening in code mode.

Follow-up

DOPE-545 — allow renaming from the code view. Deliberately out of scope here: the buffer is the user's authored text, so a refused rename has nowhere safe to land (overwrite destroys the edit; leaving it means every later commit re-fails the same check, stranding the tab). Doing it properly needs DOPE-536's propagation plus a cancel path that rewrites only the TYPE name line.

Summary by CodeRabbit

  • New Features

    • Added code-view editing for data types, with table/code switching.
    • Added validation and error feedback for invalid data type code.
    • Changes are committed automatically when leaving the editor.
    • Added support for opening and editing unreadable or unparsed .dt files.
    • Data type renaming now preserves valid code edits.
  • Bug Fixes

    • Improved synchronization between data type models, editor content, and project state.
    • Preserved code content during updates and regenerated it after changes.

Every data type tab gains the variables-style table/Monaco switch, with
the code side showing that type's `.dt` text (DOPE-535). Committing the
buffer parses it through `parseDataTypeFromText`, so an invalid
declaration keeps the user on the text instead of silently dropping the
edit, and renaming in the text stays rejected — the file name is the
type's identity.

`StructureTableType` becomes the same discriminated union the variables
table uses, which is why `updateModelStructure` grows `display`/`code`
and gains a name-scoped sibling: every open data type is mounted at
once, so a background editor writing through the active-editor action
would land on the wrong model. `display` is optional there on purpose —
a row selection must never flip the view.

The reconcile/regenerate pair keeps the buffer and the store in lockstep
when something outside the code view moves the type. A tree rename folds
pending text edits in first and refuses on invalid text, rather than
regenerating over work the user hasn't committed; undo, redo and a disk
revert regenerate so Monaco shows the restored state.

An unreadable `datatypes/<Name>.dt` has no `PLCDataType`, so it can't
appear in the project tree and until now had no way of being found or
fixed in-app. Project open now registers it and pre-opens a code-mode
tab holding the raw bytes, without stealing focus from the auto-opened
POU; committing valid text promotes it to a real type. The store can't
raise a toast (layer rule), so the parse warning still goes to the
console.

Gated by `isDataTypeFilesEnabled()`, which ships false — DOPE-542 owns
the flip.

Refs DOPE-535

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvGrhB1LyJz9MBwYhjoBDY
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR adds datatype code-view editing with table/code switching, parsing, validation, model synchronization, snapshot regeneration, rename reconciliation, and support for unreadable .dt files.

Changes

Datatype code-view editing

Layer / File(s) Summary
Editor state and display contracts
src/frontend/store/slices/editor/*, src/frontend/store/slices/shared/utils.ts, src/frontend/store/slices/tabs/utils.ts, src/frontend/store/__tests__/*
Datatype structures now distinguish table and code displays. Named model updates preserve or reset display-specific fields. Datatype editor defaults and derivation detection are covered by tests.
Datatype text reconciliation
src/frontend/store/slices/project/*, src/frontend/store/__tests__/project-slice.test.ts
Project actions parse edited datatype text, reject invalid input, regenerate code buffers, update snapshots, and remove unparsed datatype files.
Raw datatype loading and rename
src/frontend/store/slices/shared/slice.ts, src/frontend/store/__tests__/shared-slice.test.ts
Datatype renames reconcile pending code edits. Unreadable .dt files receive derived metadata and open as code-mode tabs without changing existing focus.
Datatype editor code and table views
src/frontend/components/_features/[workspace]/data-type/index.tsx, src/frontend/components/_molecules/data-types/structure/index.tsx
The editor synchronizes models and code buffers, commits edits on focus loss, switches between table and code views, displays parse errors, and falls back to the datatype name when parsing has no name.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DataTypeEditor
  participant ProjectSlice
  participant Parser
  participant EditorModel
  DataTypeEditor->>ProjectSlice: Reconcile edited datatype text
  ProjectSlice->>EditorModel: Read code buffer
  ProjectSlice->>Parser: Parse datatype text
  Parser-->>ProjectSlice: Parsed model or parse error
  ProjectSlice->>EditorModel: Regenerate synchronized code text
  ProjectSlice-->>DataTypeEditor: Commit result or validation error
Loading

Possibly related PRs

Suggested reviewers: dcoutinho1328, thiagoralves

Poem

I’m a rabbit with code in my paws,
Parsing .dt files without flaws.
Table or text, the views now align,
Buffers refresh when models combine.
Errors stay visible, names stay bright—
Hop through the editor, clean and light!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: adding a form/code toggle to the data type editor.
Description check ✅ Passed The description provides detailed changes, scope, validation results, test status, and follow-up work, but omits the template checklist.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/DOPE-535-datatype-form-code-toggle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@JoaoGSP

JoaoGSP commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 7

🧹 Nitpick comments (1)
src/frontend/components/_features/[workspace]/data-type/index.tsx (1)

170-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the as Node assertions with instanceof Node narrowing.

Lines 172 and 180 assert EventTarget to Node. The coding guidelines forbid type assertions other than as const. contains also accepts a non-Node target at runtime without the assertion telling you so.

♻️ Proposed refactor
     const onDocMouseDown = (e: MouseEvent) => {
       if (!containerRef.current) return
-      if (containerRef.current.contains(e.target as Node)) return
+      const target = e.target
+      if (target instanceof Node && containerRef.current.contains(target)) return
       tryCommit()
     }
 
     // Covers keyboard navigation, Tab and shortcuts — anything that
     // moves focus away without a mousedown.
     const onFocusOut = (e: FocusEvent) => {
       if (!containerRef.current) return
-      const newTarget = e.relatedTarget as Node | null
-      if (newTarget && containerRef.current.contains(newTarget)) return
+      const newTarget = e.relatedTarget
+      if (newTarget instanceof Node && containerRef.current.contains(newTarget)) return
       tryCommit()
     }

As per coding guidelines: "Do not use type assertions, except as const".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/components/_features/`[workspace]/data-type/index.tsx around
lines 170 - 183, Replace the EventTarget-to-Node assertions in onDocMouseDown
and onFocusOut with instanceof Node checks before calling
containerRef.current.contains. Preserve the existing behavior for targets inside
the container and for null relatedTarget values, while complying with the
no-type-assertions guideline.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/components/_features/`[workspace]/data-type/index.tsx:
- Around line 273-299: Update the visualization toggle around TableIcon and
CodeIcon by wrapping each icon in a native button with type='button'. Move each
icon’s aria-label to its corresponding button, add aria-pressed based on the
active display value, and keep the existing click handlers and visual styling
behavior intact.
- Around line 159-192: Update the commit tracking inside the useEffect’s
tryCommit flow so a failed commit records the current editorCode as the last
rejected buffer and skips subsequent attempts for that same unchanged buffer.
Preserve retry behavior after the editor content changes, while ensuring
focusout and mousedown handlers cannot trigger duplicate commits or toasts for
one failed interaction.

In `@src/frontend/components/_molecules/data-types/structure/index.tsx`:
- Around line 49-60: The structure view effect currently uses the active editor
and the first structure data type instead of this component’s own model. Update
the component to accept the data type name, select the matching model from
editors/dataTypes by name like the parent’s existing logic, and use that model
for the type, display, description, and selectedRow checks; include dataTypes in
the effect dependencies.

In `@src/frontend/store/slices/editor/slice.ts`:
- Line 17: The description update in
src/frontend/store/slices/editor/slice.ts#L17-L17 should preserve an explicitly
provided empty string, falling back to prevDescription only when
data.description is undefined; prefer the appropriate nullish-default behavior.
Update the expectation in
src/frontend/store/__tests__/editor-slice.test.ts#L292-L304 so
updateModelStructure({ description: '' }) results in description: ''.

In `@src/frontend/store/slices/project/slice.ts`:
- Around line 539-546: Update reconcileDatatypeText so replacing
project.data.dataTypes[idx] also keeps editorActions.meta.derivation and related
tab metadata synchronized with the parsed dataType, or rejects reconciliation
when derivation changes. Ensure the data-type table selects the correct editor
for enum, structure, and array buffers, and add a regression test covering
derivation-changing code.

In `@src/frontend/store/slices/shared/slice.ts`:
- Around line 1002-1015: Gate the unparsedDataTypes tab/model creation loop on
isDataTypeFilesEnabled(), including the related file registry entry added
nearby. Only call updateTabs, addModel, and updateModelStructureForName when the
flag is enabled; preserve the existing behavior when it is disabled.
- Around line 681-689: Filter the unparsedDataTypes collection before it is
consumed, excluding entries whose name matches any POU name or parsed data type
name from data.projectData.dataTypes. Reuse the existing raw-name identifiers
used by the surrounding file-entry, tab, and editor-model logic so unreadable
.dt files cannot overwrite existing elements.

---

Nitpick comments:
In `@src/frontend/components/_features/`[workspace]/data-type/index.tsx:
- Around line 170-183: Replace the EventTarget-to-Node assertions in
onDocMouseDown and onFocusOut with instanceof Node checks before calling
containerRef.current.contains. Preserve the existing behavior for targets inside
the container and for null relatedTarget values, while complying with the
no-type-assertions guideline.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b9198d5d-978d-4696-94b1-5090f35181ee

📥 Commits

Reviewing files that changed from the base of the PR and between 05dc8d7 and 732745c.

📒 Files selected for processing (14)
  • src/frontend/components/_features/[workspace]/data-type/index.tsx
  • src/frontend/components/_molecules/data-types/structure/index.tsx
  • src/frontend/store/__tests__/editor-slice.test.ts
  • src/frontend/store/__tests__/project-slice.test.ts
  • src/frontend/store/__tests__/shared-slice.test.ts
  • src/frontend/store/__tests__/shared-utils.test.ts
  • src/frontend/store/__tests__/tabs-utils.test.ts
  • src/frontend/store/slices/editor/slice.ts
  • src/frontend/store/slices/editor/types.ts
  • src/frontend/store/slices/project/slice.ts
  • src/frontend/store/slices/project/types.ts
  • src/frontend/store/slices/shared/slice.ts
  • src/frontend/store/slices/shared/utils.ts
  • src/frontend/store/slices/tabs/utils.ts

Comment thread src/frontend/components/_features/[workspace]/data-type/index.tsx
Comment thread src/frontend/components/_features/[workspace]/data-type/index.tsx
Comment thread src/frontend/components/_molecules/data-types/structure/index.tsx
Comment thread src/frontend/store/slices/editor/slice.ts
Comment thread src/frontend/store/slices/project/slice.ts
Comment thread src/frontend/store/slices/shared/slice.ts Outdated
Comment thread src/frontend/store/slices/shared/slice.ts Outdated
…able .dt files off taken names

Clicking away from the code view raises document `mousedown` and then
container `focusout`. The commit is synchronous, so `isParsingRef` is
already clear by the second event, and a failure left `lastParsedCodeRef`
un-advanced — so the same invalid buffer was parsed twice and toasted
twice. Track the rejected buffer as well, so the pair is one attempt
whatever its outcome. The variables editor never hit this because its
commit is async and its latch spans both events.

An unreadable `datatypes/<Name>.dt` took its name from the file basename
with nothing checking it against the elements already loaded. POUs and
data types share one identifier namespace and the file registry is keyed
by raw name, so a project holding both a POU `foo` and an unreadable
`foo.dt` had that POU's registry entry retyped to `data-type` — enough to
send its next save down the `.dt` branch. Skip colliding names; the file
still rides along in `unparsedDataTypeFiles`, so it is echoed back to
disk rather than dropped.

Comments across the feature trimmed to the non-obvious constraints.

Refs DOPE-535

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvGrhB1LyJz9MBwYhjoBDY
@JoaoGSP JoaoGSP closed this Aug 7, 2026
@JoaoGSP JoaoGSP reopened this Aug 7, 2026
@JoaoGSP
JoaoGSP merged commit 14d015d into development Aug 7, 2026
18 of 19 checks passed
@JoaoGSP
JoaoGSP deleted the feature/DOPE-535-datatype-form-code-toggle branch August 7, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants