ddcore 0.18
0.18.4 — 2026-09-23
Added
- Dialogs answer the keyboard. Escape closes the dialog on top and Enter runs its primary action, wherever the focus is inside it — except in a textarea or rich text, where Enter keeps its new line (Ctrl/Cmd+Enter submits), on a focused button, or while a Link field's options are open. Covers
dialog(),confirm()andprompt()from the desk SDK. ddcore.ui.confirm(message, title, { destructive: true })asks a question whose "Yes" loses something: "Yes" becomes a red danger button and "No" the primary, so Enter and Escape both keep the data. The desk uses it to discard changes, delete a document, an attachment or selected records, cancel a submitted document and revoke an API key.- A modal owns the focus while it is open. It takes it on opening (its first field, or the modal itself), keeps Tab inside, and hands it back to what had it on closing — so Escape closes the Import, Share and Assign modals right away, without a click inside them first.
GET /api/file-info?url=<file_url>returns an upload's originalfile_name,file_size,content_type,creationandownerto whoever may read the file. It uses the same rule as/private/files, so a colleague who can read the document gets it even thoughFileitself is owner-only.
Changed
- Attach and Attach Image controls hide the file name. The desk shows the thumbnail, or a file icon for a plain Attach. Either one opens the file and shows its original name, size, type and uploader on hover. The stored name is random, so showing it said nothing. Set
showFileName: trueon the field to show the name beside it again; it now shows the original name, not the stored one.
Fixed
- A burst of saves no longer floods the list with reloads. The desk ran one full reload per
list_update, so a Data Import of ~100 rows in the tree view started over a thousand concurrent/api/treerequests, which the browser refused with "Failed to fetch" toasts. Reloads are now coalesced: while one runs, further updates queue a single follow-up. (#15)
0.18.3 — 2026-09-23
Added
- Job lifecycle callbacks.
ddcore.enqueue(method, args, { onStart, onFailure })takes two method paths, each run in a transaction of its own, committed apart from the job's.onStartruns before the body on every attempt, so a document can showRunningwhile its job runs;onFailureruns after the body rolled back — on an error, a timeout, a cancellation or a dead worker — withjob.reasonandjob.final, so a document can showFailed. Both are called asfn(args, job); the SDK exportsJobInfoandJobFailure.ddcore jobs show,/api/jobsand the MCP job tools show them ason_start/on_failure. Seedocs/agent/ops.md. (#11) - An image on each card in the Cards view.
defineListView(…, { card: { image: "photo" } })names an Attach Image (or Attach) field. Each card shows it as a square thumbnail beside the title, loaded lazily. A card whose image is empty or fails to load, or whose field is above the reader's permlevel, shows an avatar with the title's initials, coloured by the title. DocTypes gainimageField, which the card'simagedefaults to andextendDoctypemay set. The meta refuses animageFieldthat is not an Attach Image or Attach field. Seeform-apiandfieldtypes. (#12)
Changed
- The tree view remembers Expand all / Collapse all. The last of the two buttons clicked on a DocType's tree is kept in the browser (
localStorage, per DocType), and the tree opens fully expanded next time when "Expand all" was the last choice.
Fixed
- No more
cached plan must not change result typeafter a migration (#14). A migration that changed a table's columns (a new field, a new fieldtype) left the server's connections holding statements prepared against the old row type. Requests reading that DocType then failed, once per connection, until each one recovered. A migration that ran DDL now closes the pool's connections. A migration run by another process (ddcore migrate, theddcore mcpserver'smigratetool) is caught when a request hits a stale statement: the server resets its pool and runs that request's transaction again, once.
0.18.2 — 2026-09-23
Added
- Tree view labels and order (DAT-07).
defineListView's newtreeoption sets how a node is labelled and in which order a level is listed:tree: { title: "{acronym} - {title}", orderBy: "title asc" }.titleis a template, whose placeholders are fetched and whose empty ones take their brackets and end separators with them, or a(row) => stringfunction withfields.orderByreplaces the default order (groups first, then the title).GET /api/tree/{doctype}acceptsfieldsandorder_byfor this, and returns the fields under each node'svalues. - Read-only external databases (#10).
ddcore.externalDb("sql_server").sql(query, params, { timeout })queries a SQL Server database from server code: a controller, service, report or job. The connection comes fromDDCORE_SECRET_<NAME>_HOST,_PORT(default 1433),_DATABASE,_USER,_PASSWORDand the optional_ENCRYPT, so the credentials stay out of every backup. OnlySELECT/WITHis accepted, each call runs in a transaction that is always rolled back, parameters are positional (@p1, …), the timeout defaults to 30 s, and rows are normalized likeddcore.db.sql(decimals as numbers, dates as strings, GUIDs in canonical form).ddcore doctorlists each external database and probes it. Seeexternal-db.
Fixed
- A report summary card with
datatype: "Data"shows its text (#8). The desk formatted every card that was not Currency or Int as a number, so a text value read0. A card is now formatted like a cell of itsdatatype, which also accepts"Date"and"Datetime"(shown on the site clock); with nodatatype, a number is formatted as a number and anything else as text. i18n extractcollects the name of a report with nolabel(#9). Such a report is shown, and translated, under itsname, but the extractor never saw that key, so it stayed in English and--checkdid not report it. Apps with unlabelled reports get a new key to translate.
0.18.1 — 2026-09-23
Added
Data Import from CSV and XLSX (DAT-01, the spreadsheet half). A list's toolbar has an Import button. It opens a dialog that:
- checks a
.csvor.xlsxfile with a dry run; - shows which column goes to which field (each one remappable);
- lists the rows that would fail and why, with a download of just those rows;
- then imports, creating records or updating them by
ID.
Each row is an ordinary insert or save as the uploader. Roles, scopes, field levels, hooks, naming, workflows, webhooks and Version all apply, and each row commits on its own.
Cells are parsed strictly by field type: the locale's decimal separator and date order, Excel date serials, yes/no, Select labels in the user's language, and a Link by its title. An export CSV re-imports unchanged in update mode, with
modifiedguarding against overwriting a later change.Also new:
- the endpoints
POST /api/data-import/<DocType>andGET /api/data-import/<DocType>/template; - the
importMaxRowssetting inddcore.json(default 5000); - a
data.importaudit event.
See data import.
- checks a
importpermission flag on DocType permission rows. It gates Data Import, together withcreateto insert orwriteto update. It defaults to off, so no role gains anything until an app grants it.Expand all and Collapse all in the tree view (DAT-07). Expanding fetches one level per round until every group is open; collapsing keeps what was loaded, so reopening a branch costs no request.
Changed
- A Duration field is edited in one box,
1d 2h 30m 0s, in a form and in a grid alike, instead of one number box per unit. ↑/↓ step the unit under the caret (Shift for 10, carrying into the next unit), ←/→ move between units, digits fill the selected unit and move on when it is full, Backspace zeroes it (and unsets an all-zero value), and a pasted1h 30mor1:30is read as before.hideDays/hideSecondsstill drop their unit; the stored value is unchanged.
Fixed
- An empty tree view no longer stays on "Loading…" (DAT-07). The tree reloaded itself every time a level arrived, so a hierarchy DocType with no records never left the loading state and kept requesting
/api/tree. It now loads once, and again only when a document changes. - A tree document's parent picker searches again (DAT-07). It filtered on
is_group = 1, which Postgres refuses for a boolean column, so opening the parent field on an existing document failed with a server error. It now filters ontrue. - Tree view leaves line up with their siblings (DAT-07). A leaf's title was pushed to the right edge of the card instead of sitting at its depth.
0.18.0 — 2026-09-22
Breaking
Text Editoris rich text. Its value is HTML, cleaned on the way in by an allowlist (paragraphs, headings, lists, quotes, code, links and images under/files/); scripts, styles, event handlers, iframes and external image URLs are removed rather than refused. A value written before this release is plain text: it is read as text — soa < bkeeps its<— and shown as paragraphs, converted for good on its next save without writing a Version entry for the conversion. An empty editor (<p></p>) storesnull, soreqdstill holds. Upgrade path: render such a value as HTML (the desk and print already do) or strip it withddcore.redact-style text extraction, and write plain text through the field as plain text — the server escapes it.ddcore.db.sqlwrites bypass the cleaning, as they always have.
Added
- Six fieldtypes (DAT-08):
Markdown EditorandCode(optionsis the language) on atextcolumn,Attach Image(anAttachrestricted to png/jpg/gif/webp, refused at upload too),Color(normalised to#rrggbb), andDuration(whole seconds;options: ["hideDays", "hideSeconds"]hides a unit on screen) andRating(0 tooptionsstars, 1–10, default 5; clamped to[0, options]on read) on abigint.DurationandRatinggeneratenumber | null; the rest generatestring | null. ConvertingText,Small TextorDatato a text type, orInttoDuration/Rating, keeps the same column and needs noconvert. See fieldtypes. - Tree DocTypes (DAT-07):
defineDoctype({ isTree: true })makes a DocType a hierarchy. It gets a self-referencing Link —parent_<snake(name)>, or theparentFieldyou name — and anis_groupCheck, both added unless you declare them yourself. The engine keeps the hierarchy honest on every write: the parent must exist and be a group, a document cannot be moved under itself or under one of its own descendants, a group with children stays a group, and deleting a document that still has children is refused with a message that says so (and is not waived byforce).dbSetis checked too when it writes either column. Structural writes on one tree DocType are serialised by an advisory lock, so two concurrent moves cannot weave a cycle between them. See trees. - Tree filter operators, usable anywhere filters are —
getList, REST, reports, the Desk:descendants of,descendants of (inclusive),not descendants of,ancestors ofandnot ancestors of, over a tree's ownidor over a Link pointing at one. They compile to a recursive query and compose with permissions, scopes and shares like any other filter. - A User Permission whose
allowis a tree DocType now covers the branch below the value it names (SEC-01): "Territory: Brazil" grants Brazil and everything under it, on lists, counts, link search, direct reads and writes. See scopes. GET /api/tree/{doctype}?parent=&limit=returns one level of a hierarchy with each node's readable child count, and the Desk's new Tree view is built on it: it is the default view for a tree DocType, expands a branch at a time, links each node to its form and offers "Add child" on a group. A node whose parent the user cannot read is shown as a root.ddcore importloads addcore exportdirectory into a site (DAT-01):plan,run,statusandreconcile, with--dry-run,--resume,--batch,--only,--include,--max-batches,--maintenanceand--map, plus animportMCP tool. A load keeps ids, owners, timestamps and docstatus (including cancelled documents), runs no controller hook and queues no webhook, notification, email or realtime event; it advancesddcore_seriespast the ids it writes and marks already-past date notifications as done. Every line leaves a ledger row, so a second run over the same directory writes nothing and an interrupted run resumes at the first line that did not commit. Attachments are verified against the export's checksums before their bytes are written.reconcilecompares rows, child rows, docstatus, files and per-docstatus Currency totals, and exits non-zero on any disagreement. A mapping file renames DocTypes and fields, drops, sets constants and remaps ids and users. See import.- Print renders a long value as a block of its own instead of a cell in the key/value grid: rich text and Markdown as the markup they are (cleaned by the allowlist),
Codeas escaped preformatted text andAttach Imageas an image. Templates getb.richText(html, title?),b.markdown(source, title?)andb.pre(text, title?);b.raw/b.htmlremain the unchecked escape hatch. ADurationprints as1d 2h 30m, aRatingas stars, and both align right in a child table. Relative image paths resolve against the site address in PDFs. See print templates.
Changed
- The desk edits rich text with a toolbar (bold, italic, strikethrough, headings, lists, quote, code, link, image), previews Markdown through the server so the preview matches the printed page, and gets controls for
Code(monospace, Tab indents),Duration(one box per unit),Rating(stars, keyboard-operable, clearing stores null),Color(picker and hex) andAttach Image(thumbnail, image files only). A list cell shows rich text as one line of text, a colour as a swatch and an image as a thumbnail, and a version diff compares the text rather than the markup. The editor loads only on a form that has a rich-text field. ddcore export --attachmentswrites the attachment bytes to<out>/files/public/<file>and<out>/files/private/<file>— their storage key — instead of<out>/files/<file>. A public and a private file sharing a base name no longer overwrite each other.manifest.jsonrecords the layout inexportFormat.
Fixed
- Checkbox fields in the Desk now render their
descriptionhelper text below the label (with proper left alignment), as well as field validation errors and required asterisks. - Color control in the Desk now maintains a minimum width for the swatch and hex input so it does not shrink when the "Clear" button is displayed.
descendants ofwas accepted as a filter operator and compiled into=, which answered a hierarchy question with an exact match — a wrong result, with no error. It now walks the tree, and naming it on a field that is neither a tree'sidnor a Link to a tree is refused.