Skip to content

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

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

davidnich wants to merge 91 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.

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.

Version 0.10.62.

🤖 Generated with Claude Code

… number that takes templates is shown as it is used — 0.10.61

David's review of qorus#646:
- An open option card lined its name up with the top of its actions, so
  "When" sat above the buttons beside it. A heading of one line is centred
  on the actions; one with a description or tags under its name still
  lines up by its top. Injected Option Actions Edit Card checks it.
- Number > Typed Freely typed a template into the bare number editor,
  which takes none, and read as a field that should offer templates and
  did not. It is the bare editor (12abc kept as typed, a decimal point
  typed before its digits kept), and says where a number that takes
  templates is. Template > Number Typed Freely is that field: the first
  click lists the templates, what is typed is kept as typed, and a chosen
  template is the value, named in the template picker.
@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.62-pr.137.gf3f5d3f

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

yarn add @qoretechnologies/reqraft@0.10.62-pr.137.gf3f5d3f

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

🔍 32 changed, 68 new of 698 snapshots.
⏳ Awaiting review.

Review build 20261012-195719 in the dashboard →

…ame; a wrong one says which, by its name

David's review of qorus#646: a row rule built in the Visual view said
"Value for argument 1 ("Value") is invalid: Missing value" twice while it
was still being built. An empty operand now says "Enter a value for
"Value"", and a wrong one ""Value" is invalid: ..." - by the name the
editor shows, not its position in the call.
…abandoned request is not a failure) ships in 0.10.61
@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
…hen it is closed

An open field is held in the box it was opened in, so finishing an edit
does not remount it in another box and take the focus. Held, it said
nothing: a value already right sat under "Needs attention" until ✓ was
pressed, with no reason given (David's review of qorus#646). It now says
where it goes - 'Moves to "Set" when you close it (✓)' - under its editor
while it is held, and nothing once it is closed or where it belongs.

Compact Row Says Where It Moves fills a required field in Needs
attention: the note appears, the editor stays open, and it goes once the
field is closed.
@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.

…ot hashes, and custom Text values as the templates they are

From 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,
`[object Object] > [object Object]`.

The Text view handed `dpql/serialize` the AST as the Visual view stores
it, and two operand shapes are not DPQL: an operand whose operation is not
chosen yet, or that is not filled in, and a custom Text value, which is a
rich-text document. The server writes them as hashes
(`{args=(null)} > {type=int}`), and a stand-in as `[object Object]`.

- An expression with a part not filled in is not serialized: the Text view
  shows a hole for each such part, `… > …`, as the row summary does.
- A rich-text operand is written as what it holds: a lone template as that
  template (a field becomes `@field`), words as their string.

Tests: expressionTextOfIncomplete (the two Text view cases fail before
the fix); story ExpressionField · Text Mode Of An Unfinished Expression.
…by the Text view and the read-first summary, and exported

The Text view was not the only writer that read operands as stored: the
read-first summary (renderExpressionToText) printed a custom Text value as
its rich-text document and an unfilled operand as null, and an
application's own summary (qorus-ide's row rule line) printed
`[object Object] > …` for the same rule.

expressionOperands reads an operand as what it holds - a rich-text value as
its template or words, an operand not filled in as nothing yet - and is the
one place that knows the shapes the Visual view stores. The Text view and
renderExpressionToText use it (a part not filled in is written `…`), and it
is exported (expressionOperand, expressionWithOperands,
isExpressionComplete, EXPRESSION_HOLE) so an application's summaries need
no copy of their own.
…hat is filled in, with its operands as they hold

CI caught it: the Text view of an unfinished expression also asked
dpql/renderExpression for its Preview, with the AST as stored, and the
stories' stand-in server threw on the operand with no operation
(Cannot read properties of null). The real server writes such operands as
hashes. renderRich asks only for a complete expression, read through
expressionOperands, as the Text view's serialize does.
@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].

…n template is a chip, with text around it; the value's type is read from the text

From David's review of qorus#646: an operand of type any opened on a
pick-only "Select Template Or Field" control, and a template chosen into a
whole-number, number or yes/no field replaced it with a control that held
the template and nothing else, so no text could be written around it.

- A field that takes templates and custom values is one text field for
  text, untyped (any/auto) and scalar (int, number, bool) types: a chosen
  template is a chip in it, named as the catalogue names it (braced
  references too - the picker exception for them predates the chip
  editor).
- What the field holds is read from the text (writtenAsText):
  - a template standing alone is that template, and 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 scalar's literal (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).
- A date keeps its date control, with the templates offered beside it
  ($): chosen, the template is the value in the template control, whose x
  returns to the date. Choosing it no longer hands the reference to the
  date picker first, which threw on it.
- Fixed choices (allowed values) keep their list.
- An expression's untyped operands are written as text from the start.
- The text field shrinks in a narrow operand row (no 150px minimum there).

Tests: writtenAsText (the whole-number field and the date ones fail
before), expressionNullSurvivesTemplates updated to the decided rule.
Stories: Template - Written As Text - Template Alone, Text Around The
Template, Number With A Template, 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.
… text field, the cursor in it

Deleting the template chip in a whole-number, number or yes/no field
emptied its text, and an empty text switched the field to its own control
(a number input, a checkbox): a different editor took the text field's
place, the cursor was lost, and what was typed next went nowhere. Found in
qorus#646's fill value of a whole-number column: the template deleted and
0 written instead left the value empty.

An emptied field written as text now stays the text field, as an untyped
one already did; its own control is one menu item away ("Use Custom
Value").

Stories: Template - Written As Text Number Emptied Keeps Its Field (new;
fails before), Template Value Can Be Removed (now the rule above, and the
checkbox from the menu), Written As Text Number Picks A Template (new).
…e declares for it

A row rule's condition in qorus#646 (the builder's type bool) with ">"
chosen showed its first value as "Unknown type!" and only its second as
the text field an untyped value is now written in. An empty first operand
took the builder's own type - what the whole condition gives (bool), or
`context` in an untyped builder - while every other operand falls back to
the type the catalogue declares for it. Once an untyped operand took
custom values, that wrong type picked a custom editor that does not
exist.

An empty untyped first operand is now of its declared type, like the
rest.

Test: expressionUntypedOperandsWrittenAsText (both cases fail before).
…h room, and narrower only where its row is

The previous commit let the text field a template is written in shrink
below a free text field's 150px, so that an operand in a narrow column
keeps its clear button and its menu inside its row. It did so by removing
the minimum, which made the field as narrow as what it held in every row:
a "contains" operand holding $local:input went from 150px to 132px, and
qlip showed the same in a dozen expression stories.

The field now has a preferred width of 150px that grows with what it
holds and shrinks only where its row has less room, as a number's input
does.

Story: ExpressionBuilder - With Simple Value checks that each text value
is 150px wide (fails before: 132px); Operands Fit Narrow Column and Nested
Fits Narrow Column still pass.
…ses one

qlip showed the story's choices as two radio buttons with no names: it
rendered without a component and gave its allowed values no display
names, and its values were not in the shape a form's allowed values
carry ({type, value}), so choosing one held nothing. It only checked that
no text field was shown, so it passed anyway.

It now renders through auto, as a form does, with Open and Closed named,
and checks that both are shown and that choosing Closed holds closed.
@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.

…und a template is not valid, and says why

From David's decision on qorus#646: "$record:{bestellnummer} Stk." in a
whole-number field is not a whole number, and the form must say so while
it is typed and not be saved until it is fixed.

It was valid. Validation took anything starting with `$` and holding a `:`
for a template, so text around a template passed as one in every typed
field. Text with a template in it that is not the template alone now makes
a value of these types invalid, with the reason under the field:

- int: "A whole number can't have text around a template"
- number: "A number can't have text around a template"
- bool: "True or false can't have text around a template"
- date: "A date can't have text around a template"

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

Tests: textAroundATemplate (fails 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.
From David's decision on qorus#646: null and no value must be told apart
by the author and stay distinct in what is sent.

An untyped field holding null fell to the generic custom editor, which
drew it as an empty box: the same as a field holding nothing. It is now
written in the field's text field as `null`, as an untyped field's other
values are, and `null` written in an untyped field is null. Deleting it
leaves the field empty, which is no value at all (undefined), not null.
Text with null in it, and null in a text field, are text.

The written-as-text stories' harness now says "(no value)" for an emptied
field rather than printing it as null.

Tests: writtenAsText - null in an untyped field (fails before). Story:
Template - Written As Text Null Stays A Value shows null, is emptied and
written null again (fails before).
@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.

…, not from the list the field offers

From David's review of qorus#646: in a yes/no field, the chip of the
whole-number field pos read "record: pos" where every other chip reads
"pos".

The text field named its chips from the templates it offers, and a typed
field offers only the fields of its type. A field not on that list - pos
in a yes/no field, any field around which text is written - had no name
to find, and its chip fell back to the reference's path. It showed in
every typed field, not only yes/no: the other stories only ever chose a
field of the field's own type.

The rich-text field takes the catalogue chips are named from
(`namingTemplates`, defaulting to `templates`); TemplateField passes the
whole catalogue there and keeps offering the filtered list.

Tests: writtenAsText - a chip of a field not of the value's type, in
bool, number and int fields (fail before). Story: Template - Written As
Text Chip Of Another Type (fails before).
…place of the field's own message

From David's decision on qorus#646: where the sheet-contract review has
its own words for what is wrong with a value ("Enter a value or an
expression", "A whole number can't have text around a field", in the
user's language), the field said its generic reason as well ("This field
is required", "…around a template"): two messages for one problem, one of
them in English.

A field's schema can carry `invalid_reason`, the host's sentence. While
it is set:
- it is the field's message, in place of the field's own validation
  reason and its "This field is required" (required groups and
  dependencies still say theirs);
- the field is invalid, whatever it makes of its value - the host knows
  things the field cannot (a field named that is not in the record) - so
  the form's validity and "Needs attention" agree with what is said.

Tests: hostInvalidReason (messages and FormEngine validity; fail before).
Stories: FormEngine - Host Reason In Place Of The Field's Own, Host
Reason In Place Of Required (fail before).
…ot as Yes

Found verifying the chip fix in qorus#646: an added yes/no field holding
the field pos read "Yes" in its collapsed row.

The collapsed row's yes/no branch came before its template branch, and
the summary called any value that is not empty "Yes". A template is not
true; it is the field it names, and reads as that field's chip, as it
does in every other type. Only a value that is true or false reads as Yes
or No.

Tests: readRowTemplateInTypedField (the summary and the collapsed row;
fail before). Story: FormEngine - Yes No Holding A Template Read (fails
before).
@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".

…shows empty

From David's review of qorus#646: "Clear value" on an added yes/no holding
the field pos had no effect. The value was cleared; the field went on
showing pos.

The text field a value is written in keeps its own copy of the text
(templateValue), set by the editor and by a template arriving from
outside. Nothing set it when the value was EMPTIED from outside - the
card's "Clear value", an undo, a host's reset - so the field kept showing
what it had held. An outside empty value now empties the text; the
editor's own emptying already did.

Tests: writtenAsText - a value written as text, cleared from outside its
editor (bool, int, string; fail before). Story: Template - Written As Text
Cleared By Its Host (fails before).
@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).

… own control, one action opens the expression Text view

From David's decision on qorus#646 (with both amendments of the
analysis):

- Typed options open on their own control: a number input for a whole
  number or a number, a checkbox for a yes/no, the date picker, the
  choices for fixed values. This reverses typed values defaulting to a
  text field with chips; text and untyped values keep it.
- One field-menu action, "Use Template / Expression", replaces "Use
  Template" and "Use Expression" where the field takes expressions
  written as text: it opens the expression editor's Text view, seeded
  with what the field holds (a template, a number, a yes/no), the Visual
  view one click away and starting from that value as its first operand.
  Nothing is emitted until the text is edited. A template-only option
  (no expressions) keeps "Use Template", the picker and the bare template.
- The Text view offers the field's templates as its catalogue names them
  ($, inserted as chips), next to the server's own `$` completion.
- A saved template in a typed option that takes expressions opens in the
  Text view, as the template; nothing is rewritten. Saved expressions
  open in the Text view too.
- A lone template or a lone value written in the Text view is stored as
  itself - the bare template, the literal - not as an expression
  wrapping it: the server parses a lone template into the `template`
  operation, whose UI form it does not read back as the template's value,
  and a `value(...)` expression is one the Visual view cannot show.
- Without a language server (connecting fails) the Text view falls back
  to the Visual view and says why (DpqlEditor `onUnavailable`, from the
  LSP session's new `isUnavailable`).
- The date's own template button goes: the action covers it.

Kept: chip naming from the whole catalogue, null shown as null, the
cleared-from-outside fix, the host's own reason, the read-row fix and the
text-around guard (for saved values).

The stories' mock language server wraps a lone template in `template(...)`
as the server's parser does.

Tests: templateOrExpression (the action, the Text view seed, the saved
template, a lone template and a lone value stored as themselves, the
text kept while held as a value, the template picker, the fallback
without a language server; all fail before), writtenAsText retargeted to
the decided rule. Stories: FormEngine - Typed Options Open On Their Own
Control, Use Template Or Expression Opens The Text View (+ phone), Saved
Template Opens In The Text View (+ phone), Saved Text With Templates
Stays Text, Saved Expression Opens In The Text View, Template Only Option
Keeps The Picker, Lone Template Written Is Stored Bare; the Template and
ExpressionField stories that asserted the reversed rules follow the
decided ones.
@davidnich

Copy link
Copy Markdown
Member Author

Two more fixes, found verifying the Qog options against hq: c435ea1, 1a6de5f. Nothing accepted.

  • c435ea1: a row summary writes + and * of more than two operands infix, as the server does. Details are in section 28. On hq, with Qore's n-ary serializer fix, Send Discord Message › Message Content (one + of three) read "…" + format_number(…) + "…" in the Text view, but +("…", format_number(…), "…") in its collapsed row.
  • 1a6de5f: a check group's choices each have their own key. Details are in section 29. React warned about missing keys in qorus-ide's Qog processor stories.

CI: run #487 passed on its re-run. Its first attempt failed Template › Written As Text Null Stays A Value with expected { value: 'nll' } to deeply equal { value: null }: one typed key, u, was lost under the parallel story load. This is the reqore rich-text editor key loss explained in the description ("Earlier failed CI run"), fixed in qoretechnologies/reqore#708. This PR does not touch that story or that editor, and the story passes locally and on the re-run. Publish #251 published 0.10.62-pr.137.g1a6de5f, and the Discord message named exactly that version.

qlip: 20261010-143734 is the same as 20261010-133232: 627 unchanged, the same 5 new, the same 2 pending since the caret change, and 1 carried-forward rejection. No story shows a collapsed + of three, so c435ea1 changes no snapshot.

…ch React 19 renders as designed (qorus#646)

react-color's Sketch picker is a function component that sets its
defaults (its width, its preset colours, whether it shows transparency)
with defaultProps, which React 19 no longer applies to function
components; React 18.3 already warns about it (for Sketch and
Checkboard) on every page that shows an rgbcolor field.

The field now uses @uiw/react-color-sketch 2.10.3 and still stores
{ r, g, b, a }:
- a colour the picker sent is handed back to it as it sent it, so a
  grey keeps the hue it was chosen from (the hue slider no longer jumps
  to red), and a hue moved on a grey stores nothing
- a value stored as hex (bare or { hex }) is shown, as readFirst reads it
- without its slider the transparency is kept, not reset by a hex or a
  swatch
- disabled and read-only now take effect: the picker is inert (pointer,
  keyboard and assistive technology), and disabled is dimmed; Field and
  AutoFormField pass both instead of spreading every field prop into
  the picker
- theme text colours, flush with the field, the compact 100px colour
  area; on a coarse pointer the hue bar is 24px and the swatches and
  inputs 44px, with 16px input text
- @uiw/color-convert and @babel/runtime (a peer of the @uiw packages)
  are declared; react-color and @types/react-color are gone

Tests: __tests__/colorFormField.test.tsx (the defaultProps warning test
fails on react-color), stories With Value, Red, Empty, Written As Hex,
Chosen From A Swatch, With Transparency, Disabled, Read Only, On A Light
Theme and On A Touch Screen (390px, coarse pointer, a tapped swatch).
…what it means, in the Visual and the Text view (qorus#646)

David's verdict on a condition that does not give true or false, as
qorus-ide's condition editors say it: the Qorus server reads a
condition - a Qog transition's, an if state's, a while or for block's -
as true unless its value is zero or empty (Fsm.qc calls .toBool()), so:

- true or false, or a type it cannot tell (any, auto): nothing
- text, a date or a time: refused, "A condition can't give Text:
  compare it explicitly, for example @status == "open" or @note != """
- any other known type (a number, a list, a hash): accepted, with a
  warning: "This condition is true whenever its value isn't zero or
  empty. To say what you mean, compare it explicitly."

A condition is a field the server marks as one: its schema's
metadata.condition (the Qog transition's value, the built-in if's value,
the while and for blocks' condition; qorus "feat(qog): the options
the runtime reads as conditions say so in their schema",
FsmConditionOptions). A value of type bool is not one:
the server checks it as a bool (applyTargetTypeConstraint) and an int
does not fit it, so it keeps its type check - which d7f3fc3 had relaxed
for every bool field.

- TemplateField reads the mark (isConditionOption) and hands
  ExpressionField and the builder `condition`
- Text view: a condition is asked as one - dpql/parse with
  `condition: true` and no target type (qorus "fix(dpql): a condition
  is judged as true or false, and a lone field gives its own type") -
  and the
  answer's condition_reading is shown, in the server's own words
  (NOT_A_CONDITION / CONDITION_BY_TRUTHINESS diagnostics, in its
  presentation language); a server that does not know the question
  answers with the inferred type, which is read the same way
- Visual view: under the condition itself, the warning or the refusal;
  the operations offered narrow to those that decide a condition only
  for a condition; each part of an AND or an OR keeps d7f3fc3's reading
- the parse API takes { condition } (IDpqlParseOptions), the result
  carries condition_reading; the mock language server answers the
  question as the server does
- conditionVerdict, conditionErrorText, CONDITION_BY_TRUTHINESS_TEXT and
  isConditionOption in helpers/expressions; isConditionType takes
  softbool too

Tests: __tests__/expressions.test.ts (the verdict),
expressionFieldTypeFitMessage (the question asked, the server's words,
the reading of an older server, true or false, a bool value's type
check; the condition cases fail on the old code), and
expressionFieldSeededTypeCheck. Stories: ExpressionBuilder > Condition
That Gives A Number (and On A Phone), Condition That Gives Text, Bool
Value That Gives A Number; ExpressionField > Text Mode Condition That
Gives A Number (and On A Phone), Text, True Or False, Bool Value That
Gives A Number.
…rver's errors, as the Text view does - a hash plus a number, say (qorus#646)

David's decision: `@create + 1` (a hash plus a number) keeps Qore's
run-time result but is an error in the editor. Qore's parser refuses it
(DPQL-E050), and professor's language server (Qore 8417bde52) says so
for the text with severity 1, in its words: "Operator '+' cannot be
applied to '@create' (a hash) and a number: it adds numbers, or a
number and a date; ...".

- Text view: already shown as an error - the language server publishes
  it, and the editor draws it under the text at the danger intent and as
  a red underline. Each diagnostic message now says what it is
  (`data-severity`: danger, warning or info).
- Visual view: said nothing, as no check on this side can tell what +
  combines. It now asks the server about the expression it builds -
  dpql/serialize, then dpql/parse, on a document bound to the record's
  fields as the Text view's is - and shows the server's errors under
  the builder (.expression-server-errors), at the danger intent. Only
  while it shows a whole expression and has the record's fields or a
  provider to type its operands by, so a form of untyped expressions
  opens no document for it.
- The stories' language server answers as professor's does: it reads
  `@field` (as `$record:{field}`), refuses a hash and a number with
  DPQL-E050, and publishes it on every open and change.

Stories (Components/Form/Expressions/ExpressionField): Text Mode
Operands The Operator Cannot Combine; Visual Operands The Operator
Cannot Combine (and On A Phone), which fail without the change; Visual
Operands That Combine guards that a sound expression shows nothing.
Tests: __tests__/dpqlMockLanguage.test.ts.
…says what the server says of it (qorus#646)

David's decision (A): each part of && || ! is read as true or false by its value, the same everywhere in Qorus,
and the server judges each part by the rule it judges a whole condition by. The server side is "fix(dpql): each
part of && || ! is read as a condition, and judged as one (#646)": && || ! take any value, and dpql/parse with
condition: true answers `parts` ({type, reading}, in its walk order) with a NOT_A_CONDITION error or a
CONDITION_BY_TRUTHINESS warning for each part that is not true or false.

- The Visual view asks the server about a condition it builds as a condition (`condition: true`), with or without
  the record's fields, and draws each part's verdict on that part's card, in the server's words: a warning for a
  number, an error for text. `conditionPartCards` places the parts by the server's walk (a nested AND or OR stands
  for its own parts; the part a NOT reads goes on the NOT's card), and places nothing when the answer is not about
  the expression on screen. `conditionPartsWithMessages` gives each part what the server said of it.
- No part is checked as a bool here: a part of an AND or an OR is typed by its operator's arguments in the
  catalogue (any value, as the server now declares them) instead of `bool`, and a part can still be grouped into
  another AND or OR.
- The error list under the Visual view leaves out the condition verdicts, which are on the cards.
- The mock language server judges the parts as the server does, its fields' types known; the mock catalogue's &&
  and || take any value, as the server's do.

Stories: ExpressionField VisualConditionWithANumberPart (and OnAPhone), VisualConditionWithATextPart and
VisualConditionWithPartsThatAreTrueOrFalse failed before the change (no part was read as a condition) and pass;
the whole story suite (658 stories) passes, and the unit tests (2098), lint and the type builds. Unit tests for the card placement, the
server's words and the mock's parts.
…nd does not block a save, and a Save that refuses says why on screen (qorus#646)

Found through the IDE's API schema stories: a schema opened for editing
held `author: []` in an optional select-array field. The form filed the
field under Optional - to its status an empty list is "not set" - while
validation checked `[]` as a value ("At least one value is required").
The form was invalid, Submit refused, and nothing on screen said why.

- One rule says what is set: `isOptionValueEmpty` (no value, null, empty
  text, an empty list) moves to helpers/validations.ts, and validation
  reads it as the read-first status does - the options check, and the
  form engine's field and form validity. An optional field that is not
  set is not validated; a required one still is, so an empty required
  list still blocks.
- The batched form's Save, while it refuses, shows why beside it as text
  (`describeBlockedSave`): the first field that blocks it by the name the
  form shows, what is wrong with it, and how many more. The reason used
  to ride only on the disabled button's tooltip, which a disabled button
  never raises and a phone cannot hover.

Stories: CompactBatchedCommitWithAnEmptyOptionalList (Save stayed
disabled) and CompactBatchedCommitInvalid / ...OnAPhone (no reason on
screen) failed before the change and pass; all 660 stories, the unit
tests (2105), lint and the type builds pass.

Qlip: Compact Batched Commit Invalid now shows the reason beside Save;
the two new stories are new snapshots.
…fuses what is not a colour and previews it (qorus#646)

The Sketch picker gave no explanation of hex, red, green, blue or alpha, its
bars and swatches were too small for a finger, its inputs accepted any text
and stored whatever came of it, and nothing showed the chosen colour on its
own.

The field is now built from the @uiw picker's parts (saturation square, hue
and opacity bars) plus its own:
- a preview of the chosen colour, with a checkerboard under it so that
  transparency shows; while no colour is set, the preview is struck through;
- labelled Hex code, Red, Green, Blue and Opacity % inputs. They accept only
  the characters a value can hold, keep values in range, say what was wrong
  under the inputs, and never store an invalid colour. A three-digit hex code
  is short for six, the arrow keys step a number, and Shift steps by ten;
- a "What do these mean?" button that explains the square, the hue bar, the
  hex code, the channels (0 to 255) and opacity (0% to 100%) in plain words.
  Nothing is shown only on hover;
- swatches that are buttons.

On a coarse pointer, every target is at least 44px, and the square, the hue
bar and the opacity bar can be dragged with a finger. The touch stories drive
them with real touch events: Chromium's touch emulation through CDP, at
phone size.

The value is still { r, g, b, a }. A grey keeps the hue it was chosen
from, disabled and read-only fields are inert, and with disableAlpha the
stored opacity is kept.
…n a tap, on every template chip and server description (qorus#646)

A template chip's hover printed its description raw (`**`, backticks)
beside a template list that draws the same string through the host's
markdown renderer. `templateTooltip` passed the description as plain
content, and so did ReadOnlyTemplateTag, TemplateText and RichText's text
chips. The same was true of the other hovers that show a server
description: a choice's (allowed-value checkboxes), a schema section's
(panels and tabs), and a DPQL editor's field and template references.

None of them could be opened by a finger. Reqore opens a hover card on
mouseenter and closes it on any click. A tap fires both, so the card closed
as soon as it opened.

Every description's card is now made by one helper, `descriptionTooltip`,
fed by `useDescriptionTooltips`:
- it draws the description with the host's renderer (compact), as the
  template list does. Without a host renderer the description stays as
  written, which is the existing contract: reqraft does not draw in a
  dialect the host never chose;
- where the pointer is coarse (`useCoarsePointer`), the card opens on a tap
  and closes on the next tap.
`templateTooltip` and the DPQL tag renderer take these options as an
argument, so no caller can leave out the renderer or the pointer.

`MarkdownRendererContext` is exported from the barrel. A host can then
supply its renderer to a chip or editor mounted outside a FormEngine, and
the IDE checklist names the seam.

Stories (ReadOnlyTemplateTag, TemplateText, RichText) hover with a mouse
and tap with a real finger at phone width. All six fail without this
change, and the stories without a host renderer pass both before and after.
… are dispatched too (qorus#646)

Storybook's own UI has no DevTools protocol, so its "Tapped On A Phone"
plays dispatch the touches on a desktop browser, where the pointer is not
coarse. The card stayed a hover card, the tap's click closed it, and the
play failed there. The colour field's touch stories avoid this with
`withCoarsePointer`, and the tap stories now use it too.

A unit test opens and closes the card with taps on a coarse pointer. It
fails with a mouse pointer, where a click closes a hover card.
…tory that loads an image from a server fails (qorus#646)

Every Qorus logo on a template chip or template-list row was an empty slot
in every capture. The template fixtures named the logo
https://hq.qoretechnologies.com:8092/api/public/apps/Qorus/qorus-logo.svg,
which the server never served: it answers 404 and names the path
`.../apps/Qorus/logo` (DataProviderAppsRestClass::doGetLogo). The request
went to the live server anyway, because the story network guard wrapped
only fetch, XHR, WebSocket and beacons, and an <img> loads its src by
itself. The Select stories loaded a GitHub avatar the same way.

- The fixtures keep the URL the server gives an app's logo, with its path
  corrected. `__fixtures__/templateFixtures.ts` swaps in a local copy of
  that logo (src/stories/Data/qorusAppLogo.svg), matched by the server's
  path on any instance, and the stories import the fixtures from there.
  The Select stories' items use the same local image.
- The guard also covers what an element loads. An <img>, <source> or
  srcset, a frame, a script, a stylesheet or a medium whose URL is on
  another origin is pointed nowhere and named. It is caught where it is
  set: setAttribute, which React uses, and the element's URL property.
  Anything else loaded from another origin, such as a CSS url() or a
  font, is named from the browser's resource timing. A story that does
  either fails and names the load.

Before this change, Select fails 2 stories and TemplateField fails 11
under the extended guard, each naming the image. After it, all 56 story
files pass (673 stories).
…rags, as in the test run (qorus#646)

Without the DevTools protocol, a story's touches are dispatched events
(`withTouchScreen` in Storybook's own UI). A drag sent its touchstart and
every touchmove in one burst. The colour picker's drag handler
(@uiw/react-drag-event-interactive) starts listening for moves in an effect
after the touch begins, so it heard none of them. Only the starting point
registered, and Color › On A Touch Screen and With Transparency On A Touch
Screen failed their plays in Storybook while passing in the test run, where
each touch waits for a protocol round trip.

A dispatched drag now lets the page draw a frame after the touch starts,
after each move and after the release, as a finger's events arrive a frame
or more apart.
…ys its other descriptions without their markdown (qorus#646)

A read-only row printed the chosen option's description as plain text,
while the server writes it in markdown: the IDE's AI endpoint and ML model
rows read "Each `POST /conversations` ...", backticks included. The same
was true of the two other read-row slots that show a server description:
the field's short description under its name, and the reasons under the
value, among them a default's description ("Default: thirty - Falls back
to `30` seconds").

- The chosen option's description has a line of its own, so the host's
  markdown renderer draws it (compact), as it draws the schema messages
  below it. Without a host renderer it reads as the prose its markdown
  renders to.
- The field's short description (clamped to three lines in the label's
  type) and the reasons (one line in the row's type) cannot hold the
  blocks a renderer draws, so they read as that prose
  (`markdownToProse`: `summariseMarkdown` without its cap).

Stories: Compact Read Only Choice Description Drawn (and On A Phone), and
Without Renderer. All three fail without this change.
…es, so a chip's card is not shown twice (qorus#646)

RichText › Template Tapped On A Phone showed the description twice in
Storybook's own UI: in the chip's card and in the editor's template list,
opened beside it. Only the UI did this; with a real finger (the test
run's DevTools touch) the list stays closed.

The simulated tap focused the element under the finger only when that
element was itself in the tab order. Under the finger was the chip's
label, a span, so nothing took the focus. A browser moves the focus to
the nearest focusable ancestor, here the chip. When the chip handed the
caret to the editor, the editor saw the focus come from outside the field
and opened its template list. The simulated tap now moves the focus as a
browser does: to the nearest element that a click can focus, or nowhere,
and not at all when the page cancels the press.

The card stories now also check that the description appears once:
nothing else open beside the card repeats it.
…ain text, never as markdown source (qorus#646)

TemplateText › Without A Host Renderer showed "The **customer's** full
name ... `customer.name`" with its marks. Where no host renderer was
supplied, a server description was left "as written": the description
cards (template chips, choices, schema sections, DPQL references), the
template list's rows, a read row's schema messages and chosen-option
description, and a field description's collapsed line.

Each of these places now shows the plain text its markdown renders to:
the words, with paragraph breaks and list items on their own lines, and
no marks (`markdownToPlainText`; `markdownToProse` for a one-line slot).
The stripping moved out of readFirst into helpers/markdownText, so the
cards, the lists and the rows share one implementation. It keeps the
CommonMark-shaped rules that leave an intraword underscore and a lone
asterisk alone. Without a host renderer, reqraft still draws in no
dialect of its own. The read row's one-line summary still drops fenced
blocks.

The Without A Host Renderer stories now expect the plain text. Both
fail without this change.
…p to a readable line, so one sentence takes one line (qorus#646)

ReadOnlyTemplateTag › Hover Draws The Description broke its one-sentence
description onto two lines on a wide screen. That was deliberate, but too
narrow: every description card had a fixed 360px max-width, and the
sentence needs 462px.

Cards still size to their text (a popover's content is fit-content). They
now grow up to min(560px, the screen less 24px), about 80 characters of
the card's 12px text, and never wider than a phone. All description cards
share this width (template chips, choices, schema sections, DPQL
references).

The hover stories now check that the description takes one line at
1280px. The check measures the text's height against its line height,
because `waitFor` runs again on every change to the page: a check that
added a measuring copy to the page ran itself forever and hung the story
run. It fails with the old 360px.
… data, and each story shows one thing happen (qorus#646)

From David's review of the Color stories:

- The labels (Hex code, Red, Green, Blue, Opacity %) sat at the left of
  inputs whose values are centred. Each label is now centred over its
  input.
- The help and the messages under the inputs quoted values as plain text
  (6495ED, “G”). Both are now markdown, with each value as a code span,
  drawn in the data style by the host's markdown renderer, or by reqraft's
  own where there is none (this is the field's own text). The help's four
  paragraphs keep their spacing.
- Hex Takes Only A Hex Code re-rendered visibly about three times before
  settling. A mount and render trace of the served story shows one mount
  and one render, so this was not a render loop, an effect or a remount.
  It was the story's play: one story typed three scenarios in turn (a
  wrong character, a code too short, a three-character code), and each
  changed the colour, the field and the message. Each scenario is now a
  story of its own: Hex Takes Only Hex Characters, Hex Of The Wrong
  Length Is Not Stored, Hex Of Three Is Short For Six. The other colour
  stories that chained scenarios are split the same way. Numbers Stay In
  Their Range becomes Red Past Its Range Is Not Stored, Green Takes Only
  Digits, Opacity Left Empty Shows Its Value Again and Arrow Keys Step A
  Number. On A Touch Screen becomes four stories, one per gesture. The
  help story opens the help once.

Ten of the stories check centred labels and data-styled values on
desktop, touch and phone, and all ten fail on the previous component.
…does only what it does; its description opens from an ⓘ beside it (qorus#646)

On a touch screen a description card opened on a tap of what it
describes. For a template chip that is right: the chip does nothing
else. For a control that does something, it was not: a tap on a choice's
checkbox ticked it and opened the card, and a tap on a schema section's
tab, or the header of a collapsible section, switched or opened it and
opened the card too.

Such a control now shows its description the way the pointer allows
(`describedControl`):
- with a mouse, as its hover card, unchanged;
- on a touch screen, never on the control itself. A finger-sized ⓘ
  (Reqore's 48px control) beside it opens the card, titled with the
  control's name: beside each checkbox (`besideItsInfo`), in the content
  of a section tab, and among the header actions of a collapsible
  section (`describedPanelProps`).
It applies to the allowed-value checkboxes (both FormField sets and the
check group), the schema editor's table-section tabs, and its collapsible
sections.

Stories: Field › Choices Described On Hover and On A Touch Screen, and
SchemaDefinition › Sections Described On A Touch Screen. Both touch
stories fail without this change.
…in the server's words, the Visual view too (qorus#646)

The condition and type-mismatch texts follow David's approved rewrite: "condition"
where the value is used as one, "expression" elsewhere, plain words.

- A condition that gives text, a date or a time: "The condition evaluates to
  {type}, but it must evaluate to true or false. Compare the value instead, for
  example {example}", with an example for its type (@status == "open",
  @shipped_on > @ordered_on, @starts_at < @ends_at). conditionErrorText takes
  the display name and the base type.
- A condition read by its value is said by its type: a number (true unless 0),
  a list (unless empty), a set of fields (unless it has none), or a type not
  known. conditionByTruthinessText replaces CONDITION_BY_TRUTHINESS_TEXT.
- These are the server's words, a fallback for when it said none. The mock
  language server says them too.
- The Visual view showed its own fallback for the whole condition and dropped
  the server's message for it. The server's words for the whole now reach the
  builder (conditionWholeMessage, the builder's new conditionMessage prop), as
  they reach the Text view and the part cards. The mock server now types a
  condition's whole by the record's fields, as the real server does; it left a
  field's sum untyped and judged nothing.
- A value of the wrong type: "This expression gives {X}, but this field takes
  {Y}. Choose an expression that gives {Y}, or put this one inside an
  expression that converts it." In the Text view, "The expression gives {X},
  and this field takes {Y}." plus "Qorus converts the value when it runs, and
  checks it then." Display names, not raw type names, in both. The mock's
  TYPE_MISMATCH says the same.

New stories: ExpressionField › Visual Condition Says The Servers Words (and On
A Phone) and Visual Text Condition Says The Servers Words. Their mock server
answers in German, and all three fail without this change.
… as the other phone stories do (qorus#646)

Lint requires a story's play to call another story's play with its own context directly, not from inside a callback. The phone check now follows the desktop play, as TextModeConditionThatGivesANumberOnAPhone does.
…o in plain words, and a condition field calls itself one (qorus#646)

David's approved rewrite for the expression editor:
- Builder: "Choose what this expression does.", "Qorus could not check this
  expression.", "This expression runs outside Qorus", "Put this expression
  inside another one", "Remove this expression and keep what is inside it" /
  "... and everything inside it", group labels "If" / "All of these are true
  (AND)" / "At least one of these is true (OR)", "Add a condition".
- Type modals: "This value needs one of these types:", "Use this type
  anyway?", "These values have other types than expected. Qorus converts them
  when it runs.", "Converted from X to Y", "Cleared, not converted", "No
  conversion needed".
- Argument labels, with display names joined by " or ": "This value is not
  {t}. Put it inside an expression that converts it" (which also fixes "has to
  wrapped"), "The type fits; Qorus may convert the value", "{list} is needed
  here".
- Validation: "Fill in this expression.", "Choose what this expression does.",
  "Qorus has no operation {x}.", "The expression in {operand} has an error".
- TemplateField: "This field takes an expression.", and the detected-expression
  offer reads "{text} could be an expression. It is kept as text; use it as an
  expression instead?"

A condition field (isConditionOption) now says "condition" where a value field
says "expression". The Expression tab is "Condition", its tooltips are "Write
the condition - it also takes templates" and "Build the condition visually",
and the Value tab says "This condition can't be shown as a value." These are
new IValueTabsLabels keys a host can translate: condition, conditionTooltip,
conditionVisualTooltip and cannotShowCondition. The menu item is "Write a
condition", without the value's "Run a function on this value" tooltip, which
would only repeat it.

New stories, all of which fail without this change: FormEngine › Condition
Option Tabs Say Condition (and On A Phone) and ExpressionField › Write A
Condition In Form Engine (and On A Phone).
David approved "True or false" as the bool type's name, to replace "True/False",
"Boolean" and "A yes / no value", wherever reqraft names the type:
- the AutoFormField type picker;
- the template conversion text, which lowercases it ("... converted to true or
  false");
- useQorusTypes' fallback until /system/qorus-type-info loads (display name,
  short_desc and desc). The server's name is changing to match.
- the mock catalogue's convert-to-boolean, now "Convert to true or false". Its
  descriptions will follow the server's new text once that lands.

The two validation texts David did not approve ("Value must be a True or False
value", "...must be a boolean") are unchanged.
…by its new name, True or false (qorus#646)

The field menu's Set Custom Value lists the types by their display names, and the bool type is now "True or false". The play clicked "Boolean"; its description says the new name too.
…Ctrl+Space, and a finger gets the editor's hover from a Details button (qorus#646)

A completion row's documentation, and what the server says of a word in
the text, were hover pop-ups only. A keyboard couldn't reach them, and a
finger couldn't either: a tap on a row takes the completion, and a finger
has no hover.

- The row the keyboard is on has an ⓘ when it has documentation. A click
  or Ctrl+Space (VS Code's key) shows the documentation under the row,
  inside the list, and the same again puts it away. The ⓘ's press neither
  takes the completion nor moves the focus from the text, so Enter still
  takes the row. On a coarse pointer every row with documentation has its
  ⓘ, since a finger cannot move the keyboard's row; each is 44px, and the
  rows drop their hover there. On desktop the hover stays.
- On a coarse pointer, where the server offers hover (`hoverProvider`), a
  Details button under the editor shows what it says of the word at the
  caret, in the pop-up a mouse gets by hovering (useLspHover.showAtCaret).
  The signature help already follows the caret, so a finger sees it.

Stories (SmartEditor): Completion Docs From Its Info Button, From The
Keyboard, On A Touch Screen, and Hover Details On A Touch Screen, on a
phone with real touch. All four fail without this change.
… section story waits for the table it opened (qorus#646)

SchemaDefinition › Sections Described On A Touch Screen passed in the
test run and failed in Storybook's own UI. There a tap is simulated: it
fired right after the "customers" table was tapped open, at the tab the
page was still building, and the tab never switched. A real tap, through
the DevTools protocol, comes after the protocol's round trips, by which
time the page has settled. The same simulated tap on the settled page
switches the tab.

The story now waits for the opened table's columns ("Add column") before
reaching for its tab, as a person would. A simulated tap or drag also
waits for any running animation that will end, and aims at where things
come to rest; an animation that never ends, such as a spinner, is not
waited for.
…t, Whole number, Number, Exact number, True or false, Date (qorus#646)

David's names. The server's type catalogue (QorusDataTypeCatalogue::Info,
served at /system/qorus-type-info) is the one source of a type's display
name, and the PO agent is changing it to these names. reqraft draws with
its own copy, `defaultQorusTypes`, until the server's list arrives, and
words its condition and type messages with it. That copy said "String"
for a string, so the fallback said "The condition evaluates to String"
where the server says "Text".

- The fallback list uses the names: string Text, richtext Rich text, int
  Whole number, float Number, number Exact number, bool True or false,
  date Date. The condition and type-fit messages take them from it
  ("The condition evaluates to Text ...", "The expression gives Whole
  number ...").
- The type picker of an untyped value uses the same names for the types
  David named: Whole number for int, Number for float.
- __tests__/fixtures/qorusTypeCatalogueNames.json is a copy of the
  catalogue's names, and __tests__/qorusTypeNames.test.ts compares the
  fallback list with it. It fails on the old list. To refresh the copy
  when the server changes:
  `yarn refresh:type-names ~/src/Qorus/git/qorus/Classes/QorusDataTypeCatalogue.qc`.
  The copy holds David's names, which develop's catalogue gets with the
  PO agent's change, and "Job" for job, where the catalogue's lowercase
  "job" is to be fixed with that change.
… their new names (qorus#646)

AutoFormField's type-picker story still listed Decimal and Integer, the
names the picker gave float and int before 9c4e1a5.
@davidnich

Copy link
Copy Markdown
Member Author

25 new commits (1a6de5f → 7b04038), from David's #646 review. Nothing accepted in qlip.

There is no open GitHub review or inline comment. Foxhoundn's three qlip rejections are answered on their threads in qlip (see the end of this comment).

Colour field (Components/Form/Color)

  • 678248c, 98c1489, d325959: the picker is now drawn from @uiw's saturation, hue and alpha parts (@uiw/react-color-sketch is dropped). Around them:
    • a preview of the colour, with a checkerboard under transparency;
    • labelled Hex code, Red, Green, Blue and Opacity % inputs, centred, that take only characters a colour can hold, keep numbers in range, say what was wrong, and never store an invalid colour;
    • a "What do these mean?" help in plain words, with example values (6495ED, F80 …) as code;
    • 44px targets and finger drags on touch.
  • Each story now shows one thing happening. Hex Takes Only A Hex Code chained three scenarios in one play, so it, Numbers Stay In Their Range and On A Touch Screen are split into one story per scenario.
  • Stories: components-form-color--with-value, --help-explains-the-controls, --hex-takes-only-hex-characters, --hex-of-the-wrong-length-is-not-stored, --hex-of-three-is-short-for-six, --red-past-its-range-is-not-stored, --green-takes-only-digits, --opacity-left-empty-shows-its-value-again, --arrow-keys-step-a-number, --on-a-touch-screen, --colour-bar-dragged-by-a-finger, --swatch-tapped-by-a-finger, --help-tapped-by-a-finger, --with-transparency-on-a-touch-screen, --invalid-entry-on-a-phone.

Descriptions (template chips, choices, schema sections, DPQL references)

  • 56a559f, 3239b6a: every description card is made by one helper (descriptionTooltip). It draws the description with the host's markdown renderer, where the card used to print ** and backticks, and on touch it opens on a tap. MarkdownRendererContext is exported from the barrel.
  • 0ee200c: without a host renderer, a description reads as plain text (its words and paragraph breaks, no marks). This covers the cards, the template list's rows, a read row's slots and a description's collapsed line (markdownToPlainText).
  • 8559a70: a card is as wide as its text, up to min(560px, screen − 24px). At 360px a one-sentence description broke onto two lines.
  • 3c8f45e: a read-only row draws the chosen option's description as markdown. Its short description and reasons read as prose.
  • c2913e4: on a touch screen, a tap on a checkbox, a schema tab or a collapsible section does only its own job; a 48px ⓘ beside it opens the description.
  • Stories: components-form-readonlytemplatetag--hover-draws-the-description, --tapped-on-a-phone, --without-a-host-renderer; components-form-templatetext--* (the same three); components-form-richtext--template-hover-draws-the-description, --template-tapped-on-a-phone; components-form-field--choices-described-on-hover, --choices-described-on-a-touch-screen; components-form-schemadefinition--sections-described-on-a-touch-screen; form-engine-formengine--compact-read-only-choice-description-drawn (and -on-a-phone, -without-renderer).

Smart editor: 399f0d1

  • The highlighted completion row gets an ⓘ (or Ctrl+Space) that opens its documentation; Enter still takes the completion.
  • On touch, a Details button replaces the editor's hover pop-ups.
  • Stories: components-smarteditor--completion-docs-from-its-info-button, --completion-docs-from-the-keyboard, --completion-docs-on-a-touch-screen, --hover-details-on-a-touch-screen.

Condition and expression texts (the approved #646 rewrite)

  • 9f48147, 0ab22e1, 6e2612d, 25a6bbf, 70787a8: a condition and a value of the wrong type are said in the server's words, in the Text view, the Visual view (new conditionMessage prop) and each part.
  • 0452813: the editor, value tabs and validation say what to do in plain words, and a condition field calls itself one (new IValueTabsLabels keys condition, conditionTooltip, conditionVisualTooltip, cannotShowCondition).
  • Stories: components-form-expressions-expressionfield--visual-condition-says-the-servers-words (and its phone and text variants), --write-a-condition-in-form-engine (and on a phone), form-engine-formengine--condition-option-tabs-say-condition (and on a phone).

Type names: 88a8014, 87b675d, 9c4e1a5, 7b04038

  • A type is called what the server's catalogue calls it: Text, Rich text, Whole number, Number, Exact number, True or false, Date.
  • __tests__/qorusTypeNames.test.ts compares reqraft's fallback list with a copy of the catalogue's names. To refresh the copy when the server changes: yarn refresh:type-names <QorusDataTypeCatalogue.qc>.

Forms: 159b6d0. An optional list field holding an empty list does not block a save, and a Save that refuses says why on screen.

Story harness (a201809, cf50380, 5d1fcd8, bf311a9)

  • A story that loads an image from a server now fails. The template fixtures' logo was a dead hq URL, so every app logo was an empty slot; it is now a local copy.
  • Touches simulated in Storybook's own UI now drag, focus and tap as a real finger does.

Foxhoundn's qlip rejections (answered in qlip as well)

  • Shows Templates List For Boolean: a yes/no in template mode is written in the shared text field with the template as a chip. Typing is never blocked; anything but one template alone is flagged with what to change, and the form can't be saved. This is David's decision of 10-09 (free typing with flags, no character restriction).
  • Template Value: the description you questioned was wrong and was fixed in 80b9c2d. The capture matches it now: one text field, the "Interface ID" chip, no number input.
  • Template With Function Value: the same decision. Free typing in the integer argument is flagged, never silently stored.

…ation never reaches another file (qorus#646)

The same race reqore's CI hit on #708. A touch story turns on Chrome's
touch emulation through the DevTools protocol (`withTouchScreen`), and
that emulation belongs to the whole page, so a story file running beside
it, in another frame, sees a coarse pointer and no hover: a hover story
would find a tap-to-open card, and a mouse-sized control its touch size.
reqraft's last run happened not to overlap. The storybook project now
runs one story file at a time (`fileParallelism: false`).
…ium cannot give a page its mouse back (qorus#646)

Turning Chromium's touch emulation off does not restore the hover and pointer
the browser was launched with: it takes the ones the machine reports.
Playwright launches its headless Chromium as a mouse
(--blink-settings=primaryHoverType=2,...,primaryPointerType=4), and a Linux CI
runner reports no mouse, so after the first touch story every later story in
the same page found (hover: none). 052ba87 ran the story files one at a time,
which put every file in that page, and FormEngineRemote's Injected Compact
Option Actions failed in CI: its hover-only action was drawn at rest. A Mac
reports a mouse, so the same run passes there: a Chromium launched with
blink-settings that say no mouse comes back from the emulation with the Mac's
mouse, not with its launch settings.

The stories that call withTouchScreen, directly or through
playTapIsDrawnOnAPhone, are tagged 'touch' and run in a Vitest project of
their own (storybook-touch): their own browser page, after the other stories,
one file at a time. The other stories run in parallel again, in pages that
never were phones. Both projects share one qlip plugin, so a run is still one
qlip build. withTouchScreen fails, with the reason, anywhere but
storybook-touch, so an untagged touch story cannot bring the problem back.
@davidnich

Copy link
Copy Markdown
Member Author

2 new commits (7b04038 → f3f5d3f): the story run's touch screen no longer leaks into other stories. Nothing accepted in qlip.

  • 052ba87 ran the story files one at a time. That was the wrong diagnosis, and f3f5d3f replaces it.
  • f3f5d3f is the root cause and fix:
    • Turning Chromium's touch emulation off does not restore the hover and pointer the browser was launched with. It takes the ones the machine reports.
    • Playwright launches headless Chromium as a mouse (--blink-settings=primaryHoverType=2,…,primaryPointerType=4), but a Linux CI runner reports no mouse. So every story after a touch story in the same page found (hover: none).
    • With one file at a time, that page held every file, and form-engine-formengineremote--injected-compact-option-actions failed: its hover-only action was drawn at rest.
    • A Mac reports a mouse, so the run passes there.
    • Stories that call withTouchScreen, directly or through playTapIsDrawnOnAPhone, are now tagged touch and run in their own Vitest project, storybook-touch. That gives them their own browser page, after the other stories, one file at a time.
    • The other stories run in parallel again.
    • Both projects share one qlip plugin, so a run is still one qlip build.
    • withTouchScreen throws, with the reason, anywhere outside storybook-touch.
    • Tagged stories:
      • components-form-color--on-a-touch-screen, --colour-bar-dragged-by-a-finger, --swatch-tapped-by-a-finger, --help-tapped-by-a-finger, --with-transparency-on-a-touch-screen;
      • the Field, SchemaDefinitionField and SmartEditor touch stories (…-choices-described-on-a-touch-screen, …-sections-described-on-a-touch-screen, …-completion-docs-on-a-touch-screen, …-hover-details-on-a-touch-screen);
      • the RichText, TemplateText and ReadOnlyTemplateTag tap stories.
  • Verified locally:
    • the 7 files with touch stories and FormEngineRemote pass (Color: 18 stories in storybook, 5 in storybook-touch);
    • an untagged touch story fails with the reason;
    • precheck passes.

CI at f3f5d3f

  • PullRequestTests passed: 208 unit files (2159 tests) and 63 story files (698 stories). The 7 files with touch stories ran in storybook-touch.
  • Publish released 0.10.62-pr.137.gf3f5d3f.
  • qlip build 20261012-195719: 698 snapshots, 68 new, 32 changed, 0 failed. These are the same 100 pending snapshots as build 318. 95 images are byte-identical; the other 5 differ by at most 3 of 255 levels in 1–96 pixels, which is anti-aliasing. Each has its explanation again; nothing is accepted.

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