merge follow-up: keep the TUI's setup-widget pairs and sharing module, finding 28 linked
The base brought its own sharing module, a manageSession password sign-in and render-model additions. The CLI now uses records/sharing.ts for qqq share, keeps its sessionId-cookie sign-in as signInWithPasswordCookie next to the TUI's signInWithPassword, and the render model keeps both sides: the web's wording and empty states from this branch, the TUI's pairs for the saved report setup widgets (saved-report show still words them as the web does), its bar-chart default, fieldValueList indents and block markers. Finding 28 is qqq#1359; SEC-030 also cites qqq-cli#10. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
6f52f6283d
commit
f16b61f496
4 files changed
+34
-13
No files matched your search
@@ -5,10 +5,10 @@ browser-only reason) or **blocked** (citing a server finding in `docs/server-fin
|
||||
`todo` at the end. The status source is `STATUS` in `scripts/parity-matrix.py`; regenerate with
|
||||
`python3 scripts/parity-matrix.py docs/parity/next-feature-matrix.md`.
|
||||
|
||||
Start: 173 pass / 42 partial / 80 todo / 106 n/a of 401.
|
||||
Start: 173 pass / 42 partial / 80 todo / 106 n/a of 401 (branch `feat/m1-cli-parity` from `feat/m1-kernel-cli` 6c56b89).
|
||||
Checkpoint 1 (bulk load): 188 pass / 41 partial / 66 todo / 106 n/a.
|
||||
Checkpoint 2 (records, queries, processes, help): 232 pass / 1 partial / 60 todo / 106 n/a / 2 blocked.
|
||||
Checkpoint 3 (navigation, widgets, reports, security): 290 pass / 0 partial / 0 todo / 107 n/a / 4 blocked. Blocked: QRY-085 (finding 24, qqq#1355), REC-063 (finding 19, qqq#1351), SEC-029 and SEC-030 (finding 28: no AUTH_0 server; SEC-030 needs tenant credentials from James). New n/a: WID-074 (standalone composite controls are browser input editors). (branch `feat/m1-cli-parity` from `feat/m1-kernel-cli` 6c56b89).
|
||||
Checkpoint 3 (navigation, widgets, reports, security): 290 pass / 0 partial / 0 todo / 107 n/a / 4 blocked. Blocked: QRY-085 (finding 24, qqq#1355), REC-063 (finding 19, qqq#1351), SEC-029 and SEC-030 (finding 28, qqq#1359: no AUTH_0 server; SEC-030 also needs tenant credentials from James, qqq-cli#10). New n/a: WID-074 (standalone composite controls are browser input editors).
|
||||
|
||||
"unit-tested" in a row note means the sample at :8077 cannot exercise the row (no fixture); the kernel and the
|
||||
command are covered by node tests (and a local stub server where a command is involved). Everything else is
|
||||
@@ -104,8 +104,8 @@ Status column: **plan** = what the row becomes; **done** once the STATUS source
|
||||
| SEC-002/008 DISABLED tables and processes | listed with the reason; use refused before the call; unit-tested | done |
|
||||
| SEC-006 copy needs insert | `table copy` refused without insert permission | done |
|
||||
| SEC-011 record-level security | server-enforced; listed as returned | done |
|
||||
| SEC-029/030 AUTH_0 | same device/PKCE code as OAUTH2 (verified with qqq-dev-idp); real tenant needs credentials | done |
|
||||
| SEC-034..037 TABLE_BASED | `qqq login` with username/password over HTTP Basic (the server's TableBasedAuthenticationModule creates the session and sets the cookie); unit-tested with a stub | done |
|
||||
| SEC-029/030 AUTH_0 | sign-in from the tenant, client id and audience (unit-tested); no AUTH_0 server to run it against | blocked (finding 28, qqq#1359; SEC-030 also qqq-cli#10) |
|
||||
| SEC-034..037 TABLE_BASED | `qqq login --username` over HTTP Basic (the server's TableBasedAuthenticationModule creates the session and sets the sessionId cookie); stub-server tested | done |
|
||||
|
||||
## Help
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('reports against a stub', () => {
|
||||
assert.match(list.stdout, /3,Open orders,order,you,,status EQUALS open/)
|
||||
assert.match(list.stdout, /4,Bob report,order,shared by bob@example.com/)
|
||||
const show = await qqq(['saved-report', 'show', 'Open orders', '-O', 'table'])
|
||||
assert.match(show.stdout, /Match all of:\n {2}Status equals open\nColumns: Id, Name\nPivot table:\nThis report does not use a pivot table\./)
|
||||
assert.match(show.stdout, /Match all of:\n {2}Status equals open\nColumns: Id, Name\nPivot table:\nNo pivot table: this report does not use one\./)
|
||||
const preview = await qqq(['saved-report', 'preview', '3', '-l', '1', '-O', 'table'])
|
||||
assert.match(preview.stdout, /Ann/)
|
||||
assert.match(preview.stdout, /2 in all/)
|
||||
@@ -126,7 +126,7 @@ describe('reports against a stub', () => {
|
||||
const pivot = await qqq(['saved-report', 'pivot', '3', '--rows', 'state', '--values', 'SUM:total,COUNT:id'])
|
||||
assert.match(pivot.stdout, /Values +Sum of Total, Count of Id/)
|
||||
const clear = await qqq(['saved-report', 'pivot', '3', '--clear'])
|
||||
assert.match(clear.stdout, /does not use a pivot table/)
|
||||
assert.match(clear.stdout, /No pivot table/)
|
||||
assert.equal(saved.pivotTableJson, null)
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { blocksToText, widgetToBlocks } from '../widgets/render-model.js'
|
||||
import { blocksToText, filterAndColumnsBlocks, pivotTableBlocks, widgetToBlocks } from '../widgets/render-model.js'
|
||||
import { loadDashboard, loadWidget } from '../widgets/loader.js'
|
||||
|
||||
const text = (data: unknown, context = {}) => blocksToText(widgetToBlocks(data, context))
|
||||
@@ -73,9 +73,12 @@ test('editor and developer widgets render read-only: cronUI, dynamicForm, pivotT
|
||||
assert.equal(text({ type: 'cronUI' }, { meta: { type: 'cronUI' }, record: {} }), 'No schedule set')
|
||||
assert.equal(text({ type: 'dynamicForm', fieldList: [{ name: 'a', label: 'A' }], recordOfFieldValues: { values: { a: 1 }, displayValues: { a: 'One' } } }), 'A One')
|
||||
assert.equal(text({ type: 'dynamicForm', fieldList: [], noFieldsMessage: 'Nothing to set' }), 'Nothing to set')
|
||||
assert.equal(text({ type: 'pivotTableSetup' }, { record: { pivotTableJson: JSON.stringify({ rows: [{ fieldName: 'state' }], columns: [], values: [{ fieldName: 'total', function: 'SUM' }] }) } }), 'Rows state\nColumns None\nValues Sum of total')
|
||||
assert.equal(text({ type: 'pivotTableSetup' }, { record: { pivotTableJson: JSON.stringify({ rows: [{ fieldName: 'state' }], columns: [], values: [{ fieldName: 'total', function: 'SUM' }] }) } }), 'Rows state\nColumns (none)\nValues total (sum)')
|
||||
assert.equal(blocksToText(pivotTableBlocks({ pivotTableJson: JSON.stringify({ rows: [{ fieldName: 'state' }], columns: [], values: [{ fieldName: 'total', function: 'SUM' }] }) })), 'Rows state\nColumns None\nValues Sum of total')
|
||||
assert.equal(text({ type: 'pivotTableSetup' }, { record: {} }), 'No pivot table: this report does not use one.')
|
||||
assert.equal(text({ type: 'filterAndColumnsSetup' }, { record: { queryFilterJson: JSON.stringify({ criteria: [{ fieldName: 'state', operator: 'EQUALS', values: ['IL'] }], booleanOperator: 'AND', orderBys: [{ fieldName: 'id', isAscending: false }] }), columnsJson: JSON.stringify({ columns: [{ name: 'id', isVisible: true }, { name: 'x', isVisible: false }] }) } }), 'Match all of:\n state equals IL\nSorted by id descending\nColumns: id')
|
||||
const saved = { queryFilterJson: JSON.stringify({ criteria: [{ fieldName: 'state', operator: 'EQUALS', values: ['IL'] }], booleanOperator: 'AND', orderBys: [{ fieldName: 'id', isAscending: false }] }), columnsJson: JSON.stringify({ columns: [{ name: 'id', isVisible: true }, { name: 'x', isVisible: false }] }) }
|
||||
assert.match(text({ type: 'filterAndColumnsSetup' }, { record: saved }), /Filter +state equals IL\nSort +id descending\nColumns +id/)
|
||||
assert.equal(blocksToText(filterAndColumnsBlocks(saved)), 'Match all of:\n state equals IL\nSorted by id descending\nColumns: id')
|
||||
assert.equal(text({ type: 'rowBuilder', records: [{ values: { a: 1 }, displayValues: { a: 'One' } }] }, { meta: { type: 'rowBuilder', defaultValues: { fields: [{ name: 'a', label: 'A' }] } } }), 'A\n---\nOne')
|
||||
assert.equal(text({ type: 'process', processMetaData: { name: 'greet', label: 'Greet' }, defaultValues: { x: 1 } }), 'Process: Greet (greet), with x=1')
|
||||
})
|
||||
|
||||
@@ -271,12 +271,30 @@ export function widgetToBlocks(data: unknown, context: WidgetContext = {}): Rend
|
||||
blocks.push({ kind: 'pairs', pairs: fields.map((f) => ({ label: `${str(f.label ?? f.name)}${f.isRequired ? ' *' : ''}`, value: str(display[String(f.name)] ?? values[String(f.name)] ?? '') })) })
|
||||
break
|
||||
}
|
||||
case 'pivotTableSetup':
|
||||
blocks.push(...pivotTableBlocks(context.record, fieldLabel))
|
||||
// the setup widgets as label/value pairs (the TUI's record view); `qqq saved-report show` words them as the web does (filterAndColumnsBlocks, pivotTableBlocks)
|
||||
case 'filterAndColumnsSetup': {
|
||||
const rv = ((context.record?.values ?? context.record ?? {}) as J)
|
||||
const filter = (parseJsonValue(rv[String(w.filterFieldName ?? 'queryFilterJson')]) ?? {}) as J
|
||||
const crit = ((filter.criteria ?? []) as J[]).map((c) => `${fieldLabel(String(c.fieldName))} ${String(c.operator ?? '').toLowerCase().replace(/_/g, ' ')} ${((c.values ?? []) as unknown[]).map(str).join(', ')}${c.otherFieldName ? str(c.otherFieldName) : ''}`.trim())
|
||||
const sorts = ((filter.orderBys ?? []) as J[]).map((o) => `${str(o.fieldName)} ${o.isAscending === false ? 'descending' : 'ascending'}`)
|
||||
const rawColumns = parseJsonValue(rv[String(w.columnsFieldName ?? w.columnFieldName ?? 'columnsJson')])
|
||||
const colList = Array.isArray(rawColumns) ? rawColumns : Array.isArray((rawColumns as J | undefined)?.columns) ? (rawColumns as J).columns as unknown[] : []
|
||||
const cols = colList.map((c) => (typeof c === 'string' ? { name: c } : c as J)).filter((c) => c.isVisible !== false && c.name !== '__check__').map((c) => str(c.name))
|
||||
const pairs = [{ label: 'Table', value: str(w.tableName ?? rv.tableName) || '(none)' }, { label: 'Filter', value: crit.length ? crit.join(` ${String(filter.booleanOperator ?? 'AND')} `) : '(no criteria)' }, { label: 'Sort', value: sorts.join(', ') || '(default)' }]
|
||||
if (w.hideColumns !== true) pairs.push({ label: 'Columns', value: cols.join(', ') || '(all)' })
|
||||
blocks.push({ kind: 'pairs', pairs })
|
||||
break
|
||||
case 'filterAndColumnsSetup':
|
||||
blocks.push(...filterAndColumnsBlocks(context.record, w, fieldLabel))
|
||||
}
|
||||
case 'pivotTableSetup': {
|
||||
const rv = ((context.record?.values ?? context.record ?? {}) as J)
|
||||
const pivot = parseJsonValue(rv.pivotTableJson) as J | undefined
|
||||
if (!pivot || typeof pivot !== 'object') blocks.push({ kind: 'text', text: EMPTY_STATE.pivotTableSetup! })
|
||||
else {
|
||||
const names = (k: string) => ((pivot[k] ?? []) as J[]).map((x) => `${str(x.fieldName)}${x.function ? ` (${str(x.function).toLowerCase()})` : ''}`).join(', ') || '(none)'
|
||||
blocks.push({ kind: 'pairs', pairs: [{ label: 'Rows', value: names('rows') }, { label: 'Columns', value: names('columns') }, { label: 'Values', value: names('values') }] })
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'rowBuilder': {
|
||||
const dv = (meta.defaultValues ?? {}) as J
|
||||
const records = (w.records ?? []) as J[]
|
||||
|
||||
Reference in new issue
Block a user