Skip to content

Layout editor: bugs fixed, import, catalogue of 123 controls, nested data binding, modules - #3

Merged
oblomov-dev merged 10 commits into
mainfrom
claude/layout-editor-analysis-cupf86
Oct 14, 2026
Merged

oblomov-dev merged 10 commits into
mainfrom
claude/layout-editor-analysis-cupf86

Conversation

@oblomov-dev

Copy link
Copy Markdown
Member

What this is

An analysis of the layout editor turned into ten commits: the bugs it found fixed, the catalogue grown from 68 to 123 controls plus any control by class name, data binding for nested tables (trees, lists in lists), the editor split into modules, and the browser tests freed from the CDN. One commit per step; each was verified with all four checks before the next.

The view and the class agree (bugs)

  • A table attribute whose sample rows set no field still seeds model_init (the canvas showed rows, the class started empty).
  • An empty binding (@ without an attribute, # without a field) is written by neither the view nor the class; the view used to bind {/}, the whole model.
  • Checks: two tables sharing a row type (t_rows and rows), a rows binding on a scalar, a boolean property on a non-flag, a public attribute nothing binds, a decimal with more than two places, a time without seconds.
  • A part no longer drops onto the view root; insertNode( ) refuses what an aggregation does not take; typing after a change of selection is a new undo step; ids of opened designs are checked; Escape in a dialog no longer selects the view.

Import

{/NAME} resolves to client->_bind( name ), {FIELD} in a bound list to a field; unresolved paths are listed with one click to create the attributes. Dropped handlers and ids are reported. Imported properties the palette does not know show under Imported and can be removed. A child without a wrapper tag lands where a drop would.

Canvas and outline

Drop before / into / after the control under the pointer. Arrow keys in the outline. Ctrl+C / X / V through the clipboard, also between tabs. Resizable panes. Tab panels with aria-selected.

Catalogue

121 controls checked against the real 1.71.80 metadata (test/ui5-1.71.json, regenerated), across sap.m, sap.ui.core, sap.ui.layout, sap.ui.unified, sap.ui.table, sap.f and sap.uxap: DynamicPage, ObjectPageLayout, the grid Table, Wizard, TabContainer, MenuButton, Card, Avatar and more. Any other control by class name: the canvas's UI5 describes it, and it then works like one of the palette's, marked as unchecked against 1.71. Tree is bound or nothing, as UI5 demands.

Data binding

A table field can be a table of its own, edited on the Data tab under the row. The class gets a row type per level, model_init nested VALUE #( ), the canvas nested arrays, as the runtime check proves. A tree binds items="{path:'/T_NODES',parameters:{arrayNames:['NODES']}}" through a string template around client->_bind( path = abap_true ); a list inside a bound list repeats a table field of its row. Inserting a list, table or tree offers to create and bind the table attribute in one click.

Editor

src/app.js is the composition root; src/ui/store.js holds state and commit( ), one module per panel. Per edit the design is serialised once less, the class built once, the panels rendered once per frame. The canvas reports runtime errors and a CDN that does not answer. tools/ui5-trees.mjs builds UI5 trees from the @openui5 npm packages; CI caches them, so the browser tests never depend on the CDN. Recent designs, a share link with the design in the URL, format migrations.

Verification

Every commit: npm test (186), npm run check:abap (0 findings), npm run verify:abap (6 classes transpiled and run on abap2UI5, view and model equal), npm run test:e2e on UI5 1.144.0 and 1.71.80 (35). The kitchen sink is two classes now: one statement with every control overruns the transpiled runtime's stack.

Known limits

  • A self-referencing row type (a structure holding a table of itself) transpiles but the node runtime rejects it, so trees have an explicit depth.
  • The linter rule relative-binding-without-context does not see the context a relative items="{NODES}" establishes; it is excluded for the kitchen sink, the runtime check covers it. A case for the linter repository.
  • The CI change (actions/cache + npm run ui5:trees) is new; its first run on GitHub will show.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp


Generated by Claude Code

claude added 10 commits October 14, 2026 17:42
- A table attribute whose sample rows set no field still seeds model_init
  with one initial line per row: the canvas showed the rows, the class
  started empty.
- An empty binding (@ without an attribute, # without a field, an empty
  expression) is written by neither the view nor the class; the view used
  to bind {/} - the whole model - or {}.
- Checks: two tables that share a row type (t_rows and rows -> ty_s_rows)
  are an error; a rows binding has to name a table and a property binding
  a scalar; a boolean property bound to a non-flag is a warning; a public
  attribute nothing binds is a warning, as the abap2UI5 linter says.
- The attribute picker of a property offers only attributes that fit.
- A part (Column, Item, ...) no longer drops onto the view root;
  insertNode( ) refuses what the aggregation does not take, so a Column
  cannot get a second header; duplicate in a single slot says why.
- Typing after a change of selection starts a new undo step.
- test/model.test.mjs: the edits, the drop target, the row context, the
  validation - with one regression test per item above.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp
- {/NAME} on an attribute of the class becomes client->_bind( name ),
  {FIELD} inside a list bound to a table with that field becomes a field
  of the row; everything else stays a UI5 expression. A binding that
  names no attribute is listed after the import, with one click to
  create the attributes (a string per value, a table per rows binding)
  and rebind.
- The import says what it dropped: event handlers, ids, controls the
  palette does not know. Text content (<Text>Hello</Text>) becomes the
  text property.
- A child without a wrapper tag lands where a drop would (IconTabBar
  items, Bar contentMiddle), not in a nameless aggregation.
- Properties an import brought along that the catalogue does not know
  show in the properties under Imported and can be removed; they used
  to be written to the class unseen.
- Binding the rows of a list that someone built by hand asks before the
  template is replaced; Cancel keeps it and sets the binding only.
- test/import.test.mjs and three browser tests cover the above.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp
- Checks warns about a sample value the class cannot carry as it is: a
  decimal with more than two places, a time without seconds, a date or
  number in another shape. The canvas shows what the class sends - the
  decimal rounded, the time padded.
- A design opened from a file or the browser store is normalised: ids
  the canvas can find, unique, and the node shape the editor relies on;
  a file of another version is refused with a reason instead of
  silently replaced.
- Alt reaches the designer from the canvas (Alt+Up selects the parent);
  no shortcut acts while a dialog is open. README lists the keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp
- A drop on the canvas lands before, into or after the control under
  the pointer - the upper quarter is before, the lower after - with a
  line where it will go; it used to land into or behind only.
- The outline walks with the keys: up and down over the visible rows,
  left collapses or goes to the parent, right expands or goes to the
  first child, Home and End, Enter to the properties. Rows are focusable
  tree items with their level.
- Ctrl+C, Ctrl+X and Ctrl+V copy, cut and paste the selection, through
  the system clipboard (into another design or tab) with the editor's
  own copy as the fallback; a paste lands where a drop would. The
  command palette lists them.
- The drawer and the properties can be dragged wider or narrower, the
  width is remembered; double-click resets.
- The panel tabs carry aria-selected and aria-controls, the panels are
  tab panels.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp
Layout: VerticalLayout, HorizontalLayout, Grid, ScrollContainer,
Carousel. Display: ObjectHeader with ObjectAttribute, Breadcrumbs,
FormattedText, BusyIndicator, GenericTile with TileContent and
NumericContent. Input: MultiComboBox, MultiInput with Token,
DateTimePicker. Lists: ObjectListItem, DisplayListItem, InputListItem,
and an infoToolbar on List and Table.

test/ui5-1.71.json is regenerated from a 1.71.80 tree (68 controls);
a `loose` property is one of a UI5 type of its own that the view writes
as text (GridSpan, string[]). The metadata tool asked the dev server for
a local tree under a path it does not serve - fixed. The kitchen sink
places the new parts in their parents, so the linter, the runtime and
both browser legs see every one of them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp
…frame

src/app.js (1,500 lines) is now the composition root: src/ui/store.js
holds the state, commit( ) with undo and the browser store, and tells
its listeners on 'change' and 'select'; each panel is a module of its
own (insert, layers, inspector, data, code, preview, palette, files,
keys, edit for drops and the clipboard) that imports the store and the
DOM helpers and knows no other panel. No build step, ESM as before.

Per edit the design was serialised four times and the class generated
twice: commit( ) serialises once more than it must, the code pane keys
its cache on a revision counter, abap.js builds the class once with its
line-to-node map, and a burst of edits renders the panels once per
animation frame.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp
- The canvas says what UI5 is doing: a status line while it loads, the
  render error or a runtime error (a renderer, a formatter, an event -
  caught in the frame and reported), and after 25 seconds without UI5
  the hint that the CDN is out of reach and how to serve a local tree.
- tools/ui5-trees.mjs assembles OpenUI5 trees from the @openui5 npm
  packages into .ui5/; the browser job of CI caches them and runs on
  them, so the tests never depend on the CDN. npm run ui5:trees,
  npm run test:e2e:local.
- The render test names what UI5 leaves out of the page (unselected
  tabs, dropdown items, carousel pages, and what depends on the width:
  overflow toolbars, breadcrumbs, columns with a minimum width, a tile
  that is not loaded); every other control has to be in the page. It
  used to tolerate a third missing.
- File > Recent designs: what New, Open, Import or a link replaced, the
  last eight. Export > Copy share link: the design deflated into the
  URL; opening the link loads it.
- A design of an older format version is migrated step by step (none
  needed yet), a newer one refused with its version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp
….unified

sap.m: Wizard and WizardStep, TabContainer and TabContainerItem,
MessagePage, HeaderContainer, SlideTile with Image-, News- and
FeedContent, ObjectMarker, DraftIndicator, PullToRefresh, ToggleButton,
SplitButton, MenuButton with Menu and MenuItem, OverflowToolbarButton,
OverflowToolbarToggleButton, ToolbarSeparator, RangeSlider, MaskInput,
DateRangeSelection, FeedInput, SelectList, Tokenizer, ColorPalette,
PagingButton, Feed-, Notification-, Action- and GroupHeaderListItem.
sap.ui.layout: Splitter, BlockLayout with row and cell. sap.ui.core:
Title (a section title in a form), HTML. sap.ui.unified: FileUploader,
Calendar. sap.ui.table: the grid Table and its Column - binding its rows
to a table attribute builds one column per field with the template in
the column, and the row context knows that the templates sit in the
columns. sap.f: DynamicPage with title and header, Avatar, Card with its
header. sap.uxap: ObjectPageLayout with dynamic header title, section
and subsection.

Every control is in test/ui5-1.71.json, regenerated from a 1.71.80 tree
that now carries the five libraries (tools/ui5-trees.mjs); a control
without a default aggregation in 1.71 (TabContainer, DynamicPage, Card)
drops into a named one. Tree stays out: without a hierarchical binding
UI5 throws, and the editor's data model has flat tables only.

The kitchen sink is two classes now - one statement with every control
is more than the transpiled runtime of the verification takes - and the
render test knows what the new controls keep out of the page (wizard
steps and tabs after the first, a menu, lazily loaded sections, a
grid-table template without rows).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp
A class name typed into the Controls panel (or Ctrl+K) - sap.m.Wizard,
a library of one's own - is sent to the preview frame, which requires
the class and answers with its metadata: properties with types and enum
values, aggregations, the default aggregation, events. runtimeDef( )
turns that into a catalogue entry in RUNTIME, getControl( ) knows both
maps, and from then on the control drops, binds, shows typed editors in
the properties and writes its namespace into the view and the class
(prefixFor( ) picks the library's last name, unique in the design).
Described controls stay in the palette for the session; a design opened
with such controls has them described anew once the canvas is ready.
Nothing checks them against 1.71, and Checks says so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp
click from a list to its table attribute

- A table field can be a table: with fields and sample rows of its own,
  edited on the Data tab under the row. The class gets a row type per
  level (ty_s_tree, ty_s_tree_nodes), model_init seeds the nested rows
  with nested VALUE #( ), the canvas gets nested arrays - what abap2UI5
  sends, as the runtime check proves.
- sap.m.Tree is back, bound or nothing (UI5 fails on static tree items):
  its items repeat a table attribute, the children the table field of
  the row - items="{path:'/T_NODES',parameters:{arrayNames:['NODES']}}"
  in the view, a string template around client->_bind( path = abap_true )
  in the class. The builder model evaluates that template, the import
  reads it back.
- A list inside a bound list repeats a table field of its row
  (items="{NODES}", # in the Rows picker); the row context follows it,
  so the inner template's fields resolve, and Checks knows a table field
  from a scalar on both sides.
- Inserting a list, a table or a tree offers, in the toast, to create the
  table attribute and bind it in one click - a tree with its nodes table.
- The kitchen sink carries a tree and a list in a list; the linter rule
  relative-binding-without-context is excluded there: it sees no context
  in a relative items binding, the runtime check does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wdz84WSKQE9qRrS2Gu9dWp
@oblomov-dev
oblomov-dev merged commit baa0378 into main Oct 14, 2026
2 checks passed
@oblomov-dev
oblomov-dev deleted the claude/layout-editor-analysis-cupf86 branch October 14, 2026 20:54
@Hhpy410

Hhpy410 commented Oct 15, 2026

Copy link
Copy Markdown

Great project! This is exactly what I’ve been looking for.
Are you planning to add advanced UI features like SAP BAS? For example:
• Right-click context menus
• Drag-and-drop support
• Component repositioning / Layout customization
I’d love to see these features implemented. Keep up the amazing work!

@oblomov-dev

Copy link
Copy Markdown
Member Author

Hi @Hhpy410,

nice to hear and thanks for beta testing ;)

if you want, describe a little bit more detailed what you need. Then I will throw it into Claude and see if it’s possible to realize.

All the best.

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.

3 participants