diff --git a/docs/parity/feature-matrix.md b/docs/parity/feature-matrix.md index d73d8f9..849ab98 100644 --- a/docs/parity/feature-matrix.md +++ b/docs/parity/feature-matrix.md @@ -31,13 +31,13 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | NAV | 40 | 20 | 0 | 0 | 1 | 19 | | PRC | 60 | 55 | 0 | 0 | 1 | 4 | | PRF | 5 | 5 | 0 | 0 | 0 | 0 | -| QRY | 70 | 60 | 0 | 0 | 1 | 9 | +| QRY | 70 | 61 | 0 | 0 | 0 | 9 | | REC | 65 | 59 | 0 | 0 | 0 | 6 | | REL | 9 | 9 | 0 | 0 | 0 | 0 | | RPT | 20 | 20 | 0 | 0 | 0 | 0 | | SEC | 43 | 30 | 0 | 0 | 1 | 12 | | WID | 69 | 56 | 0 | 0 | 0 | 13 | -| **all** | 401 | 330 | 0 | 0 | 4 | 67 | +| **all** | 401 | 331 | 0 | 0 | 3 | 67 | ## Rows @@ -72,7 +72,7 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | NAV-027 | Developer page reports the backend metadata counts and the running framework versions | pass | pass | qqq whoami: apiVersion, features, counts; TUI: F9 › Connection: UI API version, features, extensions, metadata generation and age, client build; live | | NAV-028 | Reports in the app tree are listed under their app (sidebar and app home Reports group) and open the report pa | pass | pass | qqq report list (with app) and apps show list reports in the app's Reports group, report run opens them; unit-tested and stub-server tested (the sample has no reports); TUI: reports listed under their app and in the tree, palette and F9; Enter runs one (format chosen, process screens, file saved) (unit-tested only; the sample has no reports) | | NAV-029 | A user who may access no apps sees the Dashboard entry and the no-apps message (Material NoApps parity) | n/a | n/a | browser shell | -| NAV-030 | Global search (header box, '/' dialog, search page) lists the records a backend record search (POST /qqq/v1/se | pass | blocked | qqq search over POST /search; TUI: the palette calls POST /search; the sample lists recordSearch with no provider (server finding 21) | +| NAV-030 | Global search (header box, '/' dialog, search page) lists the records a backend record search (POST /qqq/v1/se | pass | blocked | qqq search over POST /search; TUI: the palette calls POST /search; the sample lists recordSearch with no provider (server finding 21); live on fix branch 8bf6a5a66: recordSearch is no longer advertised and the palette says so; POST /search still has no provider (finding 21) | | NAV-031 | Record search respects table read permissions and record security locks, in the UI and the API | pass | pass | server-enforced; refusals shown; TUI: server-enforced; the palette shows only what the server returns | | NAV-032 | Without searchable tables (the backend declares no searchFields) global search stays local and never calls a s | n/a | n/a | browser shell | | NAV-033 | Login branding before sign-in: logo, app name, accent color, favicon and title from GET /qqq/v1/metaData/authe | n/a | n/a | browser shell | @@ -107,7 +107,7 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | PRC-017 | Dynamic step list from processMetaDataAdjustment.updatedFrontendStepList | pass | pass | updatedFrontendStepList followed; TUI: kernel runner | | PRC-018 | Updated field metadata from processMetaDataAdjustment.updatedFields | pass | pass | updatedFields applied; TUI: kernel runner | | PRC-019 | Async job progress in the live job view (message, N / M, determinate progress bar, elapsed) then continue | pass | pass | message, N/M, elapsed on stderr; TUI: bar, N/M, message, elapsed | -| PRC-020 | Cancel from a screen and mid-job runs the cancel step and leaves | pass | blocked | Ctrl-C during a job or a prompt calls POST …/cancel, exit 130; TUI: Esc asks, then POST cancel; the sample accepts the cancel and keeps the run (server finding 13) | +| PRC-020 | Cancel from a screen and mid-job runs the cancel step and leaves | pass | blocked | Ctrl-C during a job or a prompt calls POST …/cancel, exit 130; TUI: Esc asks, then POST cancel; the sample accepts the cancel and keeps the run (server finding 13); still blocked on the fix branch (finding 13) | | PRC-021 | Step errors: user-facing messages directly, internal errors behind a detail toggle, Close | pass | pass | userFacingError shown; TUI: failure message shown; F5 re-runs | | PRC-022 | Retry after an error restarts with the original record selection | n/a | pass | retry is re-running the command; TUI: F5 or r runs again with the same records | | PRC-023 | Completion: Next/Submit labels, Return on the last screen or when noMoreSteps | n/a | pass | screen labels; TUI: Ctrl+S continues; the last screen closes with Enter or Esc | @@ -162,7 +162,7 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | QRY-005 | Column widths, density and card view | n/a | pass | grid presentation; TUI: v: widths, fit or fill, zebra, density | | QRY-006 | Opening a record and returning keeps the query; refresh shows backend changes | n/a | pass | TUI: Esc lands on the row last viewed, filter and page kept | | QRY-007 | A failed query reports the backend error with a retry | pass | pass | error with code, status and trace id; TUI: error shown in the grid; F5 retries | -| QRY-010 | Text operators match Material labels and backend semantics | pass | pass | all operators and shorthand; text matches (~, CONTAINS, STARTS_WITH, …) carry CASE_INSENSITIVE by default, ~= / OPERATOR:CS / --case-sensitive match case (finding 11 is by design; the v1 route drops the option until finding 27, qqq#1358); TUI: operators by field type in the filter dialog | +| QRY-010 | Text operators match Material labels and backend semantics | pass | pass | all operators and shorthand; text matches (~, CONTAINS, STARTS_WITH, …) carry CASE_INSENSITIVE by default, ~= / OPERATOR:CS / --case-sensitive match case (finding 11 is by design; the v1 route drops the option until finding 27, qqq#1358); TUI: operators by field type in the filter dialog; text matches ignore case (CASE_INSENSITIVE), Match exact case on request (live on fix branch 8bf6a5a66) | | QRY-011 | Number operators including ranges and lists | pass | pass | BETWEEN, IN lists | | QRY-012 | Date operators and relative date expressions | pass | pass | now(), now(-7d), thisPeriod(), startOf(); TUI: date presets: now(-7d), thisPeriod(month) … | | QRY-013 | Date-time operators convert local input to UTC and support relative values | pass | pass | --where/--group DATE_TIME values local to UTC (a date is local midnight); expressions as before; TUI: DATE_TIME criteria typed in the local zone go out as UTC instants; expressions pass through (unit-tested) | @@ -171,7 +171,7 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | QRY-016 | AND/OR and nested groups combine as declared | pass | pass | --group with parentheses; TUI: AND/OR and groups, guided and advanced | | QRY-017 | Filters live in the URL: reload, back/forward and Material-style JSON links | pass | n/a | --print-filter gives the JSON the web links accept; TUI: URL state is browser-only; the TUI shows the grammar and the CLI command, and qqq ui accepts the command as a route | | QRY-018 | Backend-only operators from links render and filter correctly | pass | pass | any operator in --where; TUI: More operators… offers every operator | -| QRY-019 | Quick search combines with the advanced filter | pass | pass | --search on query, count and export: CONTAINS over the shown text fields, ORed, ANDed with the filter; sent with CASE_INSENSITIVE (--case-sensitive to match case; the v1 route drops the option until finding 27, qqq#1358); TUI: q quick search ANDed onto the filter (kernel) | +| QRY-019 | Quick search combines with the advanced filter | pass | pass | --search on query, count and export: CONTAINS over the shown text fields, ORed, ANDed with the filter; sent with CASE_INSENSITIVE (--case-sensitive to match case; the v1 route drops the option until finding 27, qqq#1358); TUI: q quick search ANDed onto the filter (kernel); q avery finds Avery (CASE_INSENSITIVE); =Text matches exact case (live on fix branch 8bf6a5a66) | | QRY-020 | One-side exposed join: labelled columns, filters and sorts match SQL | pass | pass | --join and table.field in --fields/--where/--order, labelled "Join: Field"; verified live through an association join (person to pet); exposed joins unit-tested (the sample exposes none); TUI: joined columns and filters, labelled "Join: Field", joins planned by the kernel (unit-tested only; the sample exposes no joins) | | QRY-021 | Many-side exposed join repeats rows and reports the distinct count | pass | pass | a many-side join repeats rows; count and --count add the distinct count (person --join pet: 21 rows, 16 distinct); TUI: many-side join: rows repeat and the status shows the distinct count (unit-tested only) | | QRY-022 | Column statistics aggregate the filtered rows | pass | pass | qqq table stats [-w …] through the columnStats process when offered (unit-tested; the sample has neither QUERY_STATS nor the process); --local computes the same figures and was verified live; TUI: S: kernel column statistics of the column under the cursor with the filter (unit-tested only; no sample QUERY_STATS table) | @@ -208,7 +208,7 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | QRY-082 | Basic mode cannot simplify repeated or nested advanced conditions and explains why | n/a | n/a | TUI: the guided builder shows every condition; no basic mode | | QRY-083 | Advanced query shows and removes individual conditions (Next inline editor) | n/a | pass | TUI: guided builder edits and removes conditions | | QRY-084 | Sort picker changes field and direction; clearing filters requires confirmation and keeps the sort | n/a | pass | TUI: sort in the builder; c clears | -| QRY-085 | Weekday criteria use the metadata-enabled date function and ISO weekday values | blocked | blocked | weekday(field) IN 1,7 sends the WeekdayOfDate fieldFunction with ISO weekdays (--print-filter); the sample's H2 backend answers 500 (server finding 24, qqq#1355); TUI: weekday(field) IN days sent as the WeekdayOf fieldFunction; the sample H2 backend fails it (server finding 24, qqq#1355) | +| QRY-085 | Weekday criteria use the metadata-enabled date function and ISO weekday values | blocked | pass | weekday(field) IN 1,7 sends the WeekdayOfDate fieldFunction with ISO weekdays (--print-filter); the sample's H2 backend answers 500 (server finding 24, qqq#1355); TUI: weekday(birthDate) IN 1,7 returns its 2 people (live on fix branch 8bf6a5a66; on the released sample it fails, finding 24, qqq#1355) | | QRY-086 | Relative date presets expose their evaluated date and filter records | pass | pass | expressions | | QRY-087 | Bulk pasted values become a multi-value filter | pass | pass | IN lists; quoted values; TUI: IN takes a pasted comma list | | QRY-088 | Instance query help accompanies the bulk paste dialog | n/a | pass | TUI: paste values split on commas, tabs, semicolons, new lines; the instance bulkAddFilterValues help shown (unit-tested only; the sample has no such help) | @@ -224,8 +224,8 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | REL-004 | Three-level associations: each pet shows only its own notes | pass | pass | person > pets > notes: each pet lists only its own notes; TUI: child lists three levels deep (a pet's notes under the pet); live on person 1 | | REL-005 | Editing and deleting a child leaves no stale parent, list or unexpected rows | pass | pass | records are never cached: update or delete a child, the next get/children shows the current rows; TUI: the parent reloads on return | | REL-006 | Denied child reads show an unavailable panel without data or actions | pass | pass | unreadable child tables shown as unavailable without data or actions; table children shows the server message; unit-tested (the sample has no denied child table, finding 17); TUI: unavailable panel (kernel associationGroups) | -| REL-007 | Aliased and composite associations bind to their widgets and list the right members | pass | pass | aliased and composite associations bound to their widget sections, members by every join value; unit-tested (the sample declares none, finding 17); TUI: kernel associationGroups binds aliased associations to their widgets | -| REL-008 | Adding through a composite association stores every join value | pass | pass | table create --parent sets every join value of a composite association; unit-tested (the sample declares none); TUI: a new child through a composite association gets every join value (unit-tested only) | +| REL-007 | Aliased and composite associations bind to their widgets and list the right members | pass | pass | aliased and composite associations bound to their widget sections, members by every join value; unit-tested (the sample declares none, finding 17); TUI: kernel associationGroups binds aliased associations to their widgets; adornmentLab tags through the composite association (live on fix branch 8bf6a5a66) | +| REL-008 | Adding through a composite association stores every join value | pass | pass | table create --parent sets every join value of a composite association; unit-tested (the sample declares none); TUI: a new child through a composite association gets every join value; a new adornmentLab tag gets adornmentLabId and labKey (live on fix branch 8bf6a5a66) | | REL-009 | Deleting a parent removes associated children and leaves other parents intact | pass | pass | table delete of a parent: its pets are gone, other parents keep theirs; TUI: server cascade; the list reloads | | REC-001 | Record view shows the record label, tiered sections and backend-formatted values | pass | pass | table get by section and tier; TUI: sections by tier, two columns when they fit | | REC-002 | Empty values render a placeholder; hidden fields and hidden sections are never shown | pass | pass | empty values show --; hidden sections and fields never shown; unit-tested (the sample has none, finding 17); TUI: hidden fields and sections are left out; empty values blank | @@ -248,22 +248,22 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | REC-019 | Zoned DATE_TIME display behaviors (fixed zone and per-record zone) | n/a | pass | display zones; TUI: DATE_TIME through the kernel displayDateTime: server-zoned display values as sent, else the viewer's zone; live on Field Lab | | REC-020 | HTML field edits in the rich-text editor and renders sanitized | pass | n/a | HTML fields shown as text (sanitized); table update -e edits the HTML source in $VISUAL/$EDITOR; verified live on fieldLab htmlValue; TUI: the rich-text editor is a browser component; HTML is edited as source in $EDITOR and shown as text | | REC-021 | PASSWORD is masked on read and never overwritten by the mask | pass | pass | server masks; TUI: masked | -| REC-022 | BLOB upload and download without a download adornment | pass | pass | table create/update --upload and table download (base64 BLOB value) verified on fieldLab; default name into the download directory; TUI: BLOB upload through the file dialog and save from the record (Field Lab, live) | +| REC-022 | BLOB upload and download without a download adornment | pass | pass | table create/update --upload and table download (base64 BLOB value) verified on fieldLab; default name into the download directory; TUI: BLOB upload through the file dialog and save from the record (Field Lab, live); upload into uploadedFileArchive (text primary key typed on create) and download of the same 22 bytes (live on fix branch 8bf6a5a66) | | REC-023 | Dynamic default values (user id, create day, modify day, explicit NONE) | pass | pass | server; TUI: defaults applied by FormSession and the server | | REC-024 | Max-length policies: truncate, ellipsis, pass-through and reject | pass | pass | maxLength checked locally; TUI: local check and server policy | | REC-025 | Case and whitespace normalization on save | pass | pass | behaviors; TUI: behaviors on save | | REC-026 | Numeric range policies: reject, exclusive reject, inclusive clip and exclusive clip | pass | pass | ranges checked locally; TUI: local range check and server policy | | REC-027 | Unique keys are enforced with the backend message on create and edit | pass | pass | server message; TUI: server message shown | -| REC-028 | LINK adornment: record links and URL links | pass | pass | record links name table and key, --follow opens them; URL links show the URL; TUI: links marked, Enter follows (kernel record-view) | -| REC-029 | CHIP adornment colors and icons by value | pass | pass | CHIP as [value] colored by color. (or colorMap) on a terminal, icon named in the record view; unit-tested (none in the sample); TUI: CHIP values bracketed and colored by the adornment color (unit-tested only) | -| REC-030 | SIZE adornment sets the query grid column width | n/a | pass | TUI: saved widths per column; SIZE as text width | +| REC-028 | LINK adornment: record links and URL links | pass | pass | record links name table and key, --follow opens them; URL links show the URL; TUI: links marked, Enter follows (kernel record-view); adornmentLab carrier LINK (live on fix branch 8bf6a5a66) | +| REC-029 | CHIP adornment colors and icons by value | pass | pass | CHIP as [value] colored by color. (or colorMap) on a terminal, icon named in the record view; unit-tested (none in the sample); TUI: CHIP values bracketed and colored by the adornment color; adornmentLab CHIP [ACTIVE] in the grid and the record (live on fix branch 8bf6a5a66) | +| REC-030 | SIZE adornment sets the query grid column width | n/a | pass | TUI: saved widths per column; SIZE as text width; adornmentLab SIZE (live on fix branch 8bf6a5a66) | | REC-031 | CODE_EDITOR adornment: code view with formatting and a code editor on forms | pass | n/a | CODE_EDITOR values shown verbatim; table update -e opens the code in $EDITOR with the language's extension; unit-tested (none in the sample; -e verified live on an HTML field); TUI: the code editor is a browser component; code is shown as text and edited in $EDITOR | -| REC-032 | RENDER_HTML adornment renders sanitized HTML | pass | pass | RENDER_HTML shown as sanitized text; unit-tested (none in the sample); TUI: RENDER_HTML shown as text with the markup stripped (kernel htmlToText) | -| REC-033 | REVEAL adornment masks, reveals and copies the value | pass | pass | REVEAL masked in the record view, query and child lists until --reveal; table get --copy copies the value (pbcopy/clip/wl-copy/xclip, else OSC 52); masking unit-tested, --copy verified live; TUI: masked; Enter reveals | -| REC-034 | FILE_DOWNLOAD adornment links to the stored file with its name | pass | pass | FILE_DOWNLOAD shows the file name and qqq table download saves it through the field route; unit-tested (none in the sample); BLOB values without the adornment are decoded locally (server finding 16); TUI: Enter saves through the save dialog | +| REC-032 | RENDER_HTML adornment renders sanitized HTML | pass | pass | RENDER_HTML shown as sanitized text; unit-tested (none in the sample); TUI: RENDER_HTML shown as text with the markup stripped (kernel htmlToText); adornmentLab RENDER_HTML as text in the grid and the record (live on fix branch 8bf6a5a66) | +| REC-033 | REVEAL adornment masks, reveals and copies the value | pass | pass | REVEAL masked in the record view, query and child lists until --reveal; table get --copy copies the value (pbcopy/clip/wl-copy/xclip, else OSC 52); masking unit-tested, --copy verified live; TUI: masked; Enter reveals; adornmentLab REVEAL masked in the grid and the record (live on fix branch 8bf6a5a66) | +| REC-034 | FILE_DOWNLOAD adornment links to the stored file with its name | pass | pass | FILE_DOWNLOAD shows the file name and qqq table download saves it through the field route; unit-tested (none in the sample); BLOB values without the adornment are decoded locally (server finding 16); TUI: Enter saves through the save dialog; adornmentLab FILE_DOWNLOAD (live on fix branch 8bf6a5a66) | | REC-035 | FILE_UPLOAD adornment formats, replacing and removing a stored file | pass | pass | create/update --upload field=path replaces the file, --set field= removes it (verified live on a BLOB); FILE_UPLOAD's format (button or drag and drop) is presentation only; TUI: FILE_UPLOAD fields choose a file; Backspace clears | -| REC-036 | TOOLTIP adornment shows its text on the value | pass | pass | tooltip text in parentheses after the value; unit-tested (none in the sample); TUI: tooltip text appended by valueText | -| REC-037 | WIDGET adornment renders the field value as the named widget | pass | pass | the value rendered through the named widget's render model; unit-tested (none in the sample); TUI: a WIDGET-adorned field's value drawn as its widget (unit-tested only) | +| REC-036 | TOOLTIP adornment shows its text on the value | pass | pass | tooltip text in parentheses after the value; unit-tested (none in the sample); TUI: tooltip text appended by valueText; adornmentLab TOOLTIP (live on fix branch 8bf6a5a66) | +| REC-037 | WIDGET adornment renders the field value as the named widget | pass | pass | the value rendered through the named widget's render model; unit-tested (none in the sample); TUI: a WIDGET-adorned field's value drawn as its widget; adornmentLab WIDGET field with a plain value shows the value (live on fix branch 8bf6a5a66) | | REC-038 | ERROR adornment highlights the value | pass | pass | ERROR values marked with !; unit-tested (none in the sample); TUI: marked by valueText | | REC-039 | Field help content follows screen roles and formats | pass | pass | table get --help-content: field help by view-screen role, HTML/MARKDOWN as text; qqq help show lists help alone; unit-tested (the sample has no help, finding 17); TUI: field help by screen role in F1 and in the form's field hint (unit-tested only) | | REC-040 | Section help content on the record view | pass | pass | section help under the section with --help-content; unit-tested (finding 17); TUI: section help under the section heading and in F1 (unit-tested only) | @@ -302,8 +302,8 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | SEC-008 | Process disabled for a user (DenyBehavior.DISABLED): shown disabled, link explains the denial, backend refuses | pass | pass | a DISABLED process (hasPermission false) is listed by process list and table actions with the reason; process run refuses (exit 3) before init; stub-server tested; TUI: a disabled process is listed "(no access)" and refused with the reason; table actions show it disabled (unit-tested only) | | SEC-009 | Widget restrictions: hidden widgets absent, disabled widgets show a permission state without loading data, bac | pass | pass | widget get shows the refusal; TUI: hidden widgets are absent; a disabled widget shows the permission state without a request; children of a parent skip denied ones (unit-tested only) | | SEC-010 | App restrictions: hidden apps absent from navigation and unknown by link; disabled apps keep their children ga | pass | pass | TUI: hidden apps are absent and unknown by name; a disabled app explains the denial (unit-tested only) | -| SEC-011 | Record-level security (sharing demo): users see only their own or shared saved views/reports; direct links and | pass | pass | server-enforced: saved views (and bulk load profiles) are listed as the server returns them, yours first, others as shared by their owner; changing someone else's is refused locally or by the server; stub-server tested; TUI: saved views and reports: the TUI lists what the server returns; sharing through the share dialog; a refused direct link shows the server message (unit-tested only) | -| SEC-012 | Record security lock on a data table: list filtered to the key, other users' records unreachable, forged owner | pass | pass | server; TUI: record security is server-side: grids show the server's rows; a record the server refuses shows its message (403) (unit-tested only) | +| SEC-011 | Record-level security (sharing demo): users see only their own or shared saved views/reports; direct links and | pass | pass | server-enforced: saved views (and bulk load profiles) are listed as the server returns them, yours first, others as shared by their owner; changing someone else's is refused locally or by the server; stub-server tested; TUI: saved views and reports: the TUI lists what the server returns; sharing through the share dialog; a refused direct link shows the server message; sharing demo: the owner sample:alice shares her saved report (live on fix branch 8bf6a5a66) | +| SEC-012 | Record security lock on a data table: list filtered to the key, other users' records unreachable, forged owner | pass | pass | server; TUI: record security is server-side: grids show the server's rows; a record the server refuses shows its message (403); adornmentLabLocker 2 answers Permission denied, shown on the record screen (live on fix branch 8bf6a5a66) | | SEC-013 | Field protection: hidden fields never shown or returned, password fields masked and never overwritten by the m | pass | pass | server; TUI: hidden fields never shown (grid, record, form); PASSWORD values masked; edits send only changed fields, never the masked value (unit-tested only) | | SEC-014 | Capability restrictions: tables without insert/update/delete capabilities offer none of them and refuse direct | pass | pass | TUI: without TABLE_INSERT/UPDATE/DELETE capabilities the F-keys, menu entries and copy are off and n/e/d are refused (unit-tested only) | | SEC-015 | Permission revoked mid-session: stale UI actions are refused by the backend with a clear message, nothing is w | pass | pass | TUI: a save refused mid-session keeps the form with what was typed and shows the server message (unit-tested only) | @@ -418,13 +418,13 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | RPT-006 | report input fields: required validation and the value filters the rows | pass | pass | report input fields asked on a terminal or given with -v; a missing required one exits 2 naming it before the report runs; the value is sent to the step; stub-server tested; TUI: report input fields are the report process's form: required checks, values sent with the run (unit-tested only) | | RPT-007 | report permissions: restricted report denied in UI and by the server | pass | pass | server; TUI: a restricted report is marked (no access) and refused with the reason; the server refuses too (unit-tested only) | | RPT-008 | reports appear in app navigation and open their run page | pass | pass | reports listed by report list and under their app in apps show; report run opens them; stub-server tested; TUI: reports appear in app navigation and run from there (unit-tested only) | -| RPT-009 | saved report view shows its filter/columns and pivot setup | pass | pass | qqq saved-report show: filter, sort, columns and pivot setup; stub-server tested; TUI: saved report record: filter, sort, columns and pivot setup drawn (unit-tested only) | +| RPT-009 | saved report view shows its filter/columns and pivot setup | pass | pass | qqq saved-report show: filter, sort, columns and pivot setup; stub-server tested; TUI: saved report record: filter, sort, columns and pivot setup drawn; saved report Pet Species Report: filter, sort, columns, pivot (live on fix branch 8bf6a5a66) | | RPT-010 | saved report renders through renderSavedReport and downloads the saved columns and rows | pass | pass | qqq saved-report run [--format]: renderSavedReport with the record, the file downloaded; stub-server tested; TUI: the record actions list the table processes (renderSavedReport) and run them on the record; the DOWNLOAD_FORM file is saved (process downloads unit-tested only) | | RPT-011 | streaming report route for reports without a process | pass | pass | report run on a report without a process (or --route) streams from GET /reports/{name}; stub-server tested; TUI: a report without a process streams from GET /reports/{name}?format= into the save dialog (unit-tested only) | | RPT-012 | scheduled report: create a schedule for a saved report and view its cron schedule | pass | pass | qqq saved-report schedule --cron --time-zone --to [--format]; saved-report schedules lists them with the server's cron description; stub-server tested; TUI: scheduled report: created on its form; the record shows the cron schedule (cronUI) (unit-tested only) | -| RPT-013 | sharing: the owner shares a saved report with a user (Read-Only) | pass | pass | qqq share add savedReport user: --scope read-only (insertSharedRecord); stub-server tested; TUI: share a saved report with a user Read-Only (Z, :share) (unit-tested only) | -| RPT-014 | sharing: change a share's scope to Read and Edit | pass | pass | qqq share scope savedReport read-write (editSharedRecord); stub-server tested; TUI: change a share to Read and Edit (unit-tested only) | -| RPT-015 | sharing: remove a share | pass | pass | qqq share remove savedReport (asks; --yes); stub-server tested; TUI: remove a share after a confirm (unit-tested only) | +| RPT-013 | sharing: the owner shares a saved report with a user (Read-Only) | pass | pass | qqq share add savedReport user: --scope read-only (insertSharedRecord); stub-server tested; TUI: share a saved report with a user Read-Only (Z, :share); shared Pet Species Report Read-Only (live on fix branch 8bf6a5a66) | +| RPT-014 | sharing: change a share's scope to Read and Edit | pass | pass | qqq share scope savedReport read-write (editSharedRecord); stub-server tested; TUI: change a share to Read and Edit; changed to Read and Edit (live on fix branch 8bf6a5a66) | +| RPT-015 | sharing: remove a share | pass | pass | qqq share remove savedReport (asks; --yes); stub-server tested; TUI: remove a share after a confirm; removed the share (live on fix branch 8bf6a5a66) | | RPT-016 | sharing record security: another user cannot see an unshared report until it is shared | pass | pass | server-enforced record security: saved-report list shows only what the server returns (yours, shared with you); stub-server tested; TUI: unshared reports are the server's to hide; the TUI lists what it returns and shows a refused link's message (unit-tested only) | | RPT-017 | sharing: only the owner may share | pass | pass | only the owner may share: the server's refusal shown (exit 2); stub-server tested; TUI: only the owner may share: others see Share disabled with the reason (unit-tested only) | | RPT-018 | sharing a saved view through the share dialog | pass | pass | qqq share … savedView : the same sharing processes for saved views; stub-server tested with saved reports; TUI: saved views share through the same dialog (Saved views menu) (unit-tested only) | @@ -439,7 +439,7 @@ The TUI column is `qqq ui` (M3); its notes start with "TUI:". | WID-074 | Standalone composite scalar controls and metadata editors | n/a | pass | standalone composite controls are browser input editors (caret, IME, widths) whose drafts stay on the page; the CLI shows the controls and their values (WID-057), and process-hosted inputs go through the form session (PRC-037); TUI: standalone composite INPUT_FIELD blocks show their seeded values read-only by type (no standalone input target in a terminal) (unit-tested only) | | WID-075 | Standalone DATE_TIME blocks localize seeded instants without losing seconds | pass | pass | a seeded DATE_TIME input block is shown in the local zone with its seconds (kernel displayDateTime); unit-tested; TUI: standalone DATE_TIME blocks show seeded instants in the local zone with seconds (unit-tested only) | | HLP-001 | App and widget help | pass | pass | qqq help show app/widget/instance; unit-tested (the sample carries no help content, finding 17); TUI: F1 on an app home: app help and each widget's help (slot maps) (unit-tested only) | -| HLP-002 | Table, field and section help | pass | pass | qqq help show table [field]: table, section and field help; unit-tested (finding 17); TUI: table, field and section help by screen role in F1 (unit-tested only; the sample has none) | +| HLP-002 | Table, field and section help | pass | pass | qqq help show table [field]: table, section and field help; unit-tested (finding 17); TUI: table, field and section help by screen role in F1 (unit-tested only; the sample has none); F1 on adornmentLab: table and section help (live on fix branch 8bf6a5a66) | | HLP-003 | Query columns and table action help | pass | pass | field help is the column help; table action help slots printed with their slot name; unit-tested (finding 17); TUI: the grid footer shows the help of the column under the cursor; table help slots in F1 (unit-tested only) | | HLP-004 | Process, step and record action help | pass | pass | qqq help show process

: process, step, HELP_TEXT and step field help; unit-tested (finding 17); TUI: F1 on a process: process, step and step field help; on a record: table, section and field help for the view role (unit-tested only) | | HLP-005 | Help search | pass | pass | qqq help-search; TUI: type in F1 | diff --git a/packages/core/src/records/record-view.ts b/packages/core/src/records/record-view.ts index dd67bbb..a2413f3 100644 --- a/packages/core/src/records/record-view.ts +++ b/packages/core/src/records/record-view.ts @@ -228,6 +228,8 @@ function valueTextWithoutTooltip(field: FieldMeta, record: RecordLike, options: const widgetName = typeof widget.values?.widgetName === 'string' ? widget.values.widgetName : '' if (options.widgets && !options.widgets[widgetName]) return { text: `Error: Could not load widget [${widgetName}]`, adornment: 'WIDGET', error: true } if (raw === null || raw === undefined) return { text: empty, adornment: 'WIDGET' } + // a plain value (not a widget payload) is shown as it is + if (typeof raw !== 'object') return { text: str(display ?? raw), adornment: 'WIDGET' } const text = blocksToText(widgetToBlocks(raw), { width: 80 }) return { text: text || str(raw), adornment: 'WIDGET' } } diff --git a/packages/tui/src/screens/form.tsx b/packages/tui/src/screens/form.tsx index 9626c58..29e4ce3 100644 --- a/packages/tui/src/screens/form.tsx +++ b/packages/tui/src/screens/form.tsx @@ -64,6 +64,11 @@ function textOf(value: unknown): string { * fields. Keys are read from the object's state (no React closures), so a burst of keys lands in * the field that had focus when each key was read. Used by table forms and process steps. */ +/** A primary key the server makes (a number, or not editable): not asked on create; a text key is typed in. */ +function generatedKey(f: { type?: string; isEditable?: boolean | null } | undefined): boolean { + return !f || f.isEditable === false || f.type === 'INTEGER' || f.type === 'LONG' +} + export class FormEditor { index = 0 top = 0 @@ -395,7 +400,7 @@ export class FormScreen implements Screen { this.originals = { ...initial } for (const f of Object.values(meta.fields)) if (f.type === 'DATE_TIME' && isInstant(initial[f.name])) initial[f.name] = utcToLocalInput(String(initial[f.name]), defaultTimeZone()) const fields = Object.values(meta.fields) as FieldMeta[] - const session = new FormSession({ baseUrl: client.server, fetch: client.fetch, scope: { table: this.tableName }, fields, initialValues: initial, tableMeta: meta as never, exclude: this.options.mode === 'edit' ? [] : [meta.primaryKeyField] }) + const session = new FormSession({ baseUrl: client.server, fetch: client.fetch, scope: { table: this.tableName }, fields, initialValues: initial, tableMeta: meta as never, exclude: this.options.mode === 'edit' || !generatedKey(meta.fields[meta.primaryKeyField]) ? [] : [meta.primaryKeyField] }) for (const [k, v] of Object.entries(display)) if (session.hasChoices(k) && !isMissingValue(initial[k])) session.displayValues[k] = v // labels for prefilled possible values (create child: the parent's id) for (const [k, v] of Object.entries(this.options.initial ?? {})) { diff --git a/packages/tui/src/screens/record.tsx b/packages/tui/src/screens/record.tsx index a06f832..e9c1dad 100644 --- a/packages/tui/src/screens/record.tsx +++ b/packages/tui/src/screens/record.tsx @@ -315,7 +315,10 @@ export class RecordScreen implements Screen { const widgetFields = fields.filter((f) => f.adornments?.some((a) => a.type === 'WIDGET') && rec.values?.[f.name] && typeof rec.values[f.name] === 'object') for (const f of widgetFields) { rows.push({ text: ` ${f.label}`, fg: t.fg, bold: true }) - rows.push(...blockLines(widgetToBlocks(rec.values![f.name]), inner - 6).map((l) => ({ ...l, text: ` ${l.text}` }))) + const wv = rec.values![f.name] + // a WIDGET-adorned field whose value is plain text (not a widget payload) shows the text + if (wv !== null && typeof wv !== 'object') rows.push({ text: ` ${String(wv)}` }) + else rows.push(...blockLines(widgetToBlocks(wv), inner - 6).map((l) => ({ ...l, text: ` ${l.text}` }))) } const plainFields = fields.filter((f) => !widgetFields.includes(f)) for (let i = 0; i < plainFields.length; i += two ? 2 : 1) { diff --git a/packages/tui/src/screens/table.tsx b/packages/tui/src/screens/table.tsx index a5d6184..81b3a16 100644 --- a/packages/tui/src/screens/table.tsx +++ b/packages/tui/src/screens/table.tsx @@ -29,6 +29,8 @@ export interface GridColumn { type: string width: number numeric: boolean + /** The field's adornments: cells are drawn as the record view draws them (REVEAL masked, HTML as text…). */ + adornments?: FieldMetaData['adornments'] } /** The default column order: primary key, then section fields by tier, then the rest (hidden and heavy left out). */ @@ -55,7 +57,7 @@ export function layoutColumns(fields: FieldMetaData[], rows: QueryRecord[], avai const numeric = ['INTEGER', 'LONG', 'DECIMAL'].includes(f.type) && !f.possibleValueSourceName const longest = Math.max(f.label.length + 1, ...rows.map((r) => fieldCell(r, f).length), 3) const saved = view.widths?.[f.name] - return { name: f.name, label: f.label, type: f.type, numeric, longest, width: saved ?? Math.min(40, longest) } + return { name: f.name, label: f.label, type: f.type, adornments: f.adornments, numeric, longest, width: saved ?? Math.min(40, longest) } }) const total = () => cols.reduce((a, c) => a + c.width + gap, 0) let spare = available - total() diff --git a/packages/tui/src/util/text.ts b/packages/tui/src/util/text.ts index 21073a8..87bd66b 100644 --- a/packages/tui/src/util/text.ts +++ b/packages/tui/src/util/text.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0 (see LICENSE). */ -import { displayDateTime } from '@qrun-io/qqq-core' +import { displayDateTime, valueText } from '@qrun-io/qqq-core' import { glyphs } from '../theme' /** Cuts text to a width, with an ellipsis when it was longer. */ @@ -47,7 +47,12 @@ export function cell(record: { values?: Record; displayValues?: } /** A value as text for a field: DATE_TIME in the viewer's zone (kernel displayDateTime), else the display value. */ -export function fieldCell(record: { values?: Record; displayValues?: Record } | undefined, field: { name: string; type: string }): string { +export function fieldCell(record: { values?: Record; displayValues?: Record } | undefined, field: { name: string; type: string; adornments?: unknown[] | null }): string { + // adorned fields as the record view shows them, in one cell: REVEAL masked, RENDER_HTML as text, CHIP, LINK… + if (record && field.adornments?.length) { + const text = valueText(field as never, { values: record.values ?? {}, displayValues: record.displayValues } as never, { cell: true }).text + return text.replace(/\s*\n\s*/g, ' ') + } if (field.type === 'PASSWORD') { const raw = record?.values?.[field.name] return raw === null || raw === undefined || raw === '' ? '' : '••••••' diff --git a/scripts/parity-matrix.py b/scripts/parity-matrix.py index f1c67e0..1e121f7 100644 --- a/scripts/parity-matrix.py +++ b/scripts/parity-matrix.py @@ -393,6 +393,39 @@ TUI.update({ TUI.update({ 'SEC-030': (B, 'needs a real Auth0 tenant: [qqq-cli#10](https://github.com/QRun-IO/qqq-cli/issues/10); the sample has no AUTH_0 configuration ([qqq#1359](https://github.com/QRun-IO/qqq/issues/1359), finding 28); the Auth0 protocol path is SEC-029'), }) +# Verified live on a private server from the qqq fix branch 8bf6a5a66 (UI API 1.9.0, sample with +# -Dqqq.sample.sharing=true) on port 8091; :8077 runs the released sample. +FIX = 'live on fix branch 8bf6a5a66' +LIVE = { + 'QRY-085': (P, f'weekday(birthDate) IN 1,7 returns its 2 people ({FIX}; on the released sample it fails, finding 24, qqq#1355)'), + 'QRY-010': (None, f'text matches ignore case (CASE_INSENSITIVE), Match exact case on request ({FIX})'), + 'QRY-019': (None, f'q avery finds Avery (CASE_INSENSITIVE); =Text matches exact case ({FIX})'), + 'NAV-030': (None, f'{FIX}: recordSearch is no longer advertised and the palette says so; POST /search still has no provider (finding 21)'), + 'PRC-020': (None, 'still blocked on the fix branch (finding 13)'), + 'REC-022': (None, f'upload into uploadedFileArchive (text primary key typed on create) and download of the same 22 bytes ({FIX})'), + 'REC-028': (None, f'adornmentLab carrier LINK ({FIX})'), + 'REC-029': (None, f'adornmentLab CHIP [ACTIVE] in the grid and the record ({FIX})'), + 'REC-030': (None, f'adornmentLab SIZE ({FIX})'), + 'REC-032': (None, f'adornmentLab RENDER_HTML as text in the grid and the record ({FIX})'), + 'REC-033': (None, f'adornmentLab REVEAL masked in the grid and the record ({FIX})'), + 'REC-034': (None, f'adornmentLab FILE_DOWNLOAD ({FIX})'), + 'REC-036': (None, f'adornmentLab TOOLTIP ({FIX})'), + 'REC-037': (None, f'adornmentLab WIDGET field with a plain value shows the value ({FIX})'), + 'REL-007': (None, f'adornmentLab tags through the composite association ({FIX})'), + 'REL-008': (None, f'a new adornmentLab tag gets adornmentLabId and labKey ({FIX})'), + 'SEC-012': (None, f'adornmentLabLocker 2 answers Permission denied, shown on the record screen ({FIX})'), + 'SEC-011': (None, f'sharing demo: the owner sample:alice shares her saved report ({FIX})'), + 'RPT-009': (None, f'saved report Pet Species Report: filter, sort, columns, pivot ({FIX})'), + 'RPT-013': (None, f'shared Pet Species Report Read-Only ({FIX})'), + 'RPT-014': (None, f'changed to Read and Edit ({FIX})'), + 'RPT-015': (None, f'removed the share ({FIX})'), + 'HLP-002': (None, f'F1 on adornmentLab: table and section help ({FIX})'), +} +for _rid, (_st, _extra) in LIVE.items(): + _cur = TUI.get(_rid, (P, '')) + _note = _cur[1].replace(' (unit-tested only)', '').replace('(unit-tested only)', '').strip() + TUI[_rid] = (_st or _cur[0], _extra if _st else (f'{_note}; {_extra}' if _note else _extra)) + rows = [] for line in open(SRC, encoding='utf-8', errors='replace'): m = re.match(r'^\|\s*([A-Z]{3}-\d{3})\s*\|\s*(.*?)\s*\|', line)