component_dataframe
Canonical contract (id_key cutover, 2026-06). A frame pairs to exactly one data item of a "main" component via that item's stable server-minted
id, stored on the frame locator asid_key, with the positive markertype = 'dd490'(DEDALO_RELATION_TYPE_DATAFRAME/ JSDATAFRAME_TYPE). Frames live in the same section record as the main component, in therelationJSONB column under the dataframe component's tipo.
id_keyis the single pairing key. The legacy pairsection_id_key/section_tipo_keyhas been removed from all live dataframe code. It survives ONLY at two edges: the old-CSV import (which accepts a pre-v7section_id_keyas theid_keysource, then strips it) and the historical v6→v7 data migration (which converted all stored frames to theid_keyshape, and the physicalmatrix_time_machine.section_id_keycolumn it left behind). Even the relation sibling-order is now an id_key dataframe (see Relation ordering).
The match predicate (single authority)
The client's component_common/js/dataframe.js finder and the server's dataframeEntryMatches() (src/core/concepts/subdatum.ts, consumed by src/core/relations/dataframe.ts) are the ONLY pairing matchers. A frame matches a caller when:
el.type === 'dd490'
AND el.from_component_tipo === <slot tipo> (when the caller supplies one)
AND el.main_component_tipo === caller.main_component_tipo
AND el.id_key === caller.id_key (the main item id)
isDataframeEntry() (src/core/concepts/subdatum.ts) detects a frame purely by type === 'dd490'.
End-to-end data flow
MAIN COMPONENT (e.g. portal numisdata32, item id = 1, links → material1/3)
│ the main item id (1) is the pairing key — never the target section_id (3)
▼
CLIENT render
relation main: section_record built per portal entry → self.locator.id (=1)
section_record.js get_component_data(dataframe_id_key = self.locator.id)
literal main: dataframe.js attach_item_dataframe(item.id)
│ → self.data.id_key = 1
▼
CLIENT save common.js create_source → source.caller_dataframe = { id_key:1, … }
component_dataframe.js create_new_section / link_record → value.id_key = 1
▼
SERVER dd_core_api handler reads source.caller_dataframe off the request
→ save_component.ts threads it through as callerDataframe (id_key only)
▼
read: full slot → filter by dataframeEntryMatches(caller) [id_key]
save: caller-aware merge (relations/dataframe.ts); stamp caller.id_key
on additions [id_key]
▼
PERSISTED frame: { type:'dd490', section_tipo, section_id, id_key:1,
from_component_tipo, main_component_tipo, id }
Time machine: the main component's save records a TM row under the main tipo whose data is [main items] + [ALL frames, all slots] merged together (the same array the main column stores, since a paired component's stored value already carries both). Preview and restore both split that row back apart with the shared stripDataframeFramesFromTmMain() (src/core/tm_record/tm_record.ts), which strips every dd490-marked frame entry so a frame can never leak into the main column. The matrix_id selects the snapshot.
section_id_key / section_tipo_key — where they remain (de-confusion)
Both the dataframe pairing AND the relation sibling-ordering have moved to id_key. These property names now survive only at the edges — do not "clean them up" there:
| Bucket | What it is | Status |
|---|---|---|
| A. Dataframe pairing | the pre-v7 shape of this component | Removed → id_key |
| B. relation_parent ordering | the per-parent sibling sort order on a component_number (section_map → thesaurus → order) |
Converted → the order is now a dataframe of the child's parent-link locator, paired by id_key (see Relation ordering below) |
| Old-CSV import | reading a pre-v7 export envelope | Kept — accepts section_id_key as the id_key source, then strips it on write |
C. matrix_time_machine section_id_key column |
a physical DB column from the old split-storage TM | To be dropped |
| D. one-time upgrade code | the historical v6→v7 data migration (incl. its own dual-read) | Ran once, before the cutover; not carried into the current update catalog |
Overview
{
"could_be_translatable" : false,
"is_literal" : false,
"is_related" : true,
"is_media" : false,
"modes" : ["edit","list","tm"],
"default_tools" : [
"tool_time_machine"
],
"render_views" :[
{
"_comment" : "edit: the component_portal views, verbatim",
"view" : "default | line | tree | mosaic | indexation | content | text | print",
"mode" : "edit"
},
{
"_comment" : "list: dataframe-specific renderers (dataframe_<view>); others fall back to the portal list view",
"view" : "default | text | mini",
"mode" : "list"
}
],
"data" : "array of frame locators",
"sample_data" : [
{"type":"dd490","section_id":3,"section_tipo":"dd1706","id_key":2,"from_component_tipo":"dd560","main_component_tipo":"rsc217","id":1}
],
"value" : "array of frame locators",
"sample_value" : [
{"type":"dd490","section_id":3,"section_tipo":"dd1706","id_key":2,"from_component_tipo":"dd560","main_component_tipo":"rsc217","id":1}
]
}
Typology
component_dataframe is a related component. On the client it is an alias of component_portal (export const component_dataframe = component_portal) with a few dataframe-specific additions (create_new_section, get_rating). Server-side it reuses the same shared portal engine: the descriptor src/core/components/component_dataframe/descriptor.ts registers resolveData: 'portal' (src/core/relations/models/portal.ts), so it gets the whole related-component contract (locator storage in the record's relation column, from_component_tipo filtering, grid/export/diffusion resolution) from the shared relation engines, and adds one purpose of its own: pairing frame records to individual data items of another component. The id_key pairing/merge algebra itself (not row emission) lives in src/core/relations/dataframe.ts.
About default_tools
As for the portal model it shares, the toolbar is assembled from the model + ontology, never hardcoded — the concrete list should be verified per instance in the ontology. A dataframe slot is non-translatable, so it never receives tool_lang / tool_lang_multi. In practice the frame editing surface is the target section opened in a modal, so most of the per-item tooling lives on the target record, not on the dataframe button. tool_time_machine is the only tool guaranteed by the verified source in this checkout (tools/tool_time_machine).
Definition
component_dataframe is an auxiliary relation component that extends INDIVIDUAL data items of a main component with frame records: uncertainty, qualifiers, sources, or contextual information (in the spirit of Wikidata qualifiers and references).
Why it exists. The Dédalo data model stores values, but research data often needs statements about values: how certain is this date, who assigned this label, what is the source of this number, what is the confidence rating of this attribution. Putting that metadata inside the value itself would bloat every data shape and break literal components; putting it in a free-standing record would lose the link to the specific value. The dataframe solves both: the frame content lives in a normal ontology-defined section (searchable, time-machine covered, diffusable like any record), and a small pairing locator ties each frame record to exactly one data item of the main component.
When to use it. Reach for a dataframe when you need to qualify specific values, not the whole record:
- An attribution that is uncertain only for the second of three authors, while the others are certain.
- A source/footnote that documents one particular reading of an inscription among several.
- A confidence rating on one of many proposed datings of an object.
- An IRI whose human-readable label must be stored and versioned (the built-in
component_irilabel slot).
It works for relation main components (component_portal, component_select, component_check_box, …) and for literal main components (component_input_text, component_text_area, component_date, component_number, component_email, component_iri).
When not to use it.
- To qualify the whole record (its state, its global source) -> use ordinary components in the section.
- To link the record to another record without per-item metadata -> use component_portal directly.
- To store a value that every cataloguer types inline -> use the appropriate literal component; only add a dataframe when item-level metadata is genuinely needed.
One frame slot, many pairings
A single component_dataframe instance (a slot, e.g. the IRI label slot dd560) holds the pairing locators for ALL items, and even for several main components of the same record. When a caller context is supplied, the read filters the slot's entries down to just the ones matching that caller (the match predicate below); without one, the whole slot is returned.
Data model
Data type: array of frame locators stored in the record's relation JSONB column under the dataframe slot's own tipo as the key — exactly like any relation component.
The column is relation, the shape is an object
Frames live in relation (singular), as {"<slot tipo>": [ … ]} — never in
a flat relations array, and never under the main component's tipo. See
Sections › typed JSONB columns
and Locator.
Value type: array of frame locators, or null.
The pairing contract
Every value of every component carries a stable, server-minted item id (a per-component counter inside the section record). The dataframe pairs against that id, not against array position and not against the target record:
main component data item frame locator (relation column, key = slot tipo)
{ "id": 2, "iri": "https://..." } ←── { "type": "dd490", "id_key": 2,
"main_component_tipo": "rsc217",
"from_component_tipo": "dd560",
"section_tipo": "dd1706", "section_id": 3 }
A frame locator matches a main item when these four properties agree (the match predicate, dataframeEntryMatches(), src/core/concepts/subdatum.ts):
| property | meaning |
|---|---|
type |
always dd490 (DEDALO_RELATION_TYPE_DATAFRAME) — the positive marker that an entry is a frame pairing |
from_component_tipo |
the dataframe slot (which component_dataframe owns this frame) |
main_component_tipo |
the main component the frame extends |
id_key |
the main data item's id — never a target section_id, never an array index |
section_tipo / section_id of the locator point at the frame target record (where the frame fields actually live).
The write contract (server-authoritative)
Every frame write funnels through normalizeDataframeEntry()
(src/core/concepts/subdatum.ts), reached from both write doors —
validateRelationInsert() (src/core/relations/save.ts, via the pairing
option) and mergeCallerEntries() (src/core/relations/dataframe.ts). It is
the single definition of the persisted shape:
| rule | why |
|---|---|
type forced to dd490 |
never trusted from the client, which sends it absent (autocomplete pick) or as dd151 (portal tree view). A frame without the marker is stored-but-unreadable |
from_component_tipo, main_component_tipo, id_key taken from the server's caller context |
the client sources its id_key from the last read echo, so trusting the payload lets one bad read corrupt every later write |
section_id canonicalized to an int |
a record address is an integer; a numeric string is the deprecated legacy form. Search containment is type-strict, so locator probes run in dual form while unconverted stock remains (engineering/wire_contract/WC-2026-08-10-section-id-int-canonical.md) |
paginated_key, section_id_key, section_tipo_key stripped |
a read-time transient the client echoes back, and the pre-v7 pairing keys (read-only BC) |
Duplicate rejection compares DATAFRAME_TEST_EQUAL_PROPERTIES
(dataframeEntriesEqual()), not the generic relation key: id is excluded
(it is minted per insert, so including it makes every duplicate look unique),
and id_key is included (framing the same target record from two different
main items is legitimate and must not be collapsed).
Regression note (2026-07-31).
component_dataframewas the one relation column excluded from this normalizer, so raw client locators were persisted verbatim: notype, numericsection_id, echoedpaginated_key, no dedup. Gates:test/unit/dataframe_write_contract_native.test.ts(raw payloads only — never spelltype:'dd490'in an input there) andtest/unit/dataframe_contract_tripwire.test.ts.
The frame chip colour (rating)
A dataframe slot may declare a role: "rating" ddo in the hide block of its
request_config; the client paints the frame chip with that option's colour
(view_default_list_dataframe.js). The chain, using the live wiring:
numisdata1448 (dataframe on numisdata30)
└ hide.ddo_map → rsc1246 role:"rating" component_radio_button
├ sqo.section_tipo → rsc1256 (the vocabulary)
├ show → rsc1259 "Valuation" → the option LABEL
└ hide → rsc1260 "Colour" → the option COLOUR
The colour reaches the client on the datalist: each option carries
hide: [{literal, tipo, section_id, section_tipo}] — one entry per hide ddo,
resolved against that option's own record — and the client matches the picked
locator's section_id against the datalist, then reads hide[0].literal.
Regression note (2026-07-31).
src/core/relations/datalist.tsemittedhide: []for every option (a ledgered gap), sohide[0].literalthrew and killed the whole record render. Both halves are now fixed: the datalist resolves hide ddos, and the client access is optional-chained so a colour can never again take a page down. Gate:test/unit/datalist_hide_ddos.test.ts(seeds its own vocabulary — the corpus records are not in the test DB, and an install-data assertion there passed with zeroexpect()calls).
Reading one item's frames
get_data on a component_dataframe is a paired read: it honours
source.caller_dataframe (declared on rqoSourceSchema, read through the one
accessor callerDataframePairing()), returns only that main item's frames, and
stamps id_key + main_component_tipo on the item. That stamp is load-bearing
— the client assigns the response onto self.data and sources the next write's
id_key from it, so an unstamped echo destroys the following write's pairing.
Without a usable pairing (search mode, maintenance) the read degrades to the
unpaired expansion rather than inventing one.
Storage shape
Frames live in the record's relation column — a JSONB object keyed by
component tipo, one key per component, each holding that component's locator
array. (It is relation, singular, and not a flat array: a frame is just one
more entry under its own slot's key, alongside every other relation component
of the record.)
Live matrix row numisdata5/9: the IRI value id:1 of main component
numisdata215 paired with label record dd1706:60, through slot dd560:
{
"relation": {
"dd560": [
{"id":1,"type":"dd490","id_key":1,"section_id":60,"section_tipo":"dd1706","from_component_tipo":"dd560","main_component_tipo":"numisdata215"}
]
}
}
Note that the frame locator carries its own id (it is itself a data item of the dataframe component) — do not confuse it with id_key, the pairing key.
Literal main: text with a role qualifier. The live literal wiring — a
component_input_text oh16 (section oh1) with two
values, each qualified by a frame in slot oh130 pointing at an rsc1242
("Data frame | Uncertainty") record. Note the literal's values live in the string column
(keyed by tipo, like every literal) while its frames live in relation
under the SLOT's tipo — never under the main's:
{
"string": {
"oh16": [
{"id": 1, "lang": "lg-spa", "value": "Primer testimonio"},
{"id": 2, "lang": "lg-spa", "value": "Segundo testimonio"}
]
},
"relation": {
"oh130": [
{"id":4,"type":"dd490","id_key":2,"section_id":583,"section_tipo":"rsc1242","from_component_tipo":"oh130","main_component_tipo":"oh16"},
{"id":5,"type":"dd490","id_key":1,"section_id":585,"section_tipo":"rsc1242","from_component_tipo":"oh130","main_component_tipo":"oh16"}
]
}
}
id_key: 2 pairs the first frame to the value whose id is 2 ("Segundo
testimonio") — never to its array position, which here is the opposite order.
The edit view renders the frame button next to the second value; reordering or editing the values never breaks the pairing because it follows id:2, not the position.
Relation main: portal informant with a role frame. The live wiring of oh1/368: a component_portal (tipo oh24, "Informants") pointing at two person records, the first link qualified by a frame in slot oh115 ("Role"). Main and slot are separate keys of the same relation column — the frame is never appended to the main's own array:
{
"relation": {
"oh24": [
{"id":1,"type":"dd151","section_id":15855,"section_tipo":"rsc197","from_component_tipo":"oh24"},
{"id":2,"type":"dd151","section_id":15856,"section_tipo":"rsc197","from_component_tipo":"oh24"}
],
"oh115": [
{"id":4,"type":"dd490","id_key":1,"section_id":1,"section_tipo":"rolepos1","from_component_tipo":"oh115","main_component_tipo":"oh24"}
]
}
}
The frame stays attached to portal row id:1 even if that locator is later re-pointed to a different person record, and even when the same target person is linked twice.
Legacy (pre-migration) shape
Data written before the v7 unification used section_id_key / section_tipo_key instead of type + id_key, and relation mains were keyed by the TARGET record's section_id. Dual-read has been removed: readers and the match predicate (dataframeEntryMatches, isDataframeEntry) recognise only the type + id_key shape. A database still carrying that legacy shape needed a one-time re-key of matrix data, time machine and the activity log to the unified contract before the cutover; an unmigrated legacy frame won't pair (it simply won't render). The only remaining tolerance for a legacy input shape is the old-CSV import, which accepts a pre-v7 export's section_id_key as the id_key source and strips the legacy keys on write.
Lifecycle
- Create — the user activates the frame button on a value: a new target record is created (
create_new_section) and the pairing locator saved. If the value is not persisted yet, pending changes are saved first (save-then-attach, single-writer rule): ids are minted server-side only, atomically, then the attach is repeated against the real id. - Update / reorder — pairing is untouched: the item
idis immutable and order-independent. Re-pointing a relation locator to another target also keeps its frame (the frame qualifies the statement, not the target). - Delete — a frame locator leaves its slot through two doors: removing the main item cascades server-side (
removeDataframeDataById(),src/core/relations/save.ts, wired into the component save path for a removed item, strips the paired frame locators from every dataframe slot declared on the main component; the same cascade runs for inverse-reference cleanup when a whole record is deleted,src/core/section/record/delete_record.ts), and deleting the frame itself from the dataframe modal removes that one locator from the slot. On both doors, what happens to the frame target record is the slot's delete policy: by default it survives — the time machine needs it to render past states — and is reclaimed later by maintenance; a frame-private slot can opt into emptying it or deleting it. - Time machine — frames are saved merged into the main component's TM row, so a TM snapshot always holds the full statement (value + frames);
stripDataframeFramesFromTmMain()(src/core/tm_record/tm_record.ts) splits them back apart for preview and restore. - Writes are caller-aware — the save path preserves the sibling frames of other items sharing the slot (
filterCallerEntries()/mergeCallerEntries(),src/core/relations/dataframe.ts), so clearing the frames of one item never wipes another item's frames.
Ontology instantiation
A component_dataframe is created as an ontology node whose model is component_dataframe. Its parent is normally the main component it extends; its portal request_config points at the frame target section (the section whose records hold the frame fields). Like the portal model it shares, it is non-translatable, so its language is lg-nolan.
Node definition of the live slot oh115 (the role frame of the oh24 portal,
in section oh1) — its properties block is shown in full immediately below,
so it is omitted here rather than shown empty:
{
"tipo" : "oh115",
"model" : "component_dataframe",
"parent" : "oh24",
"section_tipo" : "oh1",
"lg-eng" : "Role",
"lg-spa" : "Rol",
"translatable" : false
}
Its live properties block — a portal source whose sqo.section_tipo is the
frame target. Here the target is resolved dynamically from hierarchy_types: 4
rather than named literally: that expands to all ACTIVE hierarchies of
typology 4 on this installation, which on monedaiberica are the five
sections ds1, roleusr1, rolejob1, rolepos1 and uncertainty1 (measured;
another installation registering fewer of them, or having deactivated some,
resolves fewer — one archive here resolves ds1 alone). A fixed target uses
{"value": ["<section>"], "source": "section"} instead, as numisdata1447 does
with rsc1242 further below.
{
"mode": "edit",
"view": "tree",
"source": {
"mode": "autocomplete",
"request_config": [{
"sqo": {
"section_tipo": [{"value": [4], "source": "hierarchy_types"}]
},
"show": {
"ddo_map": [
{"tipo": "hierarchy25", "parent": "self", "section_tipo": "self", "value_with_parents": false}
],
"sqo_config": {"limit": 30},
"show_interface": {"label": false},
"fields_separator": ", "
}
}]
}
}
The optional delete policy is a sibling block on the same node:
{
"dataframe": { "delete_policy": "delete_target" }
}
Wiring it into a main component. Two things must be present:
- On the main component instance, the flag
has_dataframe: true. Required for literal mains (input_text, text_area, date, number, email): the literal's JSON controller reads it (to add the RQO + build the subdatum) and its views callattach_item_dataframe, which no-ops without it. Relation mains (portal, autocomplete, select…) ignore the flag — they render each linked record as asection_record, so the slot ddo inshow.ddo_map(step 2) is enough. A relation dataframe therefore works with nohas_dataframe; a literal one does not — see has_dataframe below. - The main instance's
request_configshow.ddo_mapmust include a ddo pointing at the dataframe slot, so the subdatum builder knows which frame slot to attach per value item:
{
"source": {
"request_config": [{
"show": {
"ddo_map": [
{"tipo": "oh115", "mode": "edit", "view": "line", "parent": "self", "section_tipo": "oh1"}
]
}
}]
}
}
Declare mode — on the ddo AND on the slot node
A slot ddo that omits mode does not inherit the caller's. When the
owner is not a section it resolves to mode: "list" (the
resolve-ddo-mode rule, src/core/relations/request_config/explicit.ts) and
the server stamps "mode":"list","fixed_mode":true on it — so you get the
compact read-only cell instead of the editable picker.
The resolution order is the same on every kind of main: the ddo's
mode/view win, then the slot node's properties.mode/view, then
list. Declaring it in either place works; declaring it in neither gives
you list.
In edit mode the views are simply
component_portal's, used verbatim — line, tree,
default, mosaic, indexation, content, text. view: "line" above
is one of them and renders through view_line_edit_portal.
List mode is the exception: there the client prefixes the view name to
dataframe_<view> to reach the dataframe-specific renderers, and only
three exist — dataframe_default, dataframe_text, dataframe_mini
(views default, text, mini). Any other name matches no case and falls
through to view_default_list_portal, the plain portal renderer, which
shows nothing for a slot that has no frames yet.
So {"view": "line"} with no mode is the trap: a good edit view
silently demoted to list, where it does not exist. Adding "mode": "edit"
is the fix — the view was never the problem.
(The two switches: render_edit_component_portal.js and
render_list_component_portal.js.)
Symptom: the server sends the frame item and its context correctly, the browser console is clean, and nothing appears.
At construction the dataframe instance is created with a caller_dataframe object (carrying id_key / main_component_tipo for the item it is paired with); this context is expected in non-search modes. section_tipo / parent of the target records are not the dataframe node's own section; persistence still flows through the main record's section (the single database writer), with the frame locators living in that record's relation column under the slot's tipo.
Properties & options
Dataframe configuration is split across two nodes: a flag on the main component, and a block on the dataframe slot node. All live in the ontology properties JSON.
has_dataframe (on the main component)
- Values:
true|false(defaultfalse). - Effect: activates dataframe handling for the main component's data. When set, the read path (
src/core/section/read.ts, thehas_dataframebranch) adds the RQO to the context and builds the per-item frame subdatum, and the edit/list views attach the dataframe control per value item. This is what makes the frame button appear next to each value. - Literal vs relation (important). The flag is only consulted by literal mains. A relation main (portal, autocomplete, select, check_box…) activates its dataframe purely from the
component_dataframeddo inshow.ddo_map(rendered through thesection_recordpath) and never readshas_dataframe. So a working relation dataframe with no flag is not a template for a literal — the literal needs the flag or its button never renders. Must be booleantrue(===true);"true"/1will not pass the strict-typed controllers.
dataframe.delete_policy (on the dataframe slot node)
- Values:
"unlink"(default) |"delete_target"|"delete_target_record". Read from the slot node'spropertiesbydataframeDeletePolicyOf()(src/core/relations/dataframe.ts) on every door a frame locator can leave its slot by: the main-item cascade (removeDataframeDataById()), the direct frame removal from the modal (the save path'sremoveon the slot), and the deletion of the host record itself in either mode. A policy written on the main component is ignored. The curator must hold a write grant (level 2) on the frame target section: the door was authorized on the host only, so the applier asks that grant itself and refuses the whole request withperm.deniedotherwise. The resolved value is served to the client as the context keydelete_policyon the slot entry. - Effect: controls what happens to a frame target record once its pairing locator has left the slot. The target is touched only after the unlink has committed (the deletes are queued on the transaction's commit lane, each in its own transaction); if the request rolls back, the locator and the target both stay as they were. An entry whose stored
section_idis not a record address is left alone under every policy.unlink— only the pairing locator is removed; the target record survives (the time machine needs it) and is reclaimed later by maintenance.delete_target— for frame-private sections where an unlinked record is meaningless: the target is emptied (every component cleared, row kept —deleteSectionData(), recoverable from the time machine).delete_target_record— the target record is deleted (time-machine snapshot, then the row goes —deleteSectionRecord(); the same path as the portal's Delete resource and all links). The client asks for a second confirmation before removing such a frame.
hard_delete: true— the v6 spelling ofdelete_target_record, carried on the slot node. It is honoured as the hard policy and wins over a conflictingdataframe.delete_policy. Anything else (a typo, a value on the wrong node) isunlink: an unknown spelling never destroys data.
{
"dataframe": {
"delete_policy": "delete_target_record"
}
}
source (the shared portal contract)
- Values: the standard portal
source/request_configobject. - Effect: on a dataframe slot node,
source.request_config.sqo.section_tipo[0]names the frame target section thatcreate_new_sectionwill create records in and open in the modal. Theshow.ddo_mapresolves how the frame target is summarised. See component_portal for the full portalsourcecontract.
role: "rating" (ddo-level, in the slot request_config)
- Values: set
"role": "rating"on a ddo inside the slot'srequest_config.hide.ddo_map, pointing at a component_radio_button in the target section. - Effect: the client resolves that component's value against its datalist and paints the frame button with the rating's colour (and contrast-aware text colour). Used to surface a confidence/quality rating directly on the frame button without opening the modal. The ddo lives in
hideso the rating is fetched for display only.
Worked example — the valuation rating
This is the live monedaiberica configuration, verbatim. Main component
numisdata161 (a component_autocomplete — an alias of
component_portal — in
section numisdata4), dataframe slot numisdata1447, frame target section
rsc1242, rating component_radio_button
rsc1246.
1. Dataframe slot node (numisdata1447, parent: numisdata161) — its
portal points at the frame target section rsc1242, and it declares rsc1246
twice: in show (mode edit) so the rating is editable inside the frame,
and in hide (mode solved, role: "rating") so the colour is resolved for
the chip without rendering a second column.
{
"label": "?",
"source": {
"request_config": [{
"sqo": { "section_tipo": [{"value": ["rsc1242"], "source": "section"}] },
"show": {
"ddo_map": [
{"tipo": "rsc1246", "mode": "edit", "view": "line", "parent": "self", "section_tipo": "self"}
],
"sqo_config": {"limit": 1}
},
"hide": {
"ddo_map": [
{"tipo": "rsc1246", "mode": "solved", "role": "rating", "parent": "self", "section_tipo": "self"}
]
}
}]
},
"hard_delete": true
}
hard_delete: true DELETES the frame target record
numisdata1447 carries it, and so do 58 other slot nodes of the monedaiberica
install (59 measured 2026-09-06). Inert from v6
until 2026-09-06 (its only reader was a commented-out client branch), it
is now read as the hard delete policy: removing this frame — from the
modal, or by removing the valuation it qualifies — deletes the rsc1242
record after a time-machine snapshot. That is what the key always meant.
New configs should spell it "dataframe": {"delete_policy":
"delete_target_record"} (below); both forms are honoured.
2. The rating component (rsc1246, a component_radio_button under the
rsc1243 grouper of section rsc1242) — its options are the records of
section rsc1256, labelled by rsc1259 in show and coloured by rsc1260
in hide:
{
"view": "rating",
"source": {
"request_config": [{
"sqo": { "section_tipo": [{"value": ["rsc1256"], "source": "section"}] },
"show": { "ddo_map": [{"tipo": "rsc1259", "parent": "self", "section_tipo": "self"}] },
"hide": { "ddo_map": [{"tipo": "rsc1260", "parent": "self", "section_tipo": "self"}] }
}]
}
}
3. The vocabulary — each rsc1256 record stores its label in rsc1259
(per language) and its colour in rsc1260 (lg-nolan). The four live options:
rsc1256 id |
rsc1259 (label) |
rsc1260 (colour) |
|---|---|---|
| 1 | Valor incierto | #b51a00 |
| 2 | Valor aproximado | #ffaa00 |
| 3 | Menos cierto | #f5ea14 |
| 4 | Más cierto | #27bb4c |
The colour reaches the client on the rating component's datalist: every
option carries hide: [{literal, tipo, section_id, section_tipo}], and
view_default_list_dataframe.js matches the picked locator's section_id
against it and paints the chip with hide[0].literal.
4. Result — open a numisdata4 record in edit: each numisdata161 value
shows a round rating button; click it to create/open the rsc1242 frame record
and pick a valuation; the button takes that colour. The same button also renders
in read-only contexts — Time Machine previews and read-only users (the edit
views attach the dataframe in both the writable and read-only render branches).
Copying this onto a LITERAL main
A dataframe behaves identically on a literal and on a relation main —
one component, one ontology contract — so this config is portable as-is.
The only difference is the activation flag: a literal main also needs
has_dataframe: true (see below); a relation main activates from the slot
ddo alone.
In both cases the frame's mode/view come from the ddo in the main's
show.ddo_map, falling back to the slot node's own properties.mode /
properties.view, then to list. That is why numisdata1447 carries no
node-level mode (its ddo supplies it) while dd560 and oh115 declare
"mode": "edit", "view": "tree" on the node — both spellings work, on
either kind of main.
The live literal wiring is oh16 (a
component_input_text in section oh1) with the
slot oh130.
Standard context properties
Like every component, component_dataframe honours the generic ontology context blocks carried into the datum context: css, request_config (RQO) and view. Any other custom key seen in production should be verified in the ontology.
Deprecated
component_iri shipped a literal title property that stored the IRI label inline; it is deprecated in favour of the dd560 label dataframe slot. Title resolution still falls back to that literal for unmigrated data. A maintenance conversion that turns those literals into label frame records and strips the property has no confirmed implementation in this checkout.
Render views & modes
The dataframe surface is intentionally minimal: a small button per value item. Views are dispatched by the per-mode render files; only the list render files ship (view_default_list_dataframe.js, view_mini_list_dataframe.js) because the button is rendered in both edit and list contexts of the main component.
| View | edit | list / tm | Notes |
|---|---|---|---|
default |
yes | yes | Round button.activate showing properties.label (or ?). With no frame yet, first interaction reveals a button.add (the +) that creates the target record; with a frame, it opens the target section in a modal. |
mini |
yes | yes | Same button rendered inside a component_dataframe_mini wrapper for tight spaces (e.g. inline next to a value). No add/modal chrome beyond the button. |
line |
yes | yes | Inline variant; used by component_iri, where the dataframe button sits inside a column_component_dataframe next to the IRI's input. |
tree |
yes | — | Inherited from the portal, not declared here: the client module is component_portal, so properties.view: "tree" in edit mode routes to the portal's term picker and the frame's target thesaurus opens for picking. oh115 is the live example (its target is resolved from hierarchy_types). |
view: "tree" in list mode has no renderer
The list dispatcher prefixes a dataframe's view with dataframe_, so a node
declaring mode: "list", view: "tree" asks for a dataframe_tree route — and there
is no such case. It falls through to the default portal list view, so the cell
renders something, just not what the declaration asked for. It is declared live on
monedaiberica: the section_list nodes rsc1442, rsc1443 and rsc1444 (all on
section rsc197) each carry a ddo_map entry with "mode": "list", "view": "tree"
for the dataframe slots rsc1433, rsc1434 and rsc1057. The capability they
anticipate is editing a frame from the list without opening the record, which is not
implemented. Declare mode: "edit" for the picker.
Modes:
- edit — read/write through the main record. The button creates target records (
create_new_section) and opens them in a modal (open_target_section); the modal footer offers a soft delete that callsunlink_record(). Read-only edit render (permissions === 1) still shows the (read-only) button: the literal edit views attach the dataframe in both the writable (get_content_value) and read-only (get_content_value_read) branches. The Time Machine tool uses exactly this path — it renders the main component in edit mode withpermissions = 1(render_tool_time_machine.js), so the historical rating shows in the TM preview. - list / tm — read-only; the same button renders, coloured by the rating ddo when present. In
tm(Time Machine) the frames are read from the merged TM row (the TM tool also drives the edit-mode read-only preview described above). - search — there is no dedicated search render view; the JSON controller tolerates
searchmode (it does not require a caller_dataframe there) but the component is not a primary search input. Search over frame content is done on the target section.
DOM (list / default): wrapper_component component_dataframe <tipo> <mode> -> content_data -> content_value -> span.button.activate (+ optional span.button.add.icon).
Import / export model
Export. A dataframe is a component with its own stored data, so a dedalo_raw export gives it its own column, headed by the dataframe component's tipo and placed right after the component it frames. Every cell is the plain dedalo_data wrapper around that component's stored slice:
oh16 (the framed component) |
oh130 (the dataframe) |
|---|---|
{"dedalo_data": [{"id":2,"value":"Segundo testimonio","lang":"lg-spa"}]} |
{"dedalo_data": [{"type":"dd490","id_key":2,"section_id":4,"section_tipo":"rolepos1","from_component_tipo":"oh130","main_component_tipo":"oh16"}]} |
The column is minted from the ontology (getDataframeChildTipos() in buildEntries, src/diffusion/export/grid.ts), so it appears for every record of the export, empty where a record carries no frames. Explicit item ids round-trip, which is exactly what keeps id_key valid across an export/import cycle — the pairing lives in the data, which is why the two components can travel in separate columns at all.
Import. The dataframe column maps like any other component column: the header resolves to the frame tipo, the relation conform facet (src/core/tools/import_conform.ts) writes the locators with id_key, main_component_tipo and type: dd490 intact, and the slot is replaced by what the column carries.
Files exported before 2026-08-09 fold the frames into the framed component's own cell as {"dedalo_data":{"data":…,"dataframe":[…]}} (dato instead of data in v6-era files). unwrapDedaloData() (src/core/tools/import_data.ts) still accepts both spellings and splits the frame array out; writeDataframeFrames() (src/core/tools/import_csv_execute.ts) writes them per slot, preserving frames owned by other components in the same slot and accepting a pre-v7 export's section_id_key as the id_key source. A dataframe-only envelope writes just the frames and leaves the component's data untouched. Nothing emits that envelope any more.
Wire contract: WC-2026-08-09-export-raw-dataframe-own-column.
See importing data and exporting data.
Relation ordering (the order is a dataframe)
The sibling sort order of component_relation_parent children is itself a dataframe, paired by id_key like everything else — there are no exceptions to the contract.
- What stores it. A section that participates in ordered hierarchies declares an order component (
section_map → thesaurus → order, acomponent_number). A child's position under one parent is a single value item of that order component. - The pairing key. The order value pairs by
id_key= the id of the child's parent-link locator (the entry in the child'scomponent_relation_parentthat points at this parent). A different parent means a different parent-link locator, a differentid, and therefore an independent order. Stored shape:{ value, id_key }(the old{ value, section_tipo_key, section_id_key }is retired). - Inline helpers.
src/core/relations/dataframe.tsexposes the id_key-keyed accessorsgetInlineDataByIdKey(),addInlineValueByIdKey(),removeInlineByIdKey(),getInlineValueByIdKey(),updateInlineValueByIdKey(). - Writing. Adding a parent link pre-allocates the parent locator's
idbefore the order value is stamped, so the order can pair before the save mints ids; removing a parent resolves the id from the stored locator. Sorting and sibling-order recalculation resolve each child's parent-link id viaresolveParentLinkIdKey()(src/core/relations/children.ts). - Reading (list order). Because
id_keydiffers per child, the order cannot be a single constant JSONB predicate.resolveParentLinkIdKey()resolves each child'sid_key+ order value, and the children engine (src/core/relations/children.ts) applies the order as a stable ascending in-process sort (children without an order value sink last) — rather than pushing a precomputedarray_position(...)ordering into SQL, so paging a very large sibling set costs a full-child-list read.
Notes
- Diffusion. Two opt-in mechanisms:
- On the main component's diffusion ddo, an opt-in flag publishes the data items with their paired frame locators attached as a
dataframeproperty, joined by item id. - A
component_dataframeddo in the diffusion map (withparentset to the main component tipo) publishes the parent-scoped frame locators; the chain processor recursion follows them into the frame target section records. Published locators carryid_keyas the join key. See the dedalo-diffusion skill.
- On the main component's diffusion ddo, an opt-in flag publishes the data items with their paired frame locators attached as a
- Maintenance. The
dataframe_controlwidget (src/core/area_maintenance/widgets/dataframe_control.ts) scans for frame locators whose main item no longer exists (orphans), reporting up to 500 in detail, and can remove them in place (target records are never deleted by this scan). - Observers / observables. Not used by the dataframe button itself; observer/observable wiring, when needed, is configured in the ontology
propertieslike any other component (see the index page Observers and observables section). - component_iri integration.
component_iriships with a fixed label slot: each IRI row'sidpairs with a label record in target sectiondd1706through slotdd560. The IRI's title resolution reads the paired label and falls back to a deprecated literaltitleproperty for unmigrated data. - Security. Follows the same relation-component persistence rules as every other relation model; direct, unauthenticated access fails closed.
- Related components: component_portal (the shared portal engine on the server, the client alias in the browser), component_iri (built-in label dataframe), component_input_text, component_text_area, component_date, component_number, component_email, component_select, component_check_box, component_radio_button.