Skip to content

fix: a card's one-line name centred on its actions; numbers with templates shown as used; operands named — 0.10.65 - #137

Open
davidnich wants to merge 270 commits into
developfrom
bugfix/typed-fields-templates-alignment
Open

davidnich wants to merge 270 commits into
developfrom
bugfix/typed-fields-templates-alignment

Conversation

@davidnich

@davidnich davidnich commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Follow-up to #131, from David's review of qorus#646.

1. A card's name and its actions. An open option card lined up its name with the top of its actions, so "When" sat above the buttons beside it.

  • A heading of one line is now centred on the actions.
  • A heading with a description or tags under its name still lines up by its top.
  • Checked in FormEngine Remote › Injected Option Actions Edit Card: it fails without the fix.

2. Number stories.

  • Number › Typed Freely used to type a template into the bare number editor, which takes no templates. It read as a field that should offer templates and did not. It now shows the bare editor only: 12abc kept as typed, and a decimal point typed before its digits kept. It points to where a number that takes templates is.
  • New: Template › Number Typed Freely is that field:
    • the first click lists the templates;
    • what is typed is kept as typed;
    • a chosen template becomes the value, named in the template picker.

Related (reqore #708). A typed field that opens with the cursor already in it showed no templates until the user left and came back. The cause is in reqore's focus popover, fixed in qoretechnologies/reqore#708.

3. Operand messages. While a row rule was being built in the Visual view, it said Value for argument 1 ("Value") is invalid: Missing value, twice.

  • An operand not yet filled in now says what to do: Enter a value for "Value".
  • A wrong operand says which one, by the name the editor shows: "Value" is invalid: ....
  • Where another operand of the call has the same name (every operand of a call taking any number of them is "Value"), it is said by its position too: Enter the 4th "Value", The 2nd "Value" is invalid: ... (8b7a187, qlip build 20261008-083118).

4. #136. #136 (fetch: a request that was abandoned is not a request that failed) was merged to develop (761550b) and is in this branch (8fbe7b5). It was published in 0.10.61 by #139, so this PR is 0.10.62.

Checks. build:test, build:test:prod and lint pass, with 2056 unit tests and 635 stories passing locally (at 1a6de5f). actionlint 1.7.7 (with shellcheck 0.10.0) is clean on both workflows.

5. An edited value says where it is going. While an option is being edited, it stays under the heading it was opened from (it moves when the editor is closed with ✓). Its open editor now says so: Moves to "Set" when you close it (✓) (fbe17de).

  • FormEngine passes the open options whose heading will change on close; CompactRow shows the note under the editor.
  • It is said once per form, on the first field edited, and on no field once that one is closed (David's decision on qlip build 20261008-083118; b7a3298).
  • New: FormEngine › Compact Row Says Where It Moves (not snapshotted: its end frame shows nothing; 8f31807). The three ExpressionField stories that edit a value show the note on their one field.

6. The Text view, Preview and summaries of a visual expression (David's review of qorus#646). In a row rule, ">" with its first operand switched to Use Expression (nothing chosen) showed undefined([object Object]) > [object Object] in the Text view. With a custom Text value holding a field template on each side, it showed [object Object] > [object Object].

  • Root cause: every writer of the expression as text took the operands as the Visual view stores them. Two shapes are not values: an operand not filled in (no operation chosen, no value), and a custom Text value, which is a rich-text document. dpql/serialize and dpql/renderExpression got them as they were; the server writes them as hashes ({args=(null)} > {type=int}), and the stories' stand-in server threw.
  • One reader: expressionOperands reads an operand as what it holds. A rich-text value becomes its template (a field is @field in DPQL) or its words; an operand not filled in is a hole.
  • Who uses it:
    • the Text view seeds from dpql/serialize only for a complete expression, and shows … > … otherwise;
    • the Preview and Explain render only a complete expression;
    • the read-first summary (renderExpressionToText) writes holes as ….
  • Exported (expressionOperand, expressionWithOperands, isExpressionComplete, EXPRESSION_HOLE), so an application's summaries need no copy of their own. qorus-ide's row rule line had one, and wrote [object Object] > ….
  • Tests: expressionTextOfIncomplete and useRenderExpressionWaits both fail before the fix. New story: ExpressionField › Text Mode Of An Unfinished Expression.

7. A value that takes templates is written as text (David's review of qorus#646: the values filled into empty cells, or given to added fields, of non-text columns) (1fabea9, 621b377, 9fb2333, db8c47c).

  • Before: in a field of type any, a whole number, a number or a yes/no, a chosen template replaced the field with a control that held the template and nothing else. No text could be written around it. An operand of type any opened on the pick-only "Select Template Or Field".
  • Now: such a field is one text field. A chosen template is a chip in it, named as the list names it (braced references too). What the field holds is read from the text (writtenAsText):
    • a template standing alone is that template; in an untyped field the value takes the template's own type;
    • text around it makes an untyped value text; deleting that text makes it the template, of its type, again;
    • a literal of the field's type (12, 3.5, true) is that literal; other text in a typed field is kept as written, for the form to flag;
    • an expression written in it is detected as before (offered, or switched to where it cannot be a literal of the type);
    • null stays a value; fixed choices (allowed values) keep their list.
  • Deleting the template leaves the text field, with the cursor in it, so what is typed next is the value. Before, an emptied whole-number field switched to its number input and the next keystroke went nowhere. The field's own control is one menu item away: "Use Custom Value".
  • Dates keep their date control. Templates are offered beside it (the $ button): a chosen template is the value, shown in the template control, whose × returns to the date. Choosing it no longer hands the reference to the date picker first, which threw ("Invalid ISO 8601 date time string").
  • An expression's untyped operands are written as text from the start. An empty untyped first operand is of the type the catalogue declares for it, as the other operands are (9fb2333). Before, it took the builder's own type, bool in a condition, and showed "Unknown type!".
  • The text field is 150px wide, as a free text field is, and wider for what it holds. It is narrower only where its row has less room, as in an operand in a narrow column (db8c47c).
  • Tests: writtenAsText (the whole-number field and the date ones fail before). New stories: Template › Written As Text (Template Alone, Text Around The Template, Number With A Template, Number Picks A Template, Number Emptied Keeps Its Field, Expression Detected, Null Stays A Value, Allowed Values, Date Keeps Its Control). The template stories that asserted the pick-only control now assert the chip in the text field. expressionUntypedOperandsWrittenAsText (fails before 9fb2333); ExpressionBuilder › With Simple Value checks the 150px width (fails before db8c47c).

8. Text around a template in a typed value, and null (David's decisions on qorus#646) (26c485c, b7f95b8).

  • Text around a template is not a whole number, a number, a yes/no or a date. Validation took anything starting with $ and holding a : for a template, so $record:{pos} Stk. passed in every typed field and the form could be saved with a value its type cannot hold. It is now invalid, with the reason under the field while it is typed:

    • int: "Stk." makes this text, not a whole number. Delete it to keep the template alone.
    • number / bool / date: the same, with "a number", "true or false", "a date"
    • several templates: Templates side by side make this text, not a number. Keep one of them alone.

    The message says what causes it and how to fix it (4e08f55, qlip build 20261008-083118).

    A template alone, a literal and an expression are judged as before; text fields are unchanged.

  • null in an untyped field shows null. It fell to the generic custom editor, which drew null as an empty box, the same as a field holding nothing. It is now written in the field's text field as null, and null written in an untyped field is null. Deleting it leaves the field empty, which is no value (undefined), not null.

  • Tests: textAroundATemplate and writtenAsText - null in an untyped field (both fail before). Stories: FormEngine › Whole Number With Text Around A Template (typed, then deleted; fails before), Whole Number Literal And Expression Stay Valid, Number / True Or False / Date With Text Around A Template; Template › Written As Text Null Stays A Value now shows null, is emptied and written null again (fails before).

9. A host's own reason; chip and read-row names (David's decisions on qorus#646) (8e3e30a, e198b39, 6303b15).

  • A host's reason in place of the field's own (invalid_reason). The sheet-contract review says what is wrong with a value in its own words, in the user's language, and the field said its generic reason as well ("This field is required", "…around a template"): two messages for one problem. A field's schema can now carry invalid_reason. While it is set, it is the field's message, in place of the field's own validation reason and "This field is required" (required groups and dependencies still say theirs), and the field is invalid, so the form's validity and "Needs attention" agree with what is said.
  • A chip is named from the whole catalogue. In a yes/no field, the chip of the whole-number field pos read "record: pos". The text field named chips from the list it offers, which a typed field filters to its own type, so a field not on that list fell back to the reference's path. This happened in every typed field. The rich-text field now takes the catalogue chips are named from (namingTemplates, defaulting to templates); TemplateField passes the whole catalogue there and still offers the filtered list.
  • A yes/no holding a template reads as that field, not as "Yes". The collapsed row's yes/no branch came before its template branch, and the summary called any value that is not empty "Yes". Only true and false read as Yes and No now.
  • Tests: hostInvalidReason, writtenAsText (a chip of a field not of the value's type), readRowTemplateInTypedField; all fail before. Stories: FormEngine › Host Reason In Place Of The Field's Own, Host Reason In Place Of Required, Yes No Holding A Template Read; Template › Written As Text Chip Of Another Type; all fail before.

10. "Use Template / Expression" is reverted (2343a15). 5fa0a45 was a paused design, replaced by the tabbed value entry being built separately; the branch is back to bc24dc0's behaviour, stories and tests.

11. A form option's value is entered on tabs (David's design for qorus#646) (86641aa). An option that takes templates and expressions has Value · Expression · Visual in its header row; one that takes templates only has Value · Template; one that takes no templates has no tabs. FormEngine turns the tabs on (valueTabs); a field outside a form's options (an expression operand, a standalone field) keeps its behaviour.

  • Value: text, numbers and whole numbers are written as text, templates as chips, and an expression typed there moves to Expression with an Undo; a yes / no, a date and fixed choices keep their own control.
  • One value, three views. Value → Expression writes the value as expression text and changes nothing until it is edited. Expression → Value converts a lone value or template; any other expression stays, and Value says it can't show it and offers to replace it. Visual starts from a value as the server reads it alone, value(12). Without a language server, Expression moves to Visual.
  • Saved values open on their tab: a literal on Value; a template on Value as a chip (text, number, whole number) or on Expression (other types; Template where the option takes templates only); text with templates on Value; an expression on Expression.
  • A template of another type than the field is a warning that it is converted when the value is used, not a block.
  • The field's ⋮ keeps value actions only; the tabs are the mode switch.
  • Tests: valueTabs (23, including a lone value written on Expression held as that value and Visual starting from value(12)); all fail before. Stories: FormEngine › Value Tab Per Option Type, Value To Expression And Visual (+ On A Phone), Saved Template Opens On Value, Saved Template On A Yes No Opens On Expression, Saved Text With Templates Stays Text, Saved Expression Opens On Expression (+ On A Phone), Template Only Option Has Value And Template (+ On A Phone), Expression Typed On Value Switches With Undo, Template Of Another Type Warns. Option With Any Type and ExpressionField › Field Menu Stays In The Toolbar now assert the tabs; the rest of the branch's stories are unchanged.

12. The tabs' words, the numeric keyboard, and a lone operand's position (445d1a3, bc2889b, 07e5333).

  • The words are the host's (valueTabsLabels): the tabs, their tooltips, the cannot-show and replace text, the conversion warning and the undo, with English defaults. A label that holds a value is a function of it, so a host keeps its language's word order. A tab is found by its key (data-tab="value"), not by its label. Tests: valueTabs › the words of the tabs (fail before).
  • A whole number or a number on the Value tab is typed on a numeric keyboard (inputmode numeric / decimal), as its own input was. Templates are picked from the list the field opens, and an expression is written on the Expression tab. Test: valueTabs › numeric keyboard (fails before).
  • A lone operand of a call that can take more says its position (David): Enter the 1st "Value" where the call has an "Add value" slot, as the operands that join it will. The position is said when another operand has the same name or the call is variadic. Tests: operandPosition, validation › an empty operand slot (fail before). Story: ExpressionBuilder › Addition Picked Fresh now asserts the message.

13. Found verifying the tabs in the IDE (2aa29bd, 5da9d74).

  • The Text view's template picker is as tall as the text and centred on it. It took the form's size (32px in a small form) beside the DPQL editor, which is drawn at the normal size (38px); it takes the editor's size. The editor also had a 48px floor of its own, an empty strip under its 38px text that nothing beside it could match; it has none now (one line as tall as its text, growing with its lines). The IDE's review stories check that a row's controls are as tall as each other and centred, and caught it. Story: FormEngine › Value To Expression And Visual asserts both (fails before).
  • An empty value opens the Expression tab empty. Value → Expression wrote an empty value as concat(), and what was typed went after it. Tests: valueTabs › an empty value opens the Expression tab empty, writes a value as expression text (fail before).

14. A host keeps values for reuse and says where a template comes from (c780b93). Two host seams, for the IDE's Qog options moving onto this field.

  • savedValues: the field's ⋮ offers "Save this value", which hands the value and the field type to the host (it asks for a name and keeps it), and "Use a saved value", which lists the values saved for the field's type. One is the value at once, on its tab, and can be removed as the host keeps it. Not offered for an empty value, a yes / no, an expression, fixed choices or a read-only field. In a form row the actions join the row's ⋮; elsewhere, the field's own.
  • templateBadge: the badge a template shows in the template picker: where it comes from, as the host knows (in the IDE, the app action that captures it).
  • Tests: savedValuesAndTemplateBadge (10). Tests that waited a fixed time now wait for the field's menu to be published. Stories: FormEngine › Saved Values In The Field Menu (+ On A Phone), Template Badge From The Host.

15. An edit made just before its field goes away is kept, as a value (ab3fc8b). Found clicking through the review in the IDE: a field picked on a value's Value tab, then the Expression tab opened straight away, was lost and the value was left empty.

  • The text field sends an edit 100ms after it is made, and it went away first, taking the edit with it. Now it sends a waiting edit as it goes.
  • The template field reported that text with the expression flag of whichever tab was open when it arrived, so the pick was stored as an expression (and so was a template chosen from a yes / no's saved values). On the tabs that text is the Value or the Template tab's: a value.
  • Tests: richTextEditBeforeUnmount (fails before), savedValuesAndTemplateBadge › a saved template on a yes / no is a value (fails before). Story: FormEngine › Saved Expression Opens On Expression now picks a field after Replace and opens the Expression tab straight away (fails before).

16. What was typed on the Expression tab is kept when the tab is left at once (d801fb4). Also found clicking through the review: a field picked into a yes / no's Text view, then the Value tab opened straight away, was lost. The Text view reads what is typed 300ms later, and leaving the tab took the editor and the waiting read with it. ExpressionField now offers its host a way to read it immediately (flushRef); the value tabs read it before they leave the Expression tab, then convert it as they do any expression. Tests: valueTabs › what was typed (and an expression typed) on the Expression tab is kept when the Value tab is opened straight after (fail before). Story: FormEngine › Expression Kept When The Tab Is Left At Once (fails before).

17. Fixes from David's review of the tab stories (qorus#646):

  • A value written as text on the Value tab takes its row's width (1fe361b). It kept an operand's 150px box, so a whole number's field was a small box beside empty room. Story: FormEngine › Value Tab Per Option Type checks the width (fails before).
  • A yes / no that holds no value shows no answer (a2fee7a). It was drawn on No while the form asked for a value, so what was shown, what was held and what was asked disagreed. No value is now the switch's unset state; No is false. Test: yesNoUnsetIsUnset (fails before).
  • An expression opened in the Text view is checked against the field's type (886cd26). The seeded text was checked only if the editor reported it back as a change, which Slate does only while the editor has the focus. With the focus in the picker, opened straight after a value moved to the Expression tab, an expression that does not fit said nothing. Test: expressionFieldSeededTypeCheck (fails before).

18. The Text view waits for its language server, and loses nothing typed (7daa068). Typed text was parsed 300ms after the last key, whether or not the session was up. A parse sent before the session is connected answers success: false, and nothing asked again, so what was typed never reached the value. Under load, the IDE's test-case story stored nothing for 1 + 2. The opening text was written by a retry loop (20 tries, 400ms apart) that then gave up.

  • DpqlEditor now says when its session can parse and write the text (onReady: connected, context bound).
  • ExpressionField keeps typed text pending and reads it when that signal comes, however late, and writes the opening text then too. No retry loop is left.
  • A server that cannot be reached is said in the Text view, and text typed and not yet read stays as typed. With nothing typed, the field shows the Visual view, as before.
  • Switching to the Visual view reads the text once the session is ready. The host's own text for the view (initialText) is not overwritten when the session comes up.
  • Tests: expressionFieldWaitsForTheSession (six cases; five fail before). Story: FormEngine › Compact Expressions now checks the seeded text, and checks that the Preview, which reads as that text, is not drawn beside it.

19. A lone template written on the Expression tab is kept as the expression template(...) (027b6ee, from 75b6d8e). Where the option takes expressions, a template written alone is stored as the expression the server parses it into, not as the bare template; a template-only option keeps the bare template. The DPQL mock does what the server does since Qorus's fix for a template in an expression from DPQL text (on Qorus develop). Test: valueTabs › a lone template written on the Expression tab is kept as the expression template(...).

20. An expression's Text view lists the field's templates when it is clicked into (0697284, David's review). The Text view's editor is the same rich-text editor the Value tab writes in, but it was given no templates. So it listed nothing on a click or a tap, only after @ or $ was typed, or from the picker beside it. DpqlEditor now hands SmartEditor the templates it labels its chips with, as the list the editor opens when it is clicked, tapped or tabbed into, and inserts from. The language server's completion is unchanged. The list is built once, in useTemplateTags, for the rich-text field and the Text view alike. Listing on keyboard focus, and Escape putting the list away and nothing else, come with reqore 0.78.5 (#708). Story: FormEngine › Value To Expression And Visual checks that a click on the Text view lists the row's fields (fails before).

21. Two lists, one family: what is typed is completed, and a field's templates are browsed (ccd8647, David's review). David asked for "the same components to the extent possible, and a consistent UX". He then allowed a second component with a justification: "some templates can be large; we need to keep the hierarchical view".

  • The completion list that typing opens (@, $, the language server's completion) is its own component, CompletionList / CompletionMenu, taken out of SmartEditor. It is a listbox the editor points to (aria-controls, aria-expanded). Its keys come from one hook, useCompletionKeys: the arrows move, Enter takes the row, Escape puts the list away, Tab leaves it. It never takes the focus.
  • $ in a value's text completes in that list too: the field's templates, found by what is typed after the $, wherever they sit in the catalogue.
  • The browse list keeps its hierarchy: groups, a record's "+" and an example's "?". It is drawn as the completion list is (completionStyle: its colour, and its rows' kind icon and monospace font). From a control (the Text view's picker, a date, a number's input, a template-only value) it is TemplateBrowser. In a text it is the editor's own list (useTemplateTags).
  • A chip clicked in the Text view opens the browse list, as a click anywhere in the text does. Before, the chip's replacement list opened beside the browse list, and what was typed reached neither.
  • An expression opens in its Text view, and Text comes before Visual.
  • Tests: oneCompletionList (no Reqore dropdown lists templates except the browse list; it fails on the code before) and templateDropdownDrawsDescriptions. Story: FormEngine › Lists Look And Behave Alike (colour, font, focus, arrows and Escape; it fails with the browse list's old colour).

22. A component's own props never reach the DOM (592c7f1, e7d853b; addendum from David's supah session).

  • The menu. ReqraftMenuItem read activePaths to decide whether it is active, then spread every prop into ReqoreMenuItem. An item drawn as a router link (as: Link, as qorus-ide's sidebar draws its items) hands the props it does not know to its <a>, so React warned that it does not recognize activePaths and wrote it onto the link (seen in qorus-ide's Components/Sidebar stories). Reqore's own fix (#708) cannot hold back a prop that is reqraft's. activePaths, submenu and a divider's divider now stop at the item; to and href still go to the link.
  • The active state follows activePaths. It was memoised on path alone, so a menu rebuilt with other active paths kept the first answer. It is computed on every render now (the memo was also a hook called after the divider's early return).
  • The same pattern elsewhere. A sweep for "read a prop of one's own, then spread every prop into a Reqore component that does not take it" found five more:
    • ExpressionItem (drawn as={ReqorePanel}): isChild, isAndOr, index and readOnly; readonly was written onto the panel's div. Its styling flags are transient props now.
    • FormEngine: the options schema, stringTemplates and skeletonReason went to its ReqoreCollection.
    • ConfirmUnsupportedTypeModal: exactMatch went to ReqoreModal.
    • The file field: readonly went to ReqorePanel (it still disables the drop area).
    • The object field: disabled went to ReqoreTabs (its buttons and editors still take it).
  • Every story was also run with React's unknown-DOM-prop warnings failing the story. No reqraft prop is left. 13 stories still warn, all about Reqore's own styling props (fill, wrap, minimal, rounded, customTheme, hasIcon, isResizableRight). They come from reqore 0.77.2, this repo's dev pin; reqore fixed those leaks in 0.77.7 (7858ab3a), and 0.78.0 is on beta.
  • Tests: menuItemOwnProps (no activepaths attribute on a link, section item or divider; a collapsed section opens and closes as its activePaths change), expressionItemOwnProps, ownPropsNotHandedOn. All fail before the fixes.

23. The tests run names only a version npm has, and links the qlip build it uploaded (a21b67d, 9d9d776; addendum from David's supah session, approved).

  • Before: tests.yml ran npm version prerelease --preid=canary.<time> in the runner and posted "install the canary version via yarn add @qoretechnologies/reqraft@{that}". Nothing published it, and the v that npm version prints would not resolve anyway. Run #482 told people to install v0.10.63-canary.20261009190442.0, a 404 on npm.
  • One computation. .github/scripts/release-version.sh works out the version a run publishes: <base>-pr.<PR>.g<sha7> for a PR, none when package.json's version is already on npm, and package.json's version for develop. publish.yml's Prerelease job stamps and publishes what it says, so the two workflows cannot drift.
  • Named only once npm has it. The two workflows run in parallel, and the publish can publish nothing, be superseded by a newer push or fail. So tests.yml asks published-version.sh, which uses the same script, and names the version only if npm view answers with exactly it. Otherwise the message says why there is nothing to install: "No prerelease was cut: package.json is at X, which is already on npm", or "No prerelease is on npm for this commit yet: see the Publish run" (with a link to the branch's Publish runs). It can never name an unpublished version. The npm registry itself shows why the check is needed: this commit's prerelease was published at 10:39:15 and npm said "may take a few minutes to become available"; it answered 404 for some time after.
  • The embed's Version field says the same in both the success and the failure message: the published version, or why there is none. It no longer shows package.json's version with a v. The failure message names no version.
  • ::set-output is gone from both workflows ($GITHUB_OUTPUT).
  • actionlint 1.7.7 with shellcheck 0.10.0 is clean: checkout and setup-node move to v4 (GitHub no longer runs their node16 v3), and the bump check quotes $FROM.
  • The qlip link in the same message went to a build that does not exist. The id was read from qlip's upload line up to the first space, and qlip now writes uploaded build 20261010-104435: 635 entries …, so the id kept its colon (runs #482 and #483 linked 20261009-190109: and 20261010-104435:; qlip's API answers 404 for them). .github/scripts/qlip-build-link.sh reads it up to the space or colon, after stripping colours (9d9d776).
  • Tests: releaseVersionScripts runs both scripts against a stand-in npm (a PR's prerelease, an all-digit sha, a version already on npm, develop; named only when npm has it) and checks the workflows use them. The workflow checks fail on the workflows before this commit. qlipBuildLinkScript covers both of qlip's line formats.
  • Verified on this PR's run: the Discord text and the published pr version match.
    • Run #484 (9d9d776) posted: ":white_check_mark: Tests run #484 for PR triggered by davidnich succeeded, install the prerelease via yarn add @qoretechnologies/reqraft@0.10.62-pr.137.g9d9d776." Its Version field says 0.10.62-pr.137.g9d9d776, and its qlip link is https://qlip.qoretechnologies.com/builds/20261010-105900.
    • Publish #248, Prerelease job: + @qoretechnologies/reqraft@0.10.62-pr.137.g9d9d776. npm's pr dist-tag: 0.10.62-pr.137.g9d9d776.
    • Run #483 (a21b67d) did the same for 0.10.62-pr.137.ga21b67d, published by Publish #247, and run #485 (80ac509) for 0.10.62-pr.137.g80ac509, published by Publish #249.

24. No text caret in a story capture (6277dbb; David's decision). The caret blinks, so a story that ends with the cursor in a field was captured with it on or off at random, and qlip flagged the same frame as changed. Template › Template Value came back as pending instead of carried forward as rejected, 9 pixels apart, all of them the caret after its "Interface ID" chip. The Storybook test setup now draws every caret transparent. 84 of the 635 stories end with the cursor in an input, a textarea or a rich-text editor; their captures lose the caret once (listed in my PR comment). Two local runs of every story give byte-identical captures for 83 of them; DpqlEditor › With Templates still differs in 7 pixels, a flat 9×2 patch that is not a caret.

25. The npm package ships the build, its README and LICENSE (80ac509; David's decision).

  • Before: package.json had no files field, so everything outside .npmignore was published: agent notes (.claude/CLAUDE.md, .tasks/*.md), design docs, the sources and the repo's configs. 0.10.62-pr.137.ga21b67d held 1242 files, 2.1 MB packed and 8.0 MB unpacked. A local npm pack also took in qlip's screenshot folder (16841 files, 79 MB), which .npmignore did not list.
  • Now: files is dist, README.md and LICENSE (npm adds package.json). npm pack --dry-run: 903 files, 1.05 MB packed, 4.3 MB unpacked. dist holds the modules, their types and their source maps.
  • CI check: .github/scripts/check-packed-files.mjs runs after the build (run #485: "903 files, 1051697 bytes packed, 4285155 unpacked; 900 built files and 34 consumer imports checked"; Publish #249 published 0.10.62-pr.137.g80ac509 with 903 files, 1.1 MB packed, 4.3 MB unpacked). It fails when the packed file list is missing an entry point (main, types, module, exports, bin), any file the build wrote, or a module or its types that a consumer imports. The consumer imports are listed in .github/consumer-imports.txt: qorus-ide's 33 deep imports (dist/components/form/fields/..., dist/helpers/..., …) and the root.
  • Verified in qorus-ide against a tarball packed from this commit (not the linked source): check:types clean, vite build passes, 8049 unit tests pass, and its Storybook (Sidebar, expression builder, Qog options, compact FormEngine stories) runs on it with no source-map warning.
  • Tests: packedFilesCheck runs the check on packages made in the test, with real npm: a missing built file, a consumer import no longer built, a missing entry point and an empty build each fail, naming the path. The real allowlist ships every module qorus-ide imports, and ships no agent notes, design docs or sources; the latter fails on the package.json before this commit.

26. What a form completes behind its host's skeleton is emitted when the skeleton clears, and stored options are kept (e1d99ba; regression from 4a9148e, found by qorus-ide's "Interfaces › Installed Release › Existing").

  • Why the early return exists (4a9148e). A host loading the schema shows the skeleton and hands the form an empty schema, or an earlier one, meanwhile. Read against that, the value is no answer: emitted, it wiped a Qog state's stored options the moment the state was opened (qorus#646). So nothing is emitted while the skeleton is up.

  • The regression. The emit only ran when the value changed. Hosts show the skeleton for other waits too: qorus-ide shows it while the templates load, with the schema already there. The form completed the value behind it (a required field's default, such as the server's created_by_user), the skeleton clearing re-ran nothing, and the default was shown but never emitted, so never saved. "Interfaces › Installed Release › Existing" waited for it forever.

  • Why re-running the emit is not enough by itself. A host that clears the skeleton as it hands the schema over still has the form holding the read against the old schema at that render. Every stored field had also already been removed from the form as "not on this instance" (the available fields are pruned against the schema). Emitted, that wiped the options. Not emitted, as at 80ac509, the form still showed them empty, and the next edit would have been emitted without them.

  • Now the form knows whether its schema is in force: the host is not behind the skeleton, and a schema it hands down has been taken in. Until then it neither prunes the value against the schema nor emits. When the schema comes into force, the emit check runs once; a value that adds nothing still emits nothing.

  • Tests (formEngineSkeletonClears):

    • a default filled in behind the skeleton is emitted once it clears, exactly once (fails at 80ac509);
    • stored options are in every emit when the schema arrives as the skeleton clears, whether the earlier read was against no schema or another one;
    • stored options are shown, with nothing emitted, once a schema that adds nothing arrives (fails at 80ac509).

    formEngineHostSkeleton's qorus#646 case is unchanged and passes. No FormEngine story drives a host skeleton, so no story assertion was added.

27. No story reaches a real server (ca993b1, ab2c9cc).

  • Before. Reqraft loads the current user whenever it is initialized, which every story's decorator does, and no mock answered it. So every story file fetched it from the live instance (hq). The useStorage and Menu stories' storage mocks named hq's URL outright: run against any other instance, or none, they matched nothing. The four useStorage stories then failed offline ("the current user could not be loaded"), and passed only while hq answered. Form Engine › Compact Field Types Editing also fetched the schema-definition option catalogue from the instance.
  • Mocks.
    • The current user (nothing stored) joins the global story mocks; a story about storage still gives its own.
    • The storage mocks are built with storyApiUrl.
    • Compact Field Types Editing is answered with the editor's mock catalogue.
  • The guard. src/stories/realNetworkGuard.ts is installed by the story test setup, after storybook-addon-mock. It blocks any request to another origin than the page's own: the fetch / XHR the addon falls through to when it has no entry, and WebSockets and beacons. A blocked request fails at once as a network error, nothing is sent, and the story fails naming it. Stories meant to reach a real server are tagged !test and never run under it.
  • Measured. Without the mocks, every story file failed under the guard, naming /api/latest/users (and one /api/latest/schemas). With them, all 635 stories pass with the instance pointed nowhere (REACT_APP_QORUS_INSTANCE=https://127.0.0.1:9/).
  • Tests:
    • storyMocksFollowTheInstance: no story or story mock names an instance host of its own; it fails on the old storage mocks.
    • realNetworkGuard: covers the guard itself.

28. A row summary writes + and * of more than two operands infix, as the server does (c435ea1; found verifying the Qog options against hq).

  • + and * take any number of operands, and the Visual view builds them that way. qorus-ide's Send Discord Message › Message Content is one + of three.
  • Since Qore's n-ary fix the server writes them infix, so the Text view and the preview read "This Qog was run at by " + format_number(…) + " and that's it". The collapsed read-first row wrote +("This Qog was run at by ", format_number(…), " and that's it") instead: the same value read two ways.
  • renderExpressionToText now writes + and * infix with any number of operands, each in the context the server gives it: a later operand that binds as loosely is parenthesised (1 + 2 + (3 + 4) + 5).
  • An operator that takes two operands but holds more is refused as text by the server (DPQL-SERIALIZE-ERROR: operator "-" takes 2 arguments; the expression has 3). The summary still writes every operand, as a call: -(10, 3, 2).
  • Test: expressionParentheses. The three-operand, nested and multiplied cases fail before the fix.

29. A check group's choices each have their own key (1a6de5f).

  • Each choice was keyed by item.value?.toString(). A choice whose value is not resolved yet had no key, and every object value keyed as "[object Object]".
  • React warned about the missing keys for the group (qorus-ide's Qog processor stories, whose Hubspot choices had not resolved), and choices that share a key can trade state.
  • They are now keyed by their value as JSON, or by their position when they have none.
  • Test: allowedValuesCheckGroupKeys, which fails before the fix.

30. A read row's reason starts on its value's line, beside an empty value's dash (9db1a1d, qorus#646, from David's review of qorus-ide's Fields/Test Cases Guided Help › Field Refused)

Symptom: the locked Expected value row drew its value, the empty dash, on one line. The reason ("This field is disabled because some dependencies are not fulfilled: …") went on the next line, so the dash read as a bullet with its text below it.

Root cause: this is reqraft, not the IDE.

  • StyledRowValue (compactRowStyles.ts) lays the value and its reasons out as flex items, meant to "share the line, wrapping the reason below only when it doesn't fit".
  • A reason was a flex item sized to its text, so it could only share the line whole. A reason longer than the room beside the value always wrapped to the next line.

Fix: the reason that follows the value takes the room beside it (flex: 1 1 16em; min-width: 0) and wraps within its own box. It goes below only when the value leaves it less than 16em. Later reasons and the full-width companions (message panels, a choice's description) are unchanged.

Story: Form/Engine/FormEngine › Compact Locked Reason Beside Its Dash uses the guided check with Exists picked. The reason wraps and starts on the dash's line, to its right. It fails before the fix, where the reason starts below the dash.

Checks: typecheck (test and prod) and lint pass; 227 files, 2390 tests pass. All 185 stories of the FormEngine story file pass locally.

31. An expression's type is the server's, and the condition verdicts are judged by it (from #140, closed: dc4fc3f, cherry-picked from ca0abc5; and 4c989ac; qoretechnologies/qorus#713, qorus#646)

Symptom: the builder worked out an expression's type itself, from the catalogue's ui_return_type, one level deep. That disagreed with the server: 1 + 2.5 was "any" here and a float there.

From #140:

  • A new useExpressionType hook asks the server over the LSP custom method dpql/expressionType, on the shared connection, debounced and cached. The server side is qorus aa5d066e2.
  • The builder's type, and the type of an argument that is an expression, come from the server.
  • Until the server answers, or when it cannot (an older server, or the connection gave up), the type is unknown and nothing is judged by it. There is no client-side stand-in. The client-side inference (return_type_first_arg, the catalogue's ui_return_type) is gone.

Folded into this PR (4c989ac): the qorus#646 condition verdicts now take that server type.

  • Wording: a verdict, and the type the expression is said to give, use the type's own name (inferred_type), as the Text view's verdicts do: "evaluates to a whole number", where the UI type of every number is number.
  • Fit: whether the expression fits its place is compared by ui_type, since the types a place takes are UI types.
  • Sentence forms: a type inside a sentence keeps its sentence form; a bare label keeps its display name.
  • Unknown type: while the type is unknown, no verdict is drawn and no mismatch is claimed. An empty card still gives nothing, which is known without asking, so Default Boolean's "This expression gives nothing, but this field takes true or false" stays.
  • Undeclared condition fields: a condition the server says gives text, a date or a time is now refused even in a condition field that declares no type of its own. Before, Visual Text Condition Says The Servers Words showed the server's refusal only because the client typed + as a number, so the refusal came out as a warning.
  • Story mock: the stories' DPQL mock answers dpql/expressionType as the server's getExpressionTypeInfo does (string → richtext, a number → number for the UI). Every story starts with a fresh expression-type client, as it already did for the render client.

Tests:

  • useExpressionType and expressions (from fix(expressions): the builder shows the type the server gives an expression (qorus#713) #140).
  • expressionConditionVerdictServerType: text is refused, also in a field that declares no type. A number is warned with its own name, although the catalogue says text. No verdict while the type is unknown, or known only at run time, and none for true or false. Three of these fail with the client-side type.
  • dpqlMockLanguage: the mock's dpql/expressionType.

Checks:

  • Typecheck (test and prod) and lint pass; 230 files, 2416 tests pass.
  • ExpressionBuilder (45 stories), ExpressionField (38) and FormEngine (187) story files pass. One ExpressionField run lost typed keys under load in two Text Mode stories; a rerun of the file passed all 38.

Earlier failed CI run (37601004195), With Markdown Docs. One typed key was lost under parallel load (arr.fr instead of arr.for). The cause is in reqore's rich-text editor: a value set by a passive effect overwrote a key typed between commit and effects. It is fixed in qoretechnologies/reqore#708, and the test there fails without the fix. This PR does not change that story, and its re-run passed.

32. A field is drawn as its type from its first render, and a rich-text field holding a string draws its chips (1c78382, 07576d2, qorus#646; found by qorus-ide's [object Object] gate in four Components/Expression Builder stories)

Symptom: in the Qorus IDE, the rich-text "List of Values" operand of a nested in(...) showed [object Object] in its text field until the field's next render.

Root cause:

  • AutoField started as the untyped any and only took its declared type in a mount effect, after its first render.
  • The any editor is the text field with the field's templates as chips, and it is handed String(value). A rich-text value is a Slate document, so String() turned it into [object Object].
  • The same first render had been hiding a second gap. In the text format it turned a string into a document with chips, and RichTextFormField kept that document when the type switched to richtext.
  • Without that accident, a rich-text field holding a string, which is what the server's rich text is, drew it as raw text: $local:some-richtext spelled out where its chip belongs.
  • The accidental path also sent the document back about 100ms after mount, rewriting a held string that nobody had edited.

Fix:

  • resolveAutoFieldTypes resolves the declared type when the state is created (or, for an untyped field holding a value, that value's type), using the rules the mount effect applied.
  • RichTextFormField reads a string as text with references, as the text format does, and compares it as the document it stands for. Nothing is sent back until the field is edited. A click into a field holding '' no longer turns it into no value.

Tests:

  • autoFieldDrawnAsDeclared: rich text holding a document is drawn in the rich-text editor holding that document on every render, with or without templates. Long text is never drawn as the one line an untyped field takes. Also cases for resolveAutoFieldTypes. The three render cases fail before the fix.
  • richTextStringValue: a reference drawn as its chip; text around a chip; nothing sent until edited; nothing sent when the editor reports the document it was handed, or '' as empty text; the document sent once edited; a new string from outside; documents unchanged. Four fail before the fix.
  • Story ExpressionBuilder › Nested In List With A Rich Text Operand: true in <rich text> nested in a Logical And, the rich text holding a template. It watches the document from before it renders, including nodes taken out again, and must never show [object Object]. It fails before the fix. The in entry is in this story's own catalogue, so no other picker's count changes.

Checks:

  • Typecheck (test and prod) and lint pass. 232 files and 2433 tests pass locally and in CI, and CI's 65 story files pass.
  • Qlip build 20261014-182127 changes no existing capture: its 17 changed snapshots are byte-identical to #341's.

The first CI run at 1c78382 failed two stories. AutoField renders in neither, and both passed in the next run (07576d2):

  • TemplateField › Written As Text Null Stays A Value got nul for typed null. A trace shows the field is TemplateField's own text editor, mounted once and never reset. Locally with reqore 0.77.2 it failed 6 of 11 runs (old and new code alike). With reqore 0.78.5-pr.708.g87239b9 it passed 8 of 8. This is the same lost-key cause as the earlier failed run above, fixed in reqore #708.
  • SmartEditor › Hover Details On A Touch Screen timed out once at 30s. The cause is in the SmartEditor and is fixed in c528388; see 33.

33. A hover's popover dismisses its own hover, never the next one (c528388, qorus#646; the one-off timeout of SmartEditor › Hover Details On A Touch Screen at 1c78382)

Where it was stuck: Qlip's error capture shows "forEach" typed and the Details button on screen, with no hover. The play had tapped Details and was waiting for the hover. That wait was as long as the test's 30s, so the report said only that the test timed out. It is not the lost-key bug (the text was all there), and not AutoField (the SmartEditor does not use it).

Root cause:

  • The hover popover was keyed by where it is anchored, and cleared the hover when it closed.
  • reqore's popover says it closed when it unmounts too, and a hover replaced by the next one unmounts.
  • In the story, the click that focused the editor left a mouse hover up. The tap on Details closed it, and the caret's hover replaced it.
  • When the server's answer was applied before the old popover had gone, the old popover's unmount cleared the new hover, and nothing was shown.
  • Throttling the CPU slows the page and the mock server alike, so it did not reorder them (4 of 4 passed at 20x). A loaded runner, where React yields and the mock socket's timer runs first, did.
  • The same flaw is deterministic on a desktop. With the mouse resting on one word and then on the next, the next word's hover is set, the popover's key changes, and the old one's unmount clears it.

Fix:

  • Each hover shown has an id. Its popover is keyed by that id and clears only that hover.
  • clearHover() without an id (the mouse leaving the editor) still clears whatever is shown.
  • The touch story's waits are 10s, inside the test's 30s, and name what they wait for.

Tests:

  • smartEditor/hoverDismissedAlone: an older hover's popover leaves the hover shown; a new id for each hover, even at the same place; no id clears the hover shown; nothing shown where the server says nothing. Two fail before the fix.
  • Story SmartEditor › Hover Follows The Mouse To The Next Word: rests the mouse on forEach, then on map, against a server that names the word asked about. It fails before the fix: no hover is shown for map.
  • SmartEditor (9 desktop stories, 2 touch), DpqlEditor (20) and ExpressionField (38) pass. Unit tests: 233 files, 2437 tests. CI at c528388 is green.

34. A tap opens no hover; only a pointer that hovers does (51bc71b, 8444150, qorus#646)

Symptom:

  • On a touch screen, the tap that put the caret in the SmartEditor opened the server's hover there, beside the Details button that is a finger's way to it.
  • In a compact form, tapping a one-of group's chip or one of its rows lit the group's members, and they stayed lit until the next tap.

Root cause:

  • A tap is followed by mouse events the browser emulates (mousemove and mouseenter among them, at the tap), and nothing in them tells them from a mouse's.
  • The SmartEditor listened for mousemove, and CompactRow for mouseenter.

Fix:

  • New isHoveringPointer (src/helpers/hoveringPointer.ts): a mouse, or a pen held above the screen (buttons 0); never a finger.
  • The SmartEditor's hover listens for pointermove / pointerleave, and CompactRow's three group highlights for onPointerEnter. Each acts only for a hovering pointer, and leaving still clears.
  • The check is per event, not per device (useCoarsePointer / isHoverCapable ask about the device), so a touch laptop's mouse still hovers, and so does a mouse plugged into a tablet.
  • A finger lifted off the editor leaves a hover that Details showed.
  • SmartEditor takes hoverDelayMs (default 300, the delay it always had).
  • Other hover spots checked:
    • The DpqlEditor is the SmartEditor.
    • Description and template tooltips already open on tap where the pointer is coarse.
    • Completion documentation opens from the ⓘ there.
    • What remains is reqore's: its popover's hover handler opens on mouseenter, so on a touch laptop whose primary pointer is fine, a tap on the screen still opens a tooltip until the click closes it.

Tests:

  • hoveringPointer: mouse, hovering pen, pressed pen, finger.
  • smartEditor/hoverIsAMouses: a resting mouse asks for the hover and its leaving clears it. A hovering pen asks; a pressed one does not. A finger's tap, with its emulated mousemove, asks for nothing. A finger lifted leaves a Details hover. Four fail before the fix, the finger's tap among them.
  • formEngineGroupHighlightIsAMouses: the chip and a row light the group for a mouse, and not for a finger or an emulated mouseenter. All four fail before the fix.
  • Story SmartEditor › Hover Details On A Touch Screen (phone project):
    • A real finger (CDP touch) taps the editor to focus it. No hover is asked for or shown.
    • Details then asks once and shows it.
    • It fails before the fix, 3 of 3 runs. hoverDelayMs: 0 makes a hover started by a tap go out before Details' own, on the same connection, so it is seen however soon Details is tapped.
  • Story SmartEditor › Hover Follows The Mouse To The Next Word:
    • The browser's own mouse, moved by Playwright, sends trusted pointermove with pointerType: 'mouse'.
    • It puts the mouse back off the page when it ends (8444150). Left where it rested, it hovered Tracking/Experiment Card's titles in the stories after it, and their hovers said each title twice: eight of them failed in CI on 51bc71b. That reproduced locally with the two files in that order, and is gone with the fix.

Checks:

  • Typecheck (test and prod) and lint pass, and 236 files with 2451 tests pass.
  • Locally: SmartEditor (9 desktop, 2 phone), DpqlEditor (20), ExpressionField (38), FormEngine (187) and FormEngineRemote (11), and the whole phone project (8 files).
  • CI at 8444150 is green: 65 story files, 716 stories.
  • Docs: design/SMART_EDITOR_UX.md §5 and design/COMPACT_PARITY.md.

35. A host editor is never handed its own report back as a change (b04990a, qorus#646)

Symptom: under CPU load, the Qorus IDE's test-case Value (a componentOverrides editor, the IDE's own rich text) lost what was typed after a reference was erased, and Slate threw "Cannot resolve a DOM point from Slate point {path:[0,0],offset:1}".

Root cause, traced in the IDE:

  • The IDE's rich text replaces what it holds with any value it is handed that differs from it.
  • The emptied field reported "", which came back as "" and, once the form had stored it, again as undefined.
  • That second delivery landed after a 1 was typed and before it was reported, and replaced it.

Fix:

  • AutoField hands a host editor its own last report, or one still on its way back, as the editor reported it: the same object, with "" and undefined read as one empty value (useHostEditorValue).
  • Its onChange is now stable. A new function on each render had restarted the host editor's pause before it reports.
  • isHoveringPointer treats a pointer of no named type as hovering, as reqore's does.

Not fixable here: the first echo of a report, arriving after the next key, is a genuine change of value from the editor's point of view. Only the editor can recognise it as its own; reqraft's own rich text does, using EmittedValues.

Tests:

  • hostEditorValue (10 cases).
  • Story Components/Form/Auto › Host Editor Keeps What Is Typed When Its Report Comes Back: fails before the fix (the typed 1 wiped), 2 of 2 runs.

36. EmittedValues / useEmittedValues are public (4086837, qorus#646)

They are exported from the package root, with the TEmittedValuesEquals type, for a host's own debounced field. The Qorus IDE's rich text uses them in qorus-ide f1c58c2d0.

Documented:

  • record each report;
  • isEcho once where an incoming value is committed;
  • includes from a render;
  • settled for what the parent will hold.

Sameness is the caller's equals, kept from the first render. By default it is Object.is, under which '', undefined, null and an empty document all differ.

Tests: emittedValuesExport, 6 cases, importing from the root.

37. Foxhoundn's review on qlip build #361 (58c58df, 4688fc1)

  • Field · Choices Described On A Touch Screen ("That info button is massive"): the ⓘ takes its group's size, as its choice does. A field's few choices are small on a touch screen, BRAND_DESIGN §5's minimum for a finger. The story checks each ⓘ is as tall as its choice.
  • SmartEditor · Hover Details On A Touch Screen ("covering the input"): Details is beside the text, in one row. Its card opens under the button, opaque and clear of the text. The story checks both.
  • The 16 Experiment Card / Experiments Overlay frames (the actions folded into a ⋮ menu): fixed in reqore #717, option A. A narrow EntityRow keeps its actions visible, and the fold is opt-in (narrowActions='menu'). This PR picks that up through a reqore bump once #717 is released. Until then those frames still show the fold.

38. A required field that arrives with the schema is the row the first-required scan opens (aac66a1; a regression the Qorus IDE's live stories found on g8612725)

  • Symptom: the IDE's pipeline processor form left its required Field Mappings row closed.
  • Cause: the scan (expandFirstRequired / autoFocusFirstRequired) ran once and was spent even when it opened nothing. It read the form's value before the sync-down had added the field the schema brings. The flaw is older than this PR. b1c83fb exposed it in the IDE, which hands its value through fixOptions: before that commit the value was taken whole a render early, and since then it is recognised as the form's own report, so the form adds the field itself a render after the scan.
  • Fix: the scan is keyed on a field's arrival. It reads the form the schema makes, and opens the first newly arrived field that needs an answer, until it has opened one or the user has moved. A field already in the form that comes to need an answer is not opened.
  • Tests: formEngineRequiredFieldArrivesWithItsSchema (6 cases; the IDE-shaped one fails at b1c83fb and passes at b04b670). Story: FormEngine › Compact Required Field Arrives With Its Schema.

39. The colour field's problem message (b222dc9): it was spaced with a rule on .reqore-message. The field's own wrapper spaces it now (FRONTEND.md). No visible change. Found auditing this PR; out of its scope and filed as #143: SelectCollection's styled(ReqoreModal) with !important min sizes, on develop.

40. develop merged in (2b7614e): #141, the rich-text choice chip.

41. #142 folded in (merge 64a5173, at David's direction; #142 is closed and its review continues here)

  • One reference chip in every view: the read summary, the read-only chip, the rich text and the DPQL editor all use REFERENCE_CHIP_PROPS, a tiny chip whose label takes the font of the text it sits in (effect.textSize: 'inherit') (e4fc552, e31bae1).
  • A chip is drawn once its catalog arrives (e4fc552, change module type to commonjs #11 first fix). The catalog is written out once a render (namingKey). Story: RichText › Chip Drawn When Its Catalog Arrives.
  • An operand's text is as wide as what it holds, through TemplateField's own textWidth prop and the panel's contentStyle, with no rules on Reqore's classes (824e27b, 0b675cd, 260083a).
  • The Text / Visual toggle is as wide as its words, and says them in the host's language (modeLabels, expressionModeLabels) (51b5020).
  • The DPQL editor is drawn at the field's size, so a reference is the same chip on a value's Value and Expression tabs (David's Set the default value of waitForStorage to false andensure it only waits in useStorage stories. #9) (ccb9fc2). Story: FormEngine › Reference Is The Same Chip On Value And Expression.
  • A reference typed out in full becomes its chip once a character that ends it follows it, or the editor is left. The converter's endsReference says which characters end a reference (David's change module type to commonjs #11) (64952bb). Test: typedReferencesBecomeChips. Story: ExpressionField › Text Mode Reference Typed Out Is A Chip.

42. A WebSocket whose handshake hangs is closed and replaced at its deadline (a2967c0; develop merged in by f6758f1)

  • Symptom: qorus-ide #299's CI failed because a creator WebSocket to hq stayed "connecting" for about 6 minutes. A handshake that hangs (the server takes the connection and never answers the upgrade) fires no open, no error and no close until Chromium gives up, about 4 minutes later. Chromium runs one handshake per host and port at a time, so a hung reqraft language-server or DPQL socket also holds back the IDE's creator socket to the same instance, and the other way round. Closing the hung socket fires its error and 1006 close at once, and the queued socket opens about 12 ms later.
  • Cause: ReqraftWebSocket.connect() set no handshake deadline and replaced a socket only after a close, so a hung socket was never replaced. Its server-status probe before each attempt (GET system/pid) had no time limit either, so a server that hangs REST too also stalled the replacement. The IDE fixed the same gap in its own socket layer on qorus-ide #281.
  • Fix (src/utils/websocket.ts):
    • Deadline: a socket's handshake has 15 s to open: the handshakeTimeout option, default REQRAFT_WS_HANDSHAKE_TIMEOUT_MS, the same as the IDE's WS_HANDSHAKE_TIMEOUT_MS (0 sets no limit). A socket still connecting at the deadline is taken out of the pool, detached and closed. Its users get what a failed handshake gives them, an error and then a close with code 1006, through their own handlers, and they reconnect through maybeReconnect with the backoff and attempt cap of a close: the first attempt at once, then one every reconnectInterval, then onReconnectFailed. A hang is never read as anything else. The deadline is cleared on open, error and close, and when the caller closes. An open socket has no deadline.
    • Probe: waits at most 10 s (probeTimeout, REQRAFT_WS_PROBE_TIMEOUT_MS), and then aborts its request through an AbortController. query() and queryRaw() take a signal for this.
    • One socket per connection: a hung socket leaves the pool before its replacement is dialed. A handler is removed from the socket it was added to, so the hung socket's own late close reaches nobody. Pooled users of a hung socket share one replacement.
    • A caller who closes leaves nothing behind: remove() and close() call off a reconnect waiting to run and its probe. A socket closed while it is still connecting reports 1006, not 4999, so a socket closed on purpose is marked and never reconnected. A user removed between sockets no longer counts down a socket another user has dialed since. closeAll() takes its sockets out of the pool at once; it deleted them under the wrong key, so a connection made right after it joined the socket that was closing.
    • A user's onClose or onReconnecting that throws no longer stops the reconnect.
    • Timers are event-driven (one deadline per socket, one per probe), with no polling.
    • Docs: a ReqraftWebSocket section in the README (options, the deadline, the probe, closing), JSDoc for the new options and constants, and the WebSocket notes in .claude/CLAUDE.md.
  • Tests (fake timers only, no sleeps; a HangingWebSocket fixture that opens only when a test opens it and closes as a browser does):
    • websocketHandshakeDeadline (25):
      • the socket is closed and replaced at the deadline, detached, and its users hear error, then close 1006, once;
      • one that opens just before its deadline is kept;
      • a configured handshakeTimeout, and 0;
      • backoff and attempt cap after a hang, with the same schedule as after a refused handshake (that case also passes on the old code);
      • recovery when a later attempt opens, and no deadline on an open socket;
      • pooled users share one replacement;
      • a throwing onClose or onReconnecting;
      • remove() and close() during the hang, while the next attempt waits, and while the probe is in flight leave no timer, no socket and no later attempt;
      • a probe that never answers is cut off at its limit;
      • closeAll() on a connecting socket.
    • websocketProbeAbort (3): through the real query and fetch, the probe's request is aborted at its limit and when the caller lets go, and query() passes its signal to fetch.
    • websocketHandshakeConsumers (6): useReqraftWebSocket says it is reconnecting at the deadline and is OPEN once the replacement opens, says "Reconnect failed" when every attempt hangs, and leaves nothing behind on unmount. ReqraftLspClient connects on the replacement, and leaves nothing behind when its last client goes during the hang or while the replacement waits.
    • Without the fix, 32 of the 34 new tests fail. Run against the fix with each part taken out, they fail where they should: no deadline (22), an unbounded probe (3), remove() that does not stop the reconnect (6), close() that does not (2), no on-purpose mark (1), no finally around onClose (1), handlers removed from the pool's socket (10).
  • Version: 0.10.65. develop is 0.10.64, and the gate wants develop + 1. The bump is in the merge commit f6758f1. feat(supportTicket): the delegated-access vocabulary, the request card with the notification look, the workspace access rail value (qorus#697) #148 is at 0.10.65 too: whichever of the two merges second must merge develop and bump once more first.
  • develop merged in (f6758f1, a normal merge): 0.10.64, with chore(deps): clear the open Dependabot alerts with one lockfile refresh — 0.10.63 #146's Dependabot lockfile refresh and chore(deps): clear the last Dependabot alert (esbuild under vite) — 0.10.64 #147's vite 8.3.5 and esbuild 0.28. develop changed only package.json and yarn.lock. In yarn.lock, develop's entries are kept where both sides changed (js-cookie 3.0.8, lodash 4.18.1), and yarn install dropped what only react-color needed, which this PR removes. reqore stays at the released 0.78.5.
  • Checks at a2967c0: lint, the type checks (test and prod) and the unit tests pass: 276 files, 2807 tests. The story run passes: 750 stories. Against a run at 92078d8 (before the merge and the fix), 732 of the 736 captures are the same byte for byte. The other 4 also differ between two runs of the same tree, and each matches the other tree's capture on a rerun: hover and fade timing, not the merge or the fix. They are DpqlEditor › Details On A Chip On A Touch Screen, DpqlEditor › Quoted Field Typed Out Is A Chip, ExampleValueModal › Mobile and Menu › Width From Storage. CI's qlip build 20261021-133007 has the same counts as fix: a card's one-line name centred on its actions; numbers with templates shown as used; operands named — 0.10.65 #137's previous build 20261018-134849 (102 changed, 23 new, 16 denied, 625 unchanged). The only image that differs between the two is the new DpqlEditor › Details On A Chip On A Touch Screen: its Details card was captured at another point of its fade-in. So the develop merge changes no frame. The fix is not visual, so there is no story table.

The combined push (62fceccb), approved by David as a story table:

  • What it adds:
    • round 7b: Cancel, Escape and Revert keep an expression an expression (a68c62db), and the Value tab draws a referenced field as its chip (b9b8891d);
    • round 8: the stories' server writes a record field as Qorus does;
    • item 2: a record field is one chip, "record: ", wherever it is drawn (d570b90f);
    • R1-R3 (9171f129): a host's tall editors open in the card; no preview under a host summary; a read-only form opens them to be read;
    • the narrow-row rule (efca71fb): a compact row stacks where its value would be narrow, its value 12px in;
    • three fixes found while checking (dd113c0c, 64398929, b89f99fa).
  • reqore 0.78.10 (8fd61f37), released from reqore#722. A narrow row keeps its actions as buttons, so the Experiment Card's Show analytics, Accept and remove are on their rows again, and 3de44751 has the stories check them. That answers Foxhoundn's 16 denials on qlip build #381; each is answered on its snapshot and in a comment here. No prerelease is pinned; the lockfile resolves 0.78.10 from npm (yarn install --frozen-lockfile passes).
  • Version: 0.10.65, one bump at develop + 1 (develop is 0.10.64). This head was merged in, not rebased.
  • Checks at 62fcecc:
    • Precheck: 281 files, 2862 tests, 0 failed.
    • Every story file: 767 passed, 0 failed (57 files, 753 captures). They are the frames David approved, except a 1px Menu difference and DpqlEditor · Details On A Chip On A Touch Screen (a known flake, which matched on two recaptures).
    • CI: PullRequestTests and the prerelease pass.
    • qlip build #382: the 16 previously denied frames equal develop's baselines (0.00%). The other 143 changed or new frames are each flagged with their cause; none is accepted.

A click on a compact row opens or closes it by what was drawn (b1aff2d8), approved by David as a story table.

  • The problem: a click on a closed row while a read-first form was still settling opened the editor and closed it again in the same task.
    • The supah session found it: the Qorus IDE's Interfaces/Schema "Draft Is Saved" failed on every run.
    • A row click was a toggle that read its direction from the latest state. The first-required scan opens its row with an update of its own, and the click on that row, still drawn closed, closed it again.
  • The fix: expandOption and collapseOption replace the toggle, and each does nothing to a row already in its state.
    • expandOption still spends the first-required scan and keeps expandMode='single'.
    • The row click, and a read-only row that opens its card, decide from what they drew.
    • Done, Cancel edit and the label collapse close.
    • The row context is internal: it is not exported, and the Qorus IDE does not use it.
  • Tests:
    • compactRowOpensAsDrawn, the race: a click on a row drawn closed while the scan's open is pending ends open. It fails first.
    • The same file checks that Done, Cancel edit and the label close an open row, and that the next click opens it again.
    • Story Form/Engine/FormEngine · A Click On A Row As The Form Opens It Keeps It Open: it fails 3 of 3 before the fix and passes 3 of 3 after. It has no timing assertions.
  • Checks at b1aff2d:
    • Precheck: 282 files, 2867 tests.
    • Story run: 768 passed, 0 failed, 754 captures. The only new frame is the story's; no frame David approved changes.
    • Version stays 0.10.65. Prerelease: @qoretechnologies/reqraft@0.10.65-pr.137.gb1aff2d.

Checks at 64a5173: typecheck (test and prod) and lint pass, with 265 files and 2685 tests passing. Every story file, run one at a time, passes: 734 stories.

Version 0.10.65 (develop is 0.10.64 since #146 and #147).

🤖 Generated with Claude Code

https://claude.ai/code/session_01T3k69KvVFmLA6bq4gwXtA9
https://claude.ai/code/session_01EvsXdPibtvZARTVPZm9L2a

@davidnich
davidnich requested a review from Foxhoundn as a code owner October 7, 2026 09:29
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

📦 Prerelease published for this PR:

@qoretechnologies/reqraft@0.10.65-pr.137.gfb70c1c

Install it in a consumer (updates package.json + lockfile):

yarn add @qoretechnologies/reqraft@0.10.65-pr.137.gfb70c1c

Published under the pr dist-tag — latest/beta are untouched. Repoint consumers to the released version once this PR merges.

@qlip-qore-technologies

qlip-qore-technologies Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

qlip visual regression

🔍 446 changed, 22 new of 806 snapshots.
⏳ Awaiting review.

Review build 20261024-155458 in the dashboard →

@davidnich davidnich changed the title fix: a card's one-line name centred on its actions; a number that takes templates shown as it is used — 0.10.61 fix: a card's one-line name centred on its actions; numbers with templates shown as used; operands named; ships #136 — 0.10.61 Oct 7, 2026
@davidnich

Copy link
Copy Markdown
Member Author

Qlip build 20261007-114032: the 12 pending snapshots, for review. I have not accepted, flagged or denied anything; this only explains each one. I compared each changed snapshot's baseline and new image around its diff regions.

New stories (2, no baseline):

  • Template › Number Typed Freely: the whole-number field that takes templates. Clicking it lists the templates at once; what is typed is kept as typed; a chosen template becomes the value.
  • FormEngine › Compact Row Says Where It Moves: a required field filled in while it is open stays under "Needs attention" and says Moves to "Set" when you close it (✓) under its editor.

Operand message wording (6, ExpressionBuilder): Add Value Slot, Add Value Slot While Incomplete, Addition Picked Fresh, Variable Arguments Can Be Added, Discard All Incompatible Function Args, With Arg With Allowed Values.

  • An empty operand's message changes from Value for argument N ("Value") is invalid: Missing value (or "...: Value must be an integer", "...: Value must be a non-empty text", "...: Text value is empty") to Enter a value for "Value" (validations.ts).
  • Nothing else moves in the first four.
  • Discard All Incompatible Function Args and With Arg With Allowed Values have two more differences that come from develop, not this PR. Their baseline is develop build 212 (bb72ef3, 2026-09-21), and develop has changed both since: the operation picker's count badge (14 → 15, 14 → 11) and the Ignore case? operand's type label (TRUE OR FALSE → BOOLEAN), from 74cd676, d7f3fc3 and b42f892 (conditions offer any operation whose result decides them). Both stories' descriptions changed on develop too.

Card header centred (1, FormEngine › Compact Expressions): the open card's one-line heading, "Condition", now sits about 4px lower, centred on the actions beside it rather than lined up with their top (CompactRow.tsx).

"Moves to" note (3, ExpressionField): Field Menu Stays In The Toolbar, Offer Survives A Compact Row, Text View Survives A Late Host Change. Each edits an expression row whose heading changes when it closes, so its open editor now shows Moves to "Set" when you close it (✓) under the 1 + 2 editor. Nothing else moves.

@davidnich

Copy link
Copy Markdown
Member Author

Qlip build 20261007-164411 (f7424c7): 13 pending, explained, not reviewed. I have not accepted, flagged or denied any of them.

  • The 12 from build 20261007-114032 are unchanged.
    • Their diff percentages are identical, and so is my explanation in the comment above:
      • the operand message wording;
      • the two develop changes in Discard All Incompatible Function Args and With Arg With Allowed Values;
      • the card heading centred in Compact Expressions;
      • the "Moves to" note.
    • The new commits (the Text view, the Preview and the read-first summary of an unfinished or rich-text operand) do not move them.
  • 1 new story:
    • ExpressionField › Text Mode Of An Unfinished Expression: the Text view of > with its first operand switched to Use Expression (no operation chosen) and its second empty.
    • The editor shows … > …, a hole for each part not filled in, where it showed undefined([object Object]) > [object Object].

@davidnich

davidnich commented Oct 7, 2026 •

Copy link
Copy Markdown
Member Author

Qlip build 20261007-211708 (4e406da): 34 pending (22 changed, 12 new), explained, not reviewed. I have not accepted, flagged or denied any of them. I compared each changed snapshot's baseline and new image around its diff regions, and looked at every new one.

Already explained above, unchanged (11):

  • the operand message wording: Add Value Slot, Add Value Slot While Incomplete, Addition Picked Fresh, Discard All Incompatible Function Args, With Arg With Allowed Values (the last two also carry develop's badge-count and type-label changes);
  • the card heading centred: FormEngine › Compact Expressions;
  • the "Moves to" note: Field Menu Stays In The Toolbar, Offer Survives A Compact Row, Text View Survives A Late Host Change;
  • new: ExpressionField › Text Mode Of An Unfinished Expression, FormEngine › Compact Row Says Where It Moves.

Their images and diff percentages are the same as in the builds those comments describe.

A template in a field that takes custom values is now a chip in a text field (section 7 of the description), changed (13):

  • Template › Template Value, Braced Template Value, Aliased State Template Value, Extended Path Template Value: the value is a chip in a text field (Interface ID, Filename, Choices, Choices[0].message.content) where it was the name in the template picker.
  • Template › Shows Templates List: the list is the text field's own list. It is headed "Templates" where the picker's was headed by its one category, "Context Data"; the entries (Epoch Seconds, Interface ID, with their types and example values) are the same.
  • ExpressionBuilder › Expression With Int Return Type, Variable Arguments Can Be Removed, Template With Function Value: an integer operand holding a template ($local:id, $local:time, $local:start) is a chip in a 150px text field, as a text operand is, where it was a narrower picker. The operands after it move right by the difference.
  • ExpressionBuilder › Variable Arguments Can Be Added: the same for $local:time, besides the message wording explained above. The fourth operand, empty and of type integer, keeps "Select Template": an empty typed operand takes custom values only once it has a type, as before.
  • ExpressionBuilder › With Int Type: an any operand holding $local:input is a chip in a text field. The operation's count badge (14 → 15) is develop's, as explained above.
  • ExpressionBuilder › Operands Fit Narrow Column, Nested Fits Narrow Column: at 186px, the any operands Quantity and Price are chips in a text field that keeps its clear button and ⋮ inside the row. They were in the picker.
  • ExpressionBuilder › Function Args Are Passed To New Function And Need Confirmation: the date operand (11/05/2025, 23:00) has the $ button beside it that offers templates for a date. The operands after it move right by its width, and the badge count is develop's (14 → 15).

New stories (10):

  • Template › Number Typed Freely (described in an earlier comment): the chosen template, Epoch Seconds, is now a chip in the text field, where it was named in the template picker.
  • Template › Written As Text (9):
    • Template Alone: a whole-number field holding pos, a chip; it holds $record:{pos}, of type int.
    • Text Around The Template: text written after pos and deleted again; it holds the template again, of type int.
    • Number With A Template: pos deleted and 12 written; it holds 12, an int.
    • Number Picks A Template: an empty whole-number field, typed freely, with pos chosen from the list its first click opens; a chip.
    • Number Emptied Keeps Its Field: pos deleted, the same text field keeps the cursor, and 7 is typed; it holds 7.
    • Expression Detected: 1 + 2 written in an untyped field, with "Use as expression" offered beside it.
    • Null Stays A Value: an untyped field holding null; it holds null of type any, shown as an empty text field. This is how such a field looked on develop too (I checked develop, f7424c7 and this head); this PR does not change it.
    • Allowed Values: fixed choices (Open, Closed) keep their list; Closed chosen. The type it holds reads auto, which is AutoFormField's own report for a field with choices, the same on develop.
    • Date Keeps Its Control: a date field holding liefertermin, in the template control whose × returns to the date picker.

Gone since build 20261007-205028 (9). That build also showed text operands narrower than 150px (Accept All / Accept Incompatible Function Args Selection, Focused Editing, Groups Can Be Deleted, New Groups Can Be Created, With Complex Value, With Injected Extra Actions, With Simple Value, Nested Operand Keeps Injected Actions). That was a side effect of 1fabea9 (it removed the 150px minimum in every row, not only in a narrow one), fixed in db8c47c, and those snapshots are unchanged again.

@davidnich

Copy link
Copy Markdown
Member Author

Qlip build 20261008-041002 (b7f95b8): 39 pending (22 changed, 17 new), explained, not reviewed. I have not accepted, flagged or denied any of them.

33 are as in build 20261007-211708, with the same images and diff percentages. They are explained in my comment on that build above.

6 are new or different (section 8 of the description):

  • FormEngine › Whole Number With Text Around A Template (new): a whole-number option holding the field pos. The story types " Stk." after it, which shows "A whole number can't have text around a template" and makes the form unsaveable, then deletes the text again. The snapshot is that end state: pos alone, and the form can be saved.
  • FormEngine › Number With Text Around A Template (new): $record:{pos} Stk. in a number option. The chip pos is followed by Stk., with "A number can't have text around a template" under it. The form cannot be saved.
  • FormEngine › True Or False With Text Around A Template (new): the same in a yes/no option, with "True or false can't have text around a template" under it.
    • Its chip reads "record: pos", not "pos". The list a yes/no field offers holds only fields of its own type, so the chip is named from the reference's path. That predates this PR.
  • FormEngine › Date With Text Around A Template (new): a date option holding $record:{pos} Stk.. A date keeps its template control, which shows the value as written, with "A date can't have text around a template" under it.
  • FormEngine › Whole Number Literal And Expression Stay Valid (new): the same option holding 12, with no message; the form can be saved.
  • Template › Written As Text Null Stays A Value (new, image changed): the untyped field holding null now shows null where it showed an empty field. The story then deletes it (no value) and writes null again, so the end state is the same: null : any.

@davidnich

Copy link
Copy Markdown
Member Author

Qlip build 20261008-053829 (6303b15): 43 pending (22 changed, 21 new), explained, not reviewed. I have not accepted, flagged or denied any of them.

38 are as in build 20261008-041002, with the same images and diff percentages. They are explained in my comments above.

5 are new or different (section 9 of the description):

  • FormEngine › Host Reason In Place Of Required (new): an empty required whole-number option whose host gives "Enter a value or an expression". That is the field's one message; "This field is required" is not shown. The form cannot be saved.
  • FormEngine › Host Reason In Place Of The Field's Own (new): the option holding pos with " Stk." after it, whose host gives "Eine Ganzzahl kann keinen Text um ein Feld enthalten". That is the field's one message, in place of its own "A whole number can't have text around a template".
  • FormEngine › True Or False With Text Around A Template (new, image changed since build 041002): the chip now reads "pos" where it read "record: pos". It is named from the whole catalogue, not from the fields a yes/no offers, which pos is not one of. The message is unchanged.
  • Template › Written As Text Chip Of Another Type (new): a yes/no field holding the whole-number field pos with " Stk." after it; its chip reads "pos".
  • FormEngine › Yes No Holding A Template Read (new): a compact form whose yes/no option holds pos. Its collapsed row shows the chip pos, where it read "Yes".

@davidnich

Copy link
Copy Markdown
Member Author

Qlip build 20261008-065308 (bc24dc0): 44 pending (22 changed, 22 new), explained, not reviewed. I have not accepted, flagged or denied any of them.

  • 43 are as in build 20261008-053829, explained in my comments above.
    • Template › Number Typed Freely has a new image file, but its pixels match the previous one within anti-aliasing (under 2% per pixel, diff 0%). It shows the same chip, Epoch Seconds.
  • 1 new: Template › Written As Text Cleared By Its Host.
    • It shows a yes/no field holding the field pos, cleared by its host's "Clear value" button. The field is empty and holds no value.
    • It used to go on showing pos: the text field kept its own copy, and nothing emptied it when the value was cleared from outside (bc24dc0; David's review of qorus#646, "Clear value" had no effect).

…kes it text, and to delete it

qlip build 20261008-083118 (Foxhoundn, on Whole Number / Number / True Or
False / Date With Text Around A Template and Host Reason In Place Of The
Field's Own): "How does the user fix this? Why has this happened?"

"A whole number can't have text around a template" said what was wrong but
not what caused it or what to do. The message now names the text that makes
the value text, says it is therefore not of the field's type, and says how to
fix it:

- "Stk." makes this text, not a whole number. Delete it to keep the template alone.
- text on both sides: "ca. … Stk." makes this text, ...
- several templates: Templates side by side make this text, not a number.
  Keep one of them alone. (with text too: Text and more than one template
  make this text, ... Keep one template alone.)

The value stays invalid, as decided on qorus#646; only what is said changes.
The Host Reason story's host gives an actionable reason of its own, as a
host's reason should.

Tests: textAroundATemplate (the reason for each type, text before and on both
sides, several templates, textAroundATemplate itself; fail before),
hostInvalidReason follows the new wording.
… - its end frame shows nothing

qlip build 20261008-083118 (Foxhoundn): "It doesn't say anything in the story
- if it says that during the play test, then remove the snapshot, as it
doesn't provide any important info."

The note it asserts is shown only while the field is open, and the play
closes it, so the capture is an ordinary filled row. The story keeps running
in the storybook tests; only its capture is skipped (qlip: { skip: true }, as
Compact Row Read Only Is A Way In).
…y its position too

qlip build 20261008-083118 (Foxhoundn, on ExpressionBuilder Add Value Slot,
Add Value Slot While Incomplete, Addition Picked Fresh and Variable
Arguments Can Be Added): "the old version, which provided the argument
index / position is a better UX for the user to understand which "value" is
the message mentioning. This does not say what is exactly wrong".

Every operand of a call taking any number of them is named "Value", so
"Enter a value for "Value"" did not say which one. Where another operand of
the call has the same name, the message says which by its position too, as
the editor shows them left to right:

- Enter the 4th "Value"
- The 2nd "Value" is invalid: ...
- The expression in the 2nd "Value" is invalid: ...

A name that says which ("Lower bound"), or a lone operand, is said by its
name alone, as decided in David's review of qorus#646.

Tests: operandPosition (fail before).
… the first field edited

qlip build 20261008-083118 (Foxhoundn, on ExpressionField Field Menu Stays In
The Toolbar, Offer Survives A Compact Row and Text View Survives A Late Host
Change): 'Moves to "Set" when you close it (✓)' "does not need to be shown
everytime, the user will learn what happens after the first time ... otherwise
it just takes vertical real-estate."

David's decision: once per form, on the first field the user edits. The note
is said on the first field held open in its box, and on no field once that one
is closed; while two are held (expandMode multi), only the first says it.

Tests: movesToNoteOncePerForm (fails before). Story: Compact Row Says Where It
Moves fills a second field after the first is closed; it is held as the first
was and says nothing.
#137's one patch bump moves to 0.10.62.
@davidnich davidnich changed the title fix: a card's one-line name centred on its actions; numbers with templates shown as used; operands named; ships #136 — 0.10.61 fix: a card's one-line name centred on its actions; numbers with templates shown as used; operands named — 0.10.62 Oct 8, 2026
@davidnich

Copy link
Copy Markdown
Member Author

Qlip build 20261008-083118 (5fa0a45): the 16 rejections, answered. Pushed as 5fa0a45..4ac092c. I have not accepted, flagged or denied anything.

ExpressionBuilder › Add Value Slot, Add Value Slot While Incomplete, Addition Picked Fresh, Variable Arguments Can Be Added - "the old version, which provided the argument index / position is a better UX ... which "value" is the message mentioning".

  • Fixed in 8b7a187 (David's decision). Every operand of a call taking any number of them is named "Value", so where another operand has the same name, the message says which by its position too: Enter the 4th "Value", The 2nd "Value" is invalid: ..., The expression in the 2nd "Value" is invalid: .... A name that already says which one ("Lower bound") is said alone.
  • Test: operandPosition (fails before).

ExpressionField › Field Menu Stays In The Toolbar, Offer Survives A Compact Row, Text View Survives A Late Host Change - 'Moves to "Set" when you close it' "does not need to be shown everytime ... only ... on the first field".

  • Fixed in b7a3298 (David's decision: once per form, on the first field the user edits). The note is said on the first field held open in its box, and on no field once that one is closed; while two are held, only the first says it.
  • These three stories edit the only field of their form, so that field is the first one edited and still shows the note once. That is the decided behaviour, not a leftover. In Field Menu Stays In The Toolbar, the extra button beside the field (from 5fa0a45) is gone with the revert.
  • Test: movesToNoteOncePerForm (fails before). Compact Row Says Where It Moves now fills a second field after the first is closed: it is held the same way and says nothing.

FormEngine › Compact Row Says Where It Moves - "if it says that during the play test, then remove the snapshot".

  • Its capture is skipped (8f31807, qlip: { skip: true }). The story still runs and asserts the note, and now that a second field says nothing.

FormEngine › Whole Number / Number / True Or False / Date With Text Around A Template - "How does the user fix this? Why has this happened?"

  • Fixed in 4e08f55. The message now names the text that causes it, says why the value is invalid, and says what to do: "Stk." makes this text, not a whole number. Delete it to keep the template alone.
    • With text on both sides it names both: "ca. … Stk.".
    • With several templates: Templates side by side make this text, not a number. Keep one of them alone.
  • The value stays invalid, as decided on qorus#646. These values come from the user typing after a template, or from a saved value.
  • In build 20261008-083118 the whole number, number and yes/no fields also showed the raw $record:{pos} Stk. in a picker (from 5fa0a45). With the revert (2343a15) they show the chip pos with "Stk." after it again, and Whole Number With Text Around A Template again ends with the text deleted and the form saveable.
  • Test: textAroundATemplate (fails before).

FormEngine › Host Reason In Place Of The Field's Own - same question.

  • The story's host reason was a sample. A host's reason should say why and what to do, as the field's own now does, so the story's host now gives Quantity counts pieces: delete "Stk." so that it holds pos alone (4e08f55). The field shows it in place of its own message.

FormEngine › Lone Template Written Is Stored Bare, Saved Template Opens In The Text View (+ On A Phone) - "The template button should be the same size as the input, and preferably in a stacked group".

  • These stories, and the button beside the Text view, came from 5fa0a45, which is reverted (2343a15). It was a paused design, replaced by the tabbed value entry (Value · Expression · Visual) now being built. The size and stacked-group point is passed on to that work.

Other changes in this push:

…sion · Visual, or Value · Template (qorus#646)

One value, three views. Value to Expression writes the value as expression text; Expression to Value
converts a lone value or template, and otherwise says it cannot show the expression and offers to
replace it. An expression typed on Value moves to Expression, with an undo. Saved values open on their
tab. A template of another type is a warning. Without a language server, Expression moves to Visual,
which starts from value(...) as the server parses a value written alone. The menu keeps value actions
only. Fields outside a form's options keep the behaviour they have.
@davidnich

Copy link
Copy Markdown
Member Author

Qlip build 20261008-123352 (bf948c6): 26 pending, 3 carried-forward rejections, explained, not reviewed. I have not accepted, flagged or denied anything. CI passed (Pull Request Tests, Publish).

Build 288's 16 rejections:

  • Removed (3): Lone Template Written Is Stored Bare, Saved Template Opens In The Text View (+ On A Phone). They went with the revert of 5fa0a45.
  • No longer captured (1): Compact Row Says Where It Moves.
  • Changed as answered (9):
    • Add Value Slot, Add Value Slot While Incomplete, Variable Arguments Can Be Added: Enter the 4th "Value".
    • Whole Number / Number / True Or False / Date With Text Around A Template: "Stk." makes this text, not a number. Delete it to keep the template alone. The chip is back where 5fa0a45 showed a picker.
    • Host Reason In Place Of The Field's Own: the host's actionable reason.
    • Field Menu Stays In The Toolbar: the extra button from 5fa0a45 is gone. The note stays: it is the form's first edited field.
  • Carried forward unchanged (3):
    • Offer Survives A Compact Row and Text View Survives A Late Host Change: each edits the only field of its form, which is the first field edited, so it shows the note once, as decided (once per form, b7a3298). A second field edited in the same form does not show it; see Compact Row Says Where It Moves and the movesToNoteOncePerForm test.
    • Addition Picked Fresh: one operand, so Enter a value for "Value" names it alone. David's decision adds the position only where another operand has the same name. Whether a lone operand of a call that takes more (the "Add value" slot) should still say "the 1st" is open for David.

The other 17 pending are the revert putting back bc24dc0's images, identical to build 20261008-065308, explained above:

  • ExpressionBuilder: Expression With Int Return Type, Function Args Are Passed..., Nested / Operands Fit Narrow Column, Variable Arguments Can Be Removed, With Int Type.
  • ExpressionField: Toggle In Form Engine, Via Form Engine Text Typing.
  • Template: Shows Templates List, Template With Function Value, Written As Text Chip Of Another Type, Date Keeps Its Control, Number Emptied Keeps Its Field, Number Picks A Template, Number With A Template.
  • Template Value and Number Typed Freely differ from build 065308 only by anti-aliasing (no pixel differs by more than 40/255).

@davidnich

Copy link
Copy Markdown
Member Author

qlip build 20261008-124320 (86641aa, the value tabs): what changed against bf948c6's build (20261008-123352)

Compared image by image with bf948c6's build. Everything below that build already had pending (the written-as-text and text-around stories, Template Value, Number Typed Freely, Shows Templates List, Chip Of Another Type, and the two denied ExpressionField stories) has the same diff as there, so those explanations still apply. What 86641aa changes:

New stories (13), the tabs themselves. Value Tab Per Option Type; Value To Expression And Visual (+ On A Phone); Saved Template Opens On Value; Saved Template On A Yes No Opens On Expression; Saved Text With Templates Stays Text; Saved Expression Opens On Expression (+ On A Phone); Template Only Option Has Value And Template (+ On A Phone); Expression Typed On Value Switches With Undo; Template Of Another Type Warns. Each end frame is the state its description names. Value To Expression And Visual On A Phone ends on the Visual tab showing Value: 12.

FormEngine stories that now show tabs in an open option (11). Basic, Small, Focused Editing, Description Is Shown, With Types Shown, Row Actions Stay In The Row, Optional With Values, Optional Opened, Invalid Shown Only, Compact Field Types Editing, Option With Any Type. Each option that takes templates has Value · Template in its header row (none of these options take expressions). Where the field's ⋮ held only the old mode switches (Use Template, Use Custom Value), it is gone, because the tabs are the mode switch. Examples are the colour, file, yes/no and date options in Basic and Invalid Shown Only. An untyped option keeps its ⋮ with the "set a value of this type" choices: Option With Any Type ends with an empty option set to Boolean from that menu. Option With Any Type also picks a rich-text template on the Template tab, which turns the value into rich text in its own editor, as before.

ExpressionField.

  • Field Menu Stays In The Toolbar (0.02% → 1.25%): typing 1 + 2 now moves the field to the Expression tab instead of offering the switch. The one ⋮ sits in the tabs' row, which is what the story asserts.
  • Via Form Engine Text Typing (13.8% → 4.5%): name typed in the Text view no longer shows "Expression definition value not found". The stories' expression catalogue now has value, as the server defines it (a value written alone is value(...)), so the parse is valid and the form has no invalid field.
  • Toggle In Form Engine: the same diff as before, within 0.0003%.

ExpressionBuilder, narrow column (Nested Fits Narrow Column, Operands Fit Narrow Column, 0.927% → 0.938%). The operation picker's count reads 16, not 15: value was added to the stories' catalogue. The other ExpressionBuilder diffs are within 0.001% of bf948c6's.

…ere not (qorus#646)

valueTabsLabels: the tabs, their tooltips, the cannot-show and replace text, the conversion warning and the
undo, with English defaults; a label that holds a value is a function of it, so a host keeps its language's
word order. A tab is identified by its key (data-tab), not by its label.
… numeric keyboard (qorus#646)

As its own input was: inputmode numeric for a whole number, decimal for a number. Templates are picked from
the list the field opens, and an expression is written on the Expression tab.
…s position (qorus#646)

David: an operand of a call with an "Add value" slot is said by its position even when it is alone -
"Enter the 1st \"Value\"" - as the operands that join it will be. The position is said when another
operand has the same name or the call is variadic.
…ext and centred on it (qorus#646)

The picker took the form's size (32px in a small form) beside the DPQL editor, which is drawn at the normal
size (38px) in any form; it takes the editor's size. And the editor had a 48px floor of its own: an empty
strip under its 38px text that nothing beside it could match. It has none now - one line as tall as its
text, growing with its lines.
…at() (qorus#646)

Value to Expression wrote an empty value as the text joined from no parts - concat() - and what was typed
then went after it.
…omes from (qorus#646)

savedValues: the field's ⋮ offers "Save this value", which hands the value and the field type to the host
(it asks for a name and keeps it), and "Use a saved value", which lists the values saved for the field's
type; one is the value at once, on its tab, and can be removed as the host keeps it. Not offered for an
empty value, a yes / no, an expression, fixed choices or a read-only field. In a form row the actions join
the row's ⋮; elsewhere the field's own.

templateBadge: the badge a template shows in the template picker - where it comes from, as the host knows.

Tests that waited a fixed time now wait for the field's menu to be published.
@davidnich

Copy link
Copy Markdown
Member Author

qlip build 20261008-142953 (c780b93): what changed against 86641aa's build (20261008-124320)

This covers 07e5333, 5da9d74 and c780b93 (sections 12 to 14 of the description), compared image by image with 86641aa's build. Everything not listed here has the same diff as there, so the earlier explanation still applies.

New stories (3). FormEngine › Saved Values In The Field Menu (+ On A Phone) and Template Badge From The Host. Saved Values ends on Quantity holding 48, after "A pallet" was picked from "Use a saved value". Template Badge ends on Source's Template tab, its picker reading pos with the badge "Row field".

The Text view is 10px shorter (2aa29bd). The DPQL editor had a 48px floor below its 38px text: an empty strip that nothing beside it could match. It has none now, so whatever sits under the Text view moves up by that strip, and nothing else changes.

  • Text Mode Type May Not Fit (0% → 1.39%): "This may not fit" sits directly under the text, and the card is shorter.
  • Offer Accepted Opens Text Mode, Offer Survives A Rederived Schema, Text Mode Auto Asks Nothing, Text Mode Type Fits, Via Form Engine Text Mode (0.007% to 0.012%): the same shift, in a card with nothing much under the text.
  • Compact Expressions (0.13%) and Via Form Engine Text Typing (4.48% → 5.14%): the same shift.
  • Field Menu Stays In The Toolbar (1.250% → 1.252%) and Saved Text With Templates Stays Text (0.87% → 0.89%): the same shift. On the Expression tab the template picker now has the text's 38px height (it was 32px in these small forms), level with the text.
  • Offer Survives A Compact Row and Text View Survives A Late Host Change (0.024% → 0.153%): these were denied on bf948c6's build for their earlier change. This diff is that strip on top of it.

Addition Picked Fresh (0.0528% → 0.0509%). The lone operand of Addition, which can take more operands, now says its position: Enter the 1st "Value" (David's decision, 07e5333).

… value (qorus#646)

Found clicking through the review in the IDE: a field picked on a value's Value tab, then the Expression tab
opened straight away, was lost - the value was left empty. The text field sends an edit 100ms after it is
made, and it went away first, with the edit. It sends a waiting edit as it goes. And the template field
reported that text with the expression flag of whichever tab was open by the time it arrived - so the pick,
and a template chosen from the saved values of a yes / no, was stored as an expression. On the tabs that text
is the Value or the Template tab's: a value.
@davidnich
davidnich requested review from Foxhoundn and removed request for Foxhoundn October 23, 2026 09:55
… badge (qorus#646)

reqore's rich text editor draws its chips as compact badges (`asBadge`, `compact`) unless told
otherwise: a reference in an expression or a rich text field was a rounder pill (7px corners)
beside the tags (3px) of the read row and the read-only chip. Foxhoundn on qlip #392: "Some
template chips are pills, some are badges - let's unify them please".

REFERENCE_CHIP_PROPS, which every view draws its chips with, now says `asBadge: false` and
`compact: false`. `expectOneChipShape` (src/stories/chips.ts) checks a story's chips share one
shape; FormEngine · Expression Typed On Value Is Offered With Undo uses it and fails with the
badge (7px against 3px).
… under the chip clicked (qorus#646)

A mouse's hover was drawn as glass (a flat, transparent popover with a backdrop blur): over an
empty page it was its words with nothing under them. Foxhoundn on qlip #392: "It looks like the
background is transparent? Is it not using a ReqorePopover / the tags tooltip?" It is now the
popover's card, the surface a chip's hover and Details are drawn on.

A chip's list was anchored at the chip, then moved to the caret when its request went out: the
re-measure meant for typing ran for a chip's request too, and the click left the caret after the
chip. Foxhoundn on qlip #392: "The opened list is not under the 2nd chip". A list opened at a chip
stays under it, and a list put away is at no chip from then on.

DpqlEditor · Hover Says What A Function Is and Hover Over A Chip Says What Its Path Is check the
hover's card is opaque; A Click On A Chip Opens Its List And A Second Closes It checks the list is
under the chip clicked, from its left edge. All three fail without the fix.
… on its name's line, and an open row with less to read (qorus#646)

Foxhoundn's qlip #392 review:

- "please right align the question mark to get rid of the stair-case look": in a row's label
  column the (?) is at the column's right edge, on the name's first line, the same x in every row.
- "The required asterisk on a new line is not good, keep it on the same line as the field name":
  the column counted the help as the 12px icon it was before it became a button (26px, 32px for a
  finger), so every name within 14px of the column's width put its asterisk on a line of its own.
  The column is now the widest name plus its chrome at their real widths (`labelAffordanceWidth`),
  never narrower than the 152px it always was (`LABEL_COLUMN_MIN`: the floor is the column's, so a
  form of short names keeps its column), and a word joiner holds the asterisk to the name's last
  character, keeping the name one text for anything that finds it by its words.
- "Remove the whole `Moves to` please, move the `Use as an expression` to the right of the field":
  an open field held in its box no longer has a note saying where it moves when closed, and an
  expression found in what was typed is offered to the right of the field, on its line, under it
  only where the row has no room for both.

The stories check it (src/stories/labelLayout.ts): Compact Option Depends On Option In Required
Group and Or Another Option, Canceling An Edit That Made An Expression Puts Back The Value, and
Expression Typed On Value Is Offered With Undo fail without the fix; Compact Row Says Where It
Moves is Compact Row Stays In Its Box While Open, and its unit test says nothing is said
(__tests__/heldFieldSaysNothingOfItsMove.test.tsx). The rules are in design/FORM_ENGINE_COMPACT.md.
…revert restores (qorus#646)

FormEngine · Reverting A Removed Expression Puts It Back held pos + 1 again after the revert, and its
Expression tab showed an empty editor. Removing the value emptied the text and left the visit marked as
written, so the seeding that writes an expression into the editor never ran for the one the revert put
back. An emptied editor is unmarked: a revert or an undo is written into it, as on entering Text. The
play checks the editor shows pos + 1, and fails without the fix (Foxhoundn denied this frame on qlip
#383; it showed the bug).

The Compact Row Cancel Edit plays wait for the row: in Storybook's UI a play starts as soon as the story
mounts and the form draws its rows a moment later, so the click went to nothing and the play failed
there while passing in the test runner.
…UI too (qorus#646)

A scan of every story served in Storybook's UI found plays that pass in the test runner and fail there:

- taking a dispatched mouse off the page did nothing (there is no browser mouse to move), so a hover the
  play waited to close never did (FormEngine · Compact Expression Template Chips). It now sends the out
  and leave events from what is under the point, taking the elements left before telling any of them.
- Sticky Headers On A Phone scrolled before the form had measured itself and taken the narrow layout;
  it waits for that layout.
- Only One Box Header Is Ever Pinned: at a 1080px screen the scroll ended before the last box reached the
  line it pins at. The last box has 30 rows, longer than the scroller at any screen height.
…e the session can (qorus#646)

The editor asked for a hover only as the pointer moved. A read-only summary drawn as a row closes
connects its session after it is drawn, so a mouse already resting on a word there showed nothing until
it moved again: FormEngine · Compact Expression Template Chips failed in Storybook's UI, whose play rests
the mouse once and does not move it. The hook keeps where a hovering pointer rests until it leaves, and
asks for its hover when the session comes to answer, once.

__tests__/smartEditor/hoverOfAMouseAlreadyResting.test.tsx: answered once the session can, without
moving (fails without the fix); not after the pointer left; asked once while the session stays ready
(fails without the fix).
@davidnich

Copy link
Copy Markdown
Member Author

Foxhoundn's 10 denials on qlip #392, answered (pushed in b62c14a; new frames on build #393):

  • Expression Typed On Value Is Offered With Undo: Fixed: every reference chip is now the same tag, the "Moves to" note is gone, and "Use as expression" sits to the right of the field on its line. #392 → now
  • Compact Option Depends On Option Or Another Option: Fixed: the label column now counts the (?) button's real width, so the asterisk stays on the name's line. #392 → now
  • Compact Option Depends On Option In Required Group: Fixed: the label column now counts the (?) button's real width, so the asterisk stays on the name's line. #392 → now
  • Canceling An Edit That Made An Expression Puts Back The Value: Fixed: every row's (?) is now at the label column's right edge, so they line up. #392 → now
  • ExpressionBuilder · Condition That Gives A Number On A Phone: Yes, by design (David agreed): a value the list names shows its name ("input"), and "local: input" was the raw form shown while the list did not include it. #392 → now
  • ExpressionBuilder · Args Change When Operator Changes: Yes, by design (David agreed): a value the list names shows its name ("input"), and "local: input" was the raw form shown while the list did not include it. #392 → now
  • DpqlEditor · Hover Says What A Function Is: Fixed: the hover was a transparent glass popover; it is now ReqorePopover's normal card, like a chip's tooltip. #392 → now
  • DpqlEditor · Hover Over A Chip Says What Its Path Is: Fixed: the hover was a transparent glass popover; it is now ReqorePopover's normal card, like a chip's tooltip. #392 → now
  • DpqlEditor · A Click On A Chip Opens Its List And A Second Closes It: Fixed: the list followed the caret the click left after the chip; it now opens under the chip that was clicked. #392 → now
  • Reverting A Removed Expression Puts It Back: Fixed a real bug this frame showed: after the revert the form held pos + 1 but the editor stayed empty, and it now shows the expression. #392 → now

@davidnich
davidnich requested review from Foxhoundn and removed request for Foxhoundn October 23, 2026 15:32
The ticket stories date their messages from a fixed instant, but the
components write each date relative to the real clock: TicketThread
through ReqoreTimeAgo, TicketReferences through epoch-timeago, both
reading Date.now(). A capture read "1 month ago" in one build and
"3 months ago" a few weeks later, and qlip reported frames nobody had
changed.

.storybook/preview.tsx now starts every story at STORY_NOW (30 June 2026,
12:00 UTC, src/stories/storyClock.ts) before it renders, and puts the
clock back after it. Components still read the platform's Date; only a
story changes it.

- The clock runs on in real time from STORY_NOW. A stopped clock stops
  lodash's debounce and throttle (ControlGroup's resize, Slate's
  selection sync), so it is not the default.
- parameters.mockdate stops the clock at an instant, as it did for the
  Log stories, and a live story keeps the real clock.
- When a story ends, the preview drops the ReqraftQueryClient answers
  already stale for the request that fetched them. The cache stamps each
  answer with the time it was fetched, and the clock takes the next story
  back to STORY_NOW, so the answer read as fetched in the future and was
  served instead of the story's own mocks (the useStorage stories failed,
  and useFetch's 409 now and then). Answers still fresh stay, as on a real
  clock: emptying the cache instead fails four FormEngine stories that
  only pass after an earlier story has fetched the type catalogue. A
  request still in flight is left to finish; removing it cancelled the
  Log stories' current-user request and failed Log/Filterable.
- This replaces @netsells/storybook-mockdate. Its decorator put the real
  Date back on every render and drew a "Mocking date" label in the
  capture host's time zone, as a keyless list (a React warning in every
  Log story).
- The ticket stories date their data with storyMinutesAgo();
  TicketConversation had literal July 2026 dates.
- The capture browser runs in UTC and US English, as CI does, in the
  desktop and the phone projects alike, so dates written in the viewer's
  zone or language (log timestamps, the StructuredDataView timestamp, date
  fields) capture the same on every machine.

storyTimesIgnoreTheRealClock renders every TicketThread,
TicketConversation and TicketReferences story through the project's
annotations with the machine clock five years apart; storyClock tests
the clock itself, and storyQueryCache that the next story is served
what a real clock would serve it.
The Log put its filter in the panel's title bar as a responsive action,
and a responsive action that does not fit is folded into the bar's
overflow menu. On a phone the filter, a text box, was the one that did
not fit: it was only reachable behind an unlabelled menu button, and
Log/Filterable's play could not find it at 390 px.

The Log's title-bar controls (the filter, Pause, Clear, Scroll to
bottom) no longer fold: the panel gives way on the title instead, and
the filter narrows (162 px at 390, 92 px at 320). The desktop bar is
unchanged. The message actions (copy, delete) and the send box are not
title-bar actions and were already in view at 390 px.

Log/Filterable On A Phone runs on the phone (touch project, drawn at
390 px from the start): the filter, Pause and Clear sit inside the
title bar, a finger taps the filter, and "error" leaves the ERROR line.
It fails on the old Log, which has no filter in the bar at that width.
The query cache lives as long as the page, so a story was served what an
earlier story had fetched, and could pass only because of it: on develop,
four FormEngine plays (Compact Row Cancel Edit and its Affordance,
Picking A Rich Text Choice Shows Its Name, Sticky Headers On A Phone)
failed when the story was opened on its own, cold. This branch already
waits for the form in each of them, and all four now pass opened
directly in Storybook, at 1440 and 390 px.

With the story clock the leak is also wrong in time: an answer carries
the time it was fetched, the clock takes every story back to STORY_NOW,
and the previous story's answer reads as fetched in the future. Dropping
only the stale answers when a story ended kept the leak; emptying the
cache before every story, as the unit tests' setup does, ends it. The
cache is emptied before anything mounts, so a request the previous
story left in flight is cancelled with nothing waiting for it.

storyQueryCache: a request that never caches gets this story's answer,
a cached one is fetched again, and the cache starts empty. The last two
fail on the previous "drop the stale answers" preview.
Qore batch 6 (28a6bbcb14) gives DPQL's diagnostics and completion details
in plain words that say what to do, and the stories' stand-in server
still said the old ones: "Incomplete template reference: expected
context identifier after '$'", "Unexpected token ')' after expression",
"requires argument", a field's detail as its type code.

The stand-in now gives what the installed DataProvider gives for the
same text: its words, its codes and its ranges, each checked with
parseDpqlExpressionWithInfo:
- a lone `$` (E030), a `$` before something that cannot follow it
  (E031, which names that character once, as the server does), a
  template with no `:` (E032, new here: it said E031 of the first
  letter), one with nothing after its `:` (E033, `$local: ` too), a lone
  `@` or one before something that cannot follow it (E003), and a
  character that cannot be used (E001);
- a value missing at the end, said where the token before it starts, or
  before a token; a `(` left open, in a call or a group; and what
  follows a whole value, by what it is (`)`, `,`, and, or, not);
- completion details: "needs a value after ':'" for a template context,
  and a field's type as DataTypeCatalogue names it ("Whole number").

The tests and the three story plays that asserted the old words assert
the new ones, and the DpqlEditor stories' own server answers its
completions as batch 6 does ("gives text"). reqraft's code reads none of
these words; a placeholder is still found by its range.
…clicked, wrapped or not (qorus#646)

Dpqleditor · A Click On A Chip Opens Its List And A Second Closes It
checked that the list had moved to the second chip by putting it right
of the first chip's right edge: true on a wide line, where the two
chips sit side by side. At 390 px they wrap onto two lines, the second
at the start of the next one, and the list opened correctly under it
failed the check in Storybook's UI.

The play now checks the list against where the chip it clicked is
drawn: under it, from its left edge. On a wide line that is past the
first chip; on a wrapped one it is below the first chip's line. A list
still at the first chip fails either way. The story passes at 1440 and
390 px, in the story run and in Storybook's UI, and its frame is
unchanged.
…rus#646)

getQorusTypes() gave reqraft's built-in copy of the type catalogue
always, and the checks built on it never saw the server's answer to
GET /system/qorus-type-info: areQorusTypesCompatible() read
`getQorusTypes() || fallbackTypes`, and the first was never empty. A type
or a conversion only the server knows was invisible to the expression
builder's return-type check and argument check, and to the templates
TemplateField offers a field (FormEngine fields included).

- areQorusTypesCompatible(), argumentMatchesType() and
  filterTemplatesByType() decide with the catalogue they are given,
  getQorusTypes() by default;
- the expression builder, its argument labels and TemplateField give
  them useQorusTypes().value: the server's answer once it has come, the
  built-in copy until then, and they draw again when it comes;
- getQorusTypes() gives the server's answer once a useQorusTypes has it,
  for code outside a component; stories and unit tests start without one.

serverTypeCatalogueDecides: a type only the server lists (an order
reference that takes text) fits, keeps its field's text templates, and
is seen by getQorusTypes() after the hook's answer. All four fail on the
previous code.
…lt-in catalogue, not offered (qorus#646)

Qorus gives a field or an option of a DataProvider string format type,
or of a file, that type as its UI type (qorus 974643455, approved by
David). reqraft's built-in copy of the type catalogue had no entry for
them, so before the server answered a form could not name such a field,
map a value into it or offer it a template.

The copy is the server's catalogue of 974643455 again:
- email, uri, uuid, hostname, ipv4, ipv6, phone and file, with the
  DataProvider module's names and descriptions; a string format takes
  and exactly matches itself, string and richtext, a file itself and a
  hash; the server lists them after the offered types, and so does the
  copy;
- "Text or binary data" takes the string formats, and a hash takes a
  file, as on the server;
- every entry has `offered`: true for the types before, false for these.

offeredQorusTypes() (and useQorusTypes().getOfferedTypes()) gives what
a picker that lists every type offers: the offered types, and the type a
value already has. No reqraft picker lists every type; its pickers keep
the compact list.

The fixture is that commit's QorusDataTypeCatalogue::Info as the
endpoint answers it in English, with Qore 28a6bbcb14's DataProvider: no
running server has it yet. The recording script requires `offered`. The
catalogue tests fail on the previous copy (6 of 8).
…is never named by its code (qorus#646)

A type the type catalogue lists under no name - a user type, a type of a
newer server - was named by its code ("qoretechnologies/order",
"order-status"), said as "a value of type order-status", and fitted
nothing, so its field was offered no template and its argument always
looked wrong.

Qorus gives every field, option and argument its base data type
(`base_type`). qorusTypeEntry() now resolves a type by its own name, an
optional, soft or complex form of one (as before), and else by its base
type, read the same way. Where neither is listed, a user type (a path)
is named by its path and anything else is "Unknown type" / "a value of
an unknown type"; no name is the raw code any more.

- the display name, sentence form and known name take the base type,
  and so do the hook's getTypeDisplayName(), getTypeSentenceForm(),
  getAcceptedTypes() and getExactMatches(), which now also read the
  optional, soft and complex forms;
- areQorusTypesCompatible() resolves a place's type the same way, with
  its base type, and a value's type by the name the catalogue lists it
  under; argumentMatchesType() uses the argument's base_type;
- TemplateField filters a field's templates by its base_type too, an
  argument label names the place by its base_type, an option's words
  ("Takes key-value pairs (a hash)") use the option's, and the
  unsupported-type dialog names a type as the catalogue does.

typeBaseFallback: 6 of its 7 cases fail on the previous code; three
tests that expected a raw name now expect "Unknown type".
…ypeEntry among them (qorus#646)

The Qorus IDE keeps its own copy of how a type is found in the type
catalogue, which drifts from reqraft's. The package root now exports
qorusTypeEntry() (a type by its name, an optional, soft or complex form
of one, or its base type), offeredQorusTypes(), the names
(qorusTypeDisplayName, qorusTypeSentenceForm, UNKNOWN_TYPE_NAME), the
built-in catalogue and getQorusTypes(), and useQorusTypes with its
types; the hook's module re-exports the new ones too.

typeCatalogueExports fails before the export.
…qorus#646)

A form draws a type's name with its own words (TypeSentenceFormProvider),
which handed the type on without its base type: an option of a user type
the catalogue does not list showed no type at all in its help and beside
its name, although the type's base type is listed. The provider now
passes the base type on, as the built-in words do.

Two FormEngine stories, at desktop width and on a phone, show a field of
a user type with its base type (named Text) and one of a type with
neither (named by nothing, never by its code).

typeBaseFallback's new case fails before the fix (undefined instead of
"Key-value pairs (hash)"), and so do both stories' plays.
…, and an unlisted type, are offered (qorus#646)

Four TemplateField stories, at desktop width and on a phone, open the
list of templates of a field:

- of an order reference, a type the server's catalogue lists and the
  built-in copy does not: the text templates, by the server's answer;
- of qoretechnologies/order-id, a user type no catalogue lists, whose
  base type is a whole number: the whole-number templates.

Both lists offered nothing before d61681e and 11a773a: run against
7a22834's sources, all four plays fail (one row instead of the
templates).
…e (qorus#646)

The auto field draws an editor for the types it knows and "Unknown
type!" for any other: a field of a string format type such as an email
address, of a user type or of a type a newer server adds could not be
set in a form, although Qorus gives every field the base data type of
its values (base_type: string for an email address). Such a field is now
edited as its base type, as the type catalogue names it by its base type
(qorusTypeEntry). Only a type with no base type that has an editor is
unknown. base_type is read by the editor choice alone and no longer
lands on the editors' DOM.

Two FormEngine stories, at desktop width and on a phone, open a field of
type email (named Email address) and one of a user type on a text field
holding its value.

autoFieldEditsAsBaseType fails before the fix in its three base-type
cases ("Unknown type!" drawn), and so do both stories' plays (no text
field).
… as its base type (qorus#646)

Qorus names the base type of every template's values (base_type, Qorus
a1e08a293), as it does a field's, so that a client can fit a template of
a type it does not know. reqraft dropped it: buildTemplates kept only the
type, and the type filter found no entry for a user type, so a template
of qoretechnologies/money was offered to no field but an untyped one.

buildTemplates keeps the base type in the item's metadata (baseType),
filterTemplatesByType hands it to areQorusTypesCompatible, which takes
the base type of the value's type as well as the place's
(checkBaseType); a type the catalogue lists is still that type.

Two TemplateField stories, at desktop width and on a phone, open the
list of a number field: it offers the money template, by its base type.

typeBaseFallback's new case fails before the change (no template
offered), and so do both stories' plays.
…r its code (qorus#646)

A template kept its type only as its list row's badge (buildTemplates:
badge: type), which Reqore draws as it is: every list of templates said
"int", "string" or "qoretechnologies/money" beside a template, and the
badge could not be named, because the field read it back as the
template's type (the value's type when a template is chosen, a lone
template's type, the type filter, a chip's hover).

The type is now a typed member of the template: buildTemplates gives
metadata.type, beside metadata.baseType; templateType() reads it, or the
badge of a row a host builds itself, and every reader of a template's
type uses it. The lists name the type in the badge they draw
(styleTemplateItems: the browse list and the text editor's list) and in
the detail of the list a $ opens (templateCompletionItems, in a text
field and in the DPQL editor), by the catalogue the surrounding form has
(useTypeLabel; the built-in one elsewhere): qorusTypeDisplayName, so a
user type with no base type is named by its path and any other unknown
type is "Unknown type". The package exports templateType and
templateBaseType.

Two TemplateField stories, at desktop width and on a phone, open a list
of templates of every kind of type. templateTypeBadge fails before the
change in 7 of its 9 cases (the other two guard what the field reads
from a named list), and so do both stories' plays ("Count: expected
'CountintExample value: 3' to contain 'Whole number'"). templateSources
and dpqlCompletions now expect the names.
…ged by the place's base type (qorus#646)

The expression builder judged what an expression gives against its
place's type alone: a field of a user type whose values are text
(qoretechnologies/order-ref, base_type string) refused an expression
that gives text ("This expression gives text, which this field does not
take"), and an argument of such a type took nothing, its accepted types
being looked up without the argument's base type.

The builder takes the place's base type (returnBaseType, which
TemplateField passes from the field's base_type through ExpressionField
where the place is the field's own type) and hands it to
areQorusTypesCompatible and to the place's name in the mismatch; an
argument's accepted types are looked up with its base_type.

Two TemplateField stories, at desktop width and on a phone, show a text
field and a date field of user types holding concat(...): the first
takes it, the second says it takes a date and time.

expressionPlaceBaseType fails before the change in its two "fits" cases
(the mismatch drawn); the stories' plays fail on the same assertion.
…named as their base type (qorus#646)

Qorus gives a list field the base data type of its elements
(element_base_type: string for a list of email addresses). The auto
field handed its elements their type alone, so each element of a list of
a user type, or of a string format type, showed "Unknown type!", and the
list was named "List" with no word for what it holds.

The auto field hands each element's field the list's element_base_type
as its base_type (a78372e edits it as that), and optionTypeWords names
the elements by it: List (Text) for a list of
qoretechnologies/order-ref.

Two FormEngine stories, at desktop width and on a phone, show a list of
email addresses and a list of a user type: named List (Email address)
and List (Text), each element on a text field holding its value.

autoFieldEditsAsBaseType's list case and typeBaseFallback's list words
fail before the change ("Unknown type!" drawn; "List" for "List
(Text)"). The test's fields now have a fetch context, which a list's
template fields need.
…no type (qorus#646)

1bc4478 read a row's badge as the template's type wherever the row had
no typed member, and a group of templates is badged with the app it
comes from: the list of a yes/no field, where no template of Context
Data fits and the group is drawn with no members, named the group
"Unknown type" instead of "Qorus" (TemplateField · Shows Templates List
For Boolean, in the r16 story run).

templateType() reads the badge only of a row that is a template, one
with a value; a group has no type, and keeps its badge.

templateTypeBadge's new case fails before the fix ("Qorus" read as a
type), and so does Shows Templates List For Boolean's play, which now
checks the group's badge.
…dering (qorus#646)

The Preview kept the last rendering it had until the next one arrived,
and its state compared that rendering with the text now showing, not
with the expression it was made from: after an edit, for the pause
before the rendering is asked for and the server's answer, the previous
expression's rendering was drawn under the new text. r15's capture of
FormEngine · The Text Lists What Can Be Written At The Caret On A Phone
caught "record: pos" under `bezeichnung == bezeichnung`
(ExpressionField.tsx, the Preview effect and previewState).

A rendering is now kept with the expression it renders, by content, and
drawn only for that expression: until the current one's arrives the
field draws nothing (data-preview="pending"). An answer for any other
expression is dropped by the key of the one asked for last, however late
or out of order it arrives.

The caret-list play waits for the field to hold its last expression and
have its rendering (data-preview="repeats") before it opens the list it
leaves for the capture, so the frame no longer depends on timing: no
Preview, and the fit message for the expression shown.

expressionPreviewIsCurrent drives a stand-in server that answers when
and in the order the test says, with fake timers: both cases fail before
the fix (the previous rendering drawn under the new text).
@davidnich

Copy link
Copy Markdown
Member Author

Round 16 (7a22834..fb70c1c), stories approved:

  • the type checks use the server's type catalogue (getQorusTypes() returned only the built-in list), and the catalogue has the string-format types (email, uri, uuid, hostname, ipv4, ipv6, phone) and file, with offered (pickers list only offered types);
  • a type the catalogue does not list is resolved and fitted through its base_type / element_base_type, and named in words ("Unknown type" or the user type's path), never by its code; fields of such types get an editor;
  • templates carry their type as metadata.type / metadata.baseType (read through templateType()), so a template's badge is named and templates fit by base type; the expression builder judges by the place's base type (returnBaseType);
  • the type lookups (qorusTypeEntry and others) are exported;
  • the expression Preview never shows the previous expression's rendering: answers for older text are discarded and the field reports pending until the current answer arrives.

Each change has a fail-first test; precheck clean (3087 unit tests); 806 story tests pass, desktop and touch.

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.

1 participant