Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 39 additions & 12 deletions DEFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,8 @@ documented as such.

## 15. `addon/components/template-builder/properties-panel.js:219` — the table's `query` data mode has no control

**Status:** DEFERRED — a later update, by decision. Kickoff brief in Appendix D.
**Found:** `else if (mode === 'query')` reports `[0,0]` — never evaluated either way.
**Status:** FIXED (PR #164)
**Found:** `else if (mode === 'query')` reported `[0,0]` — never evaluated either way.
**Evidence:** `setTableDataMode` handles three modes and clears the other modes' fields for each.
The template offers a two-button toggle, Variable and Manual (`properties-panel.hbs:258` and `:266`);
nothing anywhere calls it with `'query'`. `data_source_mode` appears in exactly three places in the
Expand All @@ -288,16 +288,43 @@ by this action and read by nothing.
**Impact:** none at runtime. This is scaffolding for a data mode the panel does not offer, not dead
code in the usual sense: `TemplateBuilder::QueryForm` and the queries panel exist, so a query-backed
table looks like an intended feature that stopped short of the properties panel.
**Fix:** finish it. Confirmed as intended behaviour — fetch from a url with params — and deferred to
its own session rather than half-built here. What it needs before anyone starts: an endpoint
contract, an auth story, loading and error states, a defined shape for `query_response_path`, and
something on the render side that consumes a query-backed table (nothing does today). It also has to
be reconciled with the `__queries__` variable route, which already solves the same problem by
exposing saved queries as variables — otherwise the panel ends up with two competing mechanisms.

Until then the branch stays uncovered rather than suppressed: an `istanbul ignore` here would have
to sit on the opening `if`, and `ignore else` there also swallows the `variable` branch, which real
tests cover.
**Fix:** finished, not deleted — the mode was confirmed as intended behaviour, fetch from a url with
params. The panel now has a three-button toggle and a query-mode form; `element-renderer` labels a
query- or variable-backed table on the canvas; and the six questions this was blocked on are
answered below.

**Applied — the decisions, so they are not rediscovered:**

- **Reconciliation with `__queries__`, the one that mattered.** They stay two mechanisms with a
stated boundary, and Variable mode remains the usual answer. A saved `TemplateQuery` is a
structured query over a registered `model_type`, reusable across elements and saved with the
template; it is reached through **Variable** mode, under `__queries__`, and that mode's hint now
names the namespace so the structured route is the one found first. Query mode is one API path
bound to one element, and exists for what the query builder cannot express — aggregates, reports,
and extension endpoints with no model behind them. The boundary is written into
`properties-panel.js` above the query helpers.
- **Endpoint contract.** A path relative to the Fleetbase API (`int/v1/orders`); leading slashes are
stripped. Absolute and protocol-relative URLs are **rejected**, as typed and again before any
request fires: the `fetch` service attaches the session to everything it sends, so a third-party
host would be handed those credentials.
- **Auth.** The injected `fetch` service. Nothing new was introduced.
- **`query_params` shape.** `[{ key, value }]`, matching the `[]` the clearing arms already seeded.
Values may hold `{variable}` tokens, resolved downstream at render.
- **`query_response_path`.** A dotted path into the response body; blank means the body is itself the
array. Every way it can fail to resolve is reported by name — a missing segment, a segment that
runs into a primitive, and a path that lands on something other than an array.
- **Fetching.** The mode stores intent, like variable mode — nothing in this addon resolves a data
source at render time. The one exception is the explicit **Test query** button, which fetches once
so the endpoint, params and path can be checked before saving. It never writes the fetched rows
onto the element; it reports the row count and keys, and offers to turn those keys into columns.
Params still holding an unresolved token are left out of that request and named.
- **Loading and error states.** In-flight reporting on the button, request and response-path failures
both surfaced, and results keyed to the element they ran against so selecting another table does
not show it the previous one's results.

The `istanbul ignore` problem the deferral noted is gone rather than suppressed: the final arm is now
a plain `else`, so there is no third condition carrying a permanently-unreachable false path, and the
`variable` branch real tests cover is untouched.

## 16. Coverage collection itself is unreliable, which the 100% gate cannot tolerate

Expand Down
8 changes: 8 additions & 0 deletions addon/components/template-builder/element-renderer.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@
{{/each}}
</tr>
{{/each}}
{{#if this.tableSourceCaption}}
<tr>
<td
class="tb-table-source-caption border px-2 py-1 text-gray-400 text-center italic truncate"
colspan={{this.tableColumns.length}}
>{{this.tableSourceCaption}}</td>
</tr>
{{/if}}
{{/if}}
</tbody>
{{else}}
Expand Down
18 changes: 18 additions & 0 deletions addon/components/template-builder/element-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,24 @@ export default class TemplateBuilderElementRendererComponent extends Component {
return this.args.element?.rows ?? [];
}

/**
* Caption drawn under the placeholder grid of a table that has no rows of its
* own. Nothing here resolves a data source — the builder stores intent and
* something downstream renders it — so without this a variable- or
* query-backed table looks exactly like an empty one on the canvas. Only read
* from the table arm of the template, where `isTable` proves the element.
*/
get tableSourceCaption() {
const el = this.args.element;
if (el.data_source_mode === 'variable') {
return el.data_source ? `Rows from ${el.data_source}` : 'Rows from a variable';
}
if (el.data_source_mode === 'query') {
return el.query_endpoint ? `Rows from ${el.query_endpoint}` : 'Rows from a query';
}
return null;
}

get tableBorderStyle() {
const color = this.args.element?.border_color;
return color ? `border-color: ${color}` : '';
Expand Down
124 changes: 122 additions & 2 deletions addon/components/template-builder/properties-panel.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@
>
<div class="space-y-2">

{{! Two-mode toggle: Variable | Manual }}
{{! Three-mode toggle: Variable | Query | Manual }}
<div class="flex rounded overflow-hidden border border-gray-200 dark:border-gray-700 text-xs">
<button
type="button"
Expand All @@ -257,6 +257,14 @@
'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700'}}"
{{on "click" (fn this.setTableDataMode "variable")}}
>Variable</button>
<button
type="button"
class="flex-1 py-1 font-medium border-l border-gray-200 dark:border-gray-700 transition-colors
{{if (eq this.tableDataMode 'query')
'bg-blue-500 text-white'
'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700'}}"
{{on "click" (fn this.setTableDataMode "query")}}
>Query</button>
<button
type="button"
class="flex-1 py-1 font-medium border-l border-gray-200 dark:border-gray-700 transition-colors
Expand Down Expand Up @@ -284,7 +292,119 @@
</button>
</div>
</TemplateBuilder::PropertiesPanel::Field>
<p class="text-xs text-gray-400 dark:text-gray-500">The variable must resolve to an array of objects. Each object's keys should match the column keys defined above.</p>
<p class="text-xs text-gray-400 dark:text-gray-500">The variable must resolve to an array of objects. Each object's keys should match the column keys defined above. Saved queries appear here under <code>__queries__</code>.</p>

{{! ── QUERY MODE ── }}
{{else if this.isTableQueryMode}}
<TemplateBuilder::PropertiesPanel::Field @label="API Endpoint">
<input
type="text"
class="tb-input tb-query-endpoint"
placeholder="int/v1/orders"
value={{this.element.query_endpoint}}
{{on "change" (fn this.updateProp "query_endpoint")}}
/>
</TemplateBuilder::PropertiesPanel::Field>
{{#if this.queryEndpointError}}
<p class="tb-query-endpoint-error text-xs text-red-500">{{this.queryEndpointError}}</p>
{{else}}
<p class="text-xs text-gray-400 dark:text-gray-500">A path on the Fleetbase API. Requests carry your session, so full URLs to other hosts are not accepted.</p>
{{/if}}

<TemplateBuilder::PropertiesPanel::Field @label="Query Parameters">
<div class="tb-query-params space-y-1">
{{#each this.queryParams as |param index|}}
<div class="tb-query-param flex items-center space-x-1">
<input
type="text"
class="tb-input tb-query-param-key flex-1"
placeholder="status"
value={{param.key}}
{{on "change" (fn this.updateQueryParam index "key")}}
/>
<input
type="text"
class="tb-input tb-query-param-value flex-1"
placeholder="{order.status}"
value={{param.value}}
{{on "change" (fn this.updateQueryParam index "value")}}
/>
<button
type="button"
class="text-red-400 hover:text-red-600"
title="Remove parameter"
{{on "click" (fn this.removeQueryParam index)}}
>
<FaIcon @icon="xmark" class="w-3 h-3" />
</button>
</div>
{{else}}
<p class="text-xs text-gray-400 dark:text-gray-500">No parameters.</p>
{{/each}}
<button
type="button"
class="w-full flex items-center justify-center space-x-1.5 px-2 py-1.5 rounded border border-dashed border-gray-300 dark:border-gray-600 text-xs text-gray-500 dark:text-gray-400 hover:border-blue-400 hover:text-blue-500 transition-colors"
{{on "click" this.addQueryParam}}
>
<FaIcon @icon="plus" class="w-3 h-3" />
<span>Add parameter</span>
</button>
</div>
</TemplateBuilder::PropertiesPanel::Field>
<p class="text-xs text-gray-400 dark:text-gray-500">Values may hold variable tokens such as <code>{order.uuid}</code>, resolved when the template is rendered.</p>

<TemplateBuilder::PropertiesPanel::Field @label="Response Path">
<input
type="text"
class="tb-input tb-query-response-path"
placeholder="data.results"
value={{this.element.query_response_path}}
{{on "change" (fn this.updateProp "query_response_path")}}
/>
</TemplateBuilder::PropertiesPanel::Field>
<p class="text-xs text-gray-400 dark:text-gray-500">Dotted path to the array of rows in the response. Leave blank when the response is that array itself.</p>

<button
type="button"
class="tb-query-test w-full flex items-center justify-center space-x-1.5 px-2 py-1.5 rounded border border-gray-300 dark:border-gray-600 text-xs text-gray-600 dark:text-gray-400 hover:border-blue-400 hover:text-blue-500 transition-colors disabled:opacity-50"
disabled={{this.isTestingQuery}}
{{on "click" this.testQuery}}
>
{{#if this.isTestingQuery}}
<FaIcon @icon="spinner" @spin={{true}} class="w-3 h-3" />
<span>Testing…</span>
{{else}}
<FaIcon @icon="play" class="w-3 h-3" />
<span>Test query</span>
{{/if}}
</button>

{{#if this.queryTestError}}
<p class="tb-query-test-error text-xs text-red-500">{{this.queryTestError}}</p>
{{/if}}

{{#if this.queryTestResult}}
<div class="tb-query-test-result rounded border border-gray-200 dark:border-gray-700 p-2 space-y-1">
<p class="text-xs text-gray-600 dark:text-gray-400">
Returned {{this.queryTestResult.count}}
{{if (eq this.queryTestResult.count 1) "row" "rows"}}.
</p>
{{#if this.queryTestResult.skippedParams.length}}
<p class="tb-query-test-skipped text-xs text-amber-500">Sent without unresolved parameters: {{this.queryTestResult.skippedParamsLabel}}.</p>
{{/if}}
{{#if this.queryTestResult.keys.length}}
<p class="tb-query-test-keys text-xs text-gray-500 dark:text-gray-400 break-all">Keys: {{this.queryTestResult.keysLabel}}</p>
<button
type="button"
class="tb-query-apply-columns w-full flex items-center justify-center space-x-1.5 px-2 py-1 rounded border border-dashed border-gray-300 dark:border-gray-600 text-xs text-gray-500 dark:text-gray-400 hover:border-blue-400 hover:text-blue-500 transition-colors"
{{on "click" this.applyDiscoveredColumns}}
>
<FaIcon @icon="table-columns" class="w-3 h-3" />
<span>Use these as columns</span>
</button>
{{/if}}
</div>
{{/if}}

{{! ── MANUAL MODE ── }}
{{else}}
Expand Down
Loading