@masterteam/dashboard-builder
v0.0.109
Published
Dashboard Builder components and services for Angular 21+ with Tailwind CSS 4. The package supports authoring, saving, loading, and rendering dashboards with charts, header widgets, topbar widgets, metadata-driven data sources, source links, and respon
Readme
@masterteam/dashboard-builder
Dashboard Builder components and services for Angular 21+ with Tailwind CSS 4. The package supports authoring, saving, loading, and rendering dashboards with charts, header widgets, topbar widgets, metadata-driven data sources, source links, and responsive ECharts output.
Features
- Multilingual dashboard item titles stored as
clientConfig.title.en/ar. - Plain bar-chart entries can be edited together in Style > Bar colors: each entry stores an English label, Arabic label, and color against its stable bar key, so those choices persist when the data is sorted or its source label changes.
- Header, topbar, and chart item title editing in builder mode.
- Host-interceptor-owned save messages, with automatic saves marked silent.
- Registry-driven chart query schemas, including safe preview states for incomplete draft charts.
- Line Chart uses one backend-compatible query
(
xAxisProperty,yAxisProperty,function: Sum|Avg|Count,extraProperties). Saved legacyseries[]settings are migrated from the first series;AveragebecomesAvg. The current backend does not support multiple Line Chart series. Its flatvalues[]response is rendered as one line, with backend category labels and colors. - Stacked-bar query fields switch between auto-stack and manual-stack modes so only the relevant stack property selector is displayed and emitted.
- Quick Manage style changes stay local in the drawer until Apply, with an isolated Preview tab for checking the edited chart config before the dashboard canvas is updated.
- Chart header icons provide preset-based appearance and placement controls, including independent standalone/group visibility and grouped icon-only mode.
- Timeline multi-level queries persist per-level
extraPropertiesand still read legacyextrasfields. Open timeline detail popovers close when the page, dashboard, or timeline scrolls outside the overlay, while the detail panel's own scrolling remains available. - PhaseGate Table uses a dedicated semantic-role editor and sends every configured runtime property in one deduplicated backend projection.
- Phase Gate Stepper shows one record's phases in order, marking the current
and completed ones. It reads the PhaseGate module through the chart endpoint
(#1664) — the former
api/Levels/{levelId}/PhaseGatescall has no backend route. Steppers saved before the repair ask for a data source. See Phase Gate Stepper Contract. - Map Chart plots
Locationproperties over region boundaries (mt-map-view) with grouped marker colours, a group legend and clickable markers. The boundaries are GeoJSON bundled with the package and registered with ECharts, so nothing is fetched at runtime — no tile server, no API key, works offline. See Map Chart Contract. - Ring Gauge with Formula accepts either
valuePropertyorformulafor each series, exposes no Group By or Extra Properties controls, and renders legacyvalues[].bars[]responses through the concentric-ring gauge adapter. It keeps each numeric detail visible (including0) without exposing the generated series key as a gauge label or legend entry. - Every GeneralQuery capability that includes
formulaexposes a visible per-series Show/Hide advanced settings action and a Formula Builder scoped to that series; formula availability is schema-driven rather than chart-specific. It uses the legacy P+4 Dashboard Builder dialect rather than Formula Engine: the original nine function keys and eight punctuation operators compose a compact expression without inter-token whitespace, and selected properties serialize as bare keys such asactual_progress(for example,sum(actual_progress), neversum ( @Current::actual_progress )). Existing scoped or spaced chart formulas are migrated on load, save, and execution; whitespace inside quoted custom values is preserved. - Selected-property chips in dashboard query editors can be removed inline.
- Properties/card-list items normalize null Percentage values to
0with an empty progress track and never inherit a sibling card's runtime percentage. - Duplicated charts receive a new local dashboard identity, omit the source
link/component IDs so the next save creates a new backend chart, and persist
duplicatedFromas provenance to the source dashboard identity. - Workspace-aware metadata loading for modules and properties.
- A dedicated chart Filters action separates viewer-facing filter controls and
built-in conditions from datasource configuration, using shared
mt-*form fields throughout the condition editor. - Navigation actions expose a
Navigation Targetselector. Both page and entity navigation default to the current tab and can explicitly open in a new tab throughaction.config.navigationTarget. Page navigation replaces only the active dashboard id while preserving host route segments such asmanageordashboards. Entity navigation uses browser-level application navigation so a current-tab action triggered in the control-panel builder can load the app-client workspace route instead of falling through the admin router to All Workspaces. - Progress Update cards render activity HTML through ngx-quill's read-only
renderer, preserving the rich-text size, color, font, alignment, direction,
and semantic formatting configured in the source editor. When the response
contains multiple updates, the card displays the final entry in
values(the most recently entered update); an empty list retains the empty state. - Persisted dashboards can be deleted from the page-settings drawer after a named, danger-styled confirmation; successful deletion returns the host to its dashboard list while standalone builders keep deletion unavailable.
- Module multi-select serialized as selectors such as
Level:1,2,3. - Source-link chaining with previous result fields, relationship fields, and the
Parentoption exposed to child-level configuration. - Distinct aggregation normalization from legacy
DisticntCounttoDistinctCount. - Container-aware ECharts option mapping for compact dashboard grid cells,
including shared legend placement for donut, pie, bar, line, stacked bar, and
other ECharts-based charts. Pie/donut radius, center, and slice-label geometry
stay handler-owned to match the old dashboard builder; the shared EChart
wrapper reserves the same chart area as the old wrapper and does not apply
pie-specific responsive overrides. Native ECharts legend rendering stays
authoritative; the wrapper maps legend dimensions before grid offsets and
clamps saved percentage grid values against the current card pixels so stacked
bar legends keep reserved space after resize. Donut slice labels never repeat
the category name — that stays in the legend and the ring carries the value —
and they are shown or hidden by the
Show labelstoggle (configAsType.label.show), not by whether a legend is visible. Pie legend vertical alignment applies only to left/right legends, so old saved vertical-align values do not move the default bottom legend into the chart body. Compact bar and line charts preserve their configured value-label visibility instead of hiding labels when the card height crosses a responsive breakpoint.
Installation
pnpm add @masterteam/dashboard-builderUsage
import { Component } from "@angular/core";
import { DashboardBuilder } from "@masterteam/dashboard-builder";
@Component({
imports: [DashboardBuilder],
template: ` <mt-dashboard-builder [services]="['pplus', 'splus']" (onSave)="saveDashboard()" /> `,
})
export class MyComponent {
saveDashboard(): void {
// React to a completed explicit save.
}
}Metadata Contract
Hosts must provide the dashboard metadata endpoints used by
DashboardBuilderService:
GET metadata/workspacesreturns workspace options:WorkspaceOption { id: string | number; name: string }.GET metadata/modules/treeacceptsservices,workspaceId, andincludeValues. Returned module groups, items, and values may includeworkspaceIdfor client-side filtering.POST metadata/modules/propertiesacceptsitemsand optionalworkspaceId. Each request item may also includeworkspaceId.GET Modules?LevelId={levelId}backs the Status filter's Log select. It returns the product module catalog withisActivemarking which modules are attached to that level; the builder keeps the active entries whoseclassificationisLog(the enum name or the ordinal0). Hosts that already own this list can override it with[levelLogs]onManageFilterOnPage/DynamicFiltersConfig.
When a workspace changes, the builder clears selections and source links that no longer belong to the selected workspace.
A source link's "Selection ↔ Selection" / "From Previous Link" choice is not
persisted — it is derived from the saved row (a chained row is one with a
sourceLinkId and no source1SelectionId). The builder writes a chained row as
the backend's sourceLinkId, so toFormSourceLink must read that key back; it
also accepts the sourceLinkId2 encoding the backend's context-navigation path
builder emits, mirrored onto side 1 with the link properties swapped. Losing
either key on the round trip resets the tab to "Selection ↔ Selection" as soon
as a previous link is picked, because the host feeds every edit back through
[ngModel] and a value the form did not emit rebuilds the whole form array.
Table Query Editing Contract
The generic Table query editor persists ordered selectedProperties,
PivotProperties, AggregationProperties, and an optional lookupProperty.
Its ControlValueAccessor treats writeValue as silent model-to-view
synchronization: an equivalent value is ignored, and aggregation controls are
rebuilt with emitEvent: false. This is required because DataSourceSettings
immediately feeds each ngModelChange back into the editor; emitting during
that echo creates a synchronous loop that freezes the Properties picker
(#1284).
Snapshot Query Contract
Snapshot charts distinguish the selected Level schema from the concrete Level record whose history is requested:
interface SnapshotQuery {
workspaceSelector?: string | null;
levelId: number; // schema id used to load record options
levelDataId: number; // record id sent to GetSnapShotInfo
snapshotTargetVersion: 2; // prevents schema ids from using the legacy fallback
timeFrame: "yearly" | "monthly" | "weekly";
year: number | "{{currentYear}}";
getAveragePerPeriod?: boolean;
selectedProperties: string[];
}After a Level is selected, the builder requests its records through
POST fetch/query with:
{
contextKey: `level:${levelId}`;
projection: "Card";
display: {
areas: ["card"];
}
}This authoring request deliberately omits includeState. Record options map
directly from data.records[].id/name. Because fetch/query is owned by the
application API, the request carries REQUEST_CONTEXT { useBaseUrl: false };
gateway metadata requests continue to use useBaseUrl: true. This keeps the
record picker on the active application's API base instead of sending it to the
Gateway origin (#1668). Runtime calls
POST dashboards/{apiVersion}GetSnapShotInfo/{levelDataId}/{timeFrame}?year={year}
with { AvgPerPeriod, dashboardId }; the schema levelId must not be used in
that URL. Supported selected-property
keys are plannedProgress, actualProgress, budget, baselineProgress,
spent, remaining, and paymentPlan.
Axis Label Wrapping Contract
Bar and stacked-bar category labels preserve their original casing and wrap
normal spaces, underscores, existing line breaks, and single tokens that exceed
the selected character limit. Runtime keeps all generated lines visible and
sets hideOverlap: false; the shared ECharts responsive layout still owns the
available label width and grid containment.
The persisted controls live under clientConfig.barConfigOverride and
clientConfig.stackBarConfigOverride:
{
axisLabelWrapMode: "characters" | "words" | "none";
maxCharsPerLine?: 8 | 12 | 16 | 20 | 24 | 30 | 40;
maxWordsPerLine?: 1 | 2 | 3 | 4 | 5 | 6;
}Missing mode values use character wrapping, so old dashboards that only saved
maxCharsPerLine remain compatible. The default is 20 characters per line;
word mode defaults to three words per line. none normalizes separators but
keeps the label on one line.
Host Currency Settings Contract
Dashboard currency labels, tooltips, and values read the host's canonical
systemSettings.settings local-storage entry. The stored value must be a
JSON-encoded array containing a case-sensitive Currency key whose value
provides the currency code, localized units, and numeric format:
localStorage.setItem(
"systemSettings.settings",
JSON.stringify([
{
key: "Currency",
value: {
name: "OMR",
unit: {
en: "OMR",
ar: "ريال عماني",
},
format: "{0:N3}",
},
},
]),
);Runtime currency precedence is:
- The valid
Currencyentry insystemSettings.settings. - The legacy JSON object stored under
currency(itsunitstring). - The built-in SAR unit and legacy number formatting.
Arabic dashboards (including regional language codes such as ar-SA) use
value.unit.ar; every other language uses value.unit.en. Supported system
formats include decimal/grouping patterns such as {0:#,0.00}, compact
thousands such as {0:#,0, K}, and named decimal patterns such as {0:N3}.
The host currency format is authoritative; a chart's legacy decimal option does
not override its decimal count.
The showcase's opt-in offline dashboard fixture
(showcase-mock-dashboard-builder=1) appends the OMR example above only when
the settings array has no Currency entry, so an existing host choice remains
authoritative.
PhaseGate Table Query Contract
2-table-view does not use the generic table pivot/aggregation query. Its
canonical service query is:
{
rowIdentityProperty: string;
phaseNameProperty: string;
phaseIdProperty: string;
phaseOrderProperty: string;
phaseStatusProperty: string;
phaseProgressProperty: string;
phaseIsCurrentProperty: string;
phaseIsCompletedProperty: string;
phaseAllItemsCompletedProperty: string;
baseProperties: string[];
properties: string[];
selectedProperties: string[];
}baseProperties is the ordered multi-select of visible business columns. Both
properties and selectedProperties are rebuilt before save/request as the
same deduplicated union of those columns and every non-empty semantic role, so
the chart service receives all fields needed by the renderer.
The runtime resolves configured roles by property identity, matching the row
object key or wrapper key, normalizedKey, label, or name without
depending on response order. Consequently a visible project Status and the
phase Status can coexist safely. Dynamic phase headers use the configured
phase property's runtime value rather than its schema/metadata label, and an
empty phaseProgressProperty does not render a zero-value progress track.
Legacy generic-table configs are still read;
lookupProperty becomes the phase-status role, with conservative runtime
fallbacks until the remaining roles are configured and saved.
Phase columns follow phaseOrderProperty when it is set; without it they keep
the order the rows arrive in. The backend does not return phases in phase
order, and the PhaseGate module currently has no order property to bind, so a
table with no order role shows the backend's row order.
Phase Gate Stepper Contract
phase-gate-stepper-card is a TableView chart request (requestType:
'custom') over one selection — the PhaseGate module (Module:<id>) — and
uses the PhaseGate Table query contract above. Its editor sets these roles:
| Role | Required | Example key |
| --- | --- | --- |
| rowIdentityProperty — the record the phases belong to | yes | phasegate_level_data_id |
| phaseNameProperty | yes | phasegate_name |
| phaseOrderProperty | yes | phasegate_order |
| phaseIsCurrentProperty | yes | phasegate_is_current |
| phaseIsCompletedProperty | no | phasegate_is_completed |
The stepper draws exactly one record, so the selection needs a condition on
the record's level data id: a literal id, or a page-filter {{token}}. Steps
are sorted by the order role because the backend does not return phases in
phase order. Without a completion role, the phases before the current one show
as completed. When the rows span more than one record — no condition, or an
empty page filter that drops it — the widget asks for a single record instead
of mixing phases.
Steppers saved with the old { levelId } query are migrated on load:
requestType phaseGate → custom, chartType phaseGate → TableView,
and query.levelId is removed. They then show the Configure CTA until a data
source is chosen.
Map Chart Contract
The Map Chart widget (componentName: 'map', functionName:
'handleLeafletMap') is rendered by mt-map-view.
MapChartHandler.handleLeafletMap resolves each record's configured location
property into { lat, lon } and buckets the records by the grouping property,
returning:
Array<{
groupName: string;
color: string;
points: Array<{ displayName; lat; lon; color; extraValues }>;
}>;That array is truthy and has no noData flag, so it must not reach
mt-echart directly — chart-card routes it to the map branch, and
mt-map-view turns it into a geo + scatter option.
Boundaries are bundled, not fetched. The region outlines are GeoJSON shipped
with the package and registered with echarts.registerMap, the same approach
the legacy dashboardBuilder app used. Nothing is requested at runtime: there is
no tile server, no API key, no third-party usage policy to comply with, and the
widget renders identically offline or behind a firewall. No record data leaves
the browser.
The GeoJSON files are a few hundred KB, so MAP_REGIONS lazy-loads them —
only a dashboard that actually contains a Map Chart pays for them. Registration
is async and global; the option stays null until it lands, because ECharts
renders an empty canvas with no error when handed a geo.map name it does not
know.
Boundary data is © OpenStreetMap contributors under the ODbL. The widget renders that attribution in its corner and it must not be removed.
Query. serviceConfig.query.locationProperty must point at a Location
property — the Query editor offers only Location properties for it, since any
other choice saves fine and then plots nothing. A record is plotted only when
its location value parses to finite coordinates within the world; everything
else is skipped silently.
groupingProperty is optional and colours the markers. With none set, every
record lands in one unnamed group and the map draws no legend. A record with no
grouping value is grouped as "Unknown" instead of being dropped (the backend
omits null properties from the row). The group name is the value's display text:
a Status's localized label, a User's displayName, a Lookup's localized
display, the names of a MultiUser joined with commas, or a plain value as-is.
Regions (MAP_SECTION writes the choice to clientConfig.map):
| Value | Registered as | Boundaries |
| --- | --- | --- |
| SaudiArabia (default) | Saudi | National + regional outline |
| Riyadh | RiyadhGovernorate | Riyadh Governorate |
RiyadhGovernorate is accepted as a persisted value too — it is the name the
legacy handler used. Anything unrecognised falls back to Saudi Arabia, including
the world option the Style Studio offered before the boundaries were bundled;
there is no world GeoJSON, so a config still holding it renders Saudi Arabia
rather than nothing.
Adding a region means adding its GeoJSON under map-view/geo/, an entry in
MAP_REGIONS (with the center and zoom that frame it in a dashboard
card) and an option in MAP_SECTION.
Removed controls. Tile layer, marker clustering and zoom bounds used to
appear in the Style Studio's Map section. They described a raster-tile renderer
that was never built, so they were removed along with their control bindings.
Values already persisted under clientConfig.configAsType.mapChart.* are
ignored.
Legend and interaction. The widget's own legend controls (show,
position, iconType) drive the map's group legend, and the boundaries are
laid out in a box that reserves the legend's edge so the two never overlap.
Overlapping marker labels are dropped rather than stacked
(labelLayout.hideOverlap); the marker stays and its name is in the tooltip.
The pin is anchored by its tip (symbolOffset: [0, '-50%']), so the point of
the pin, not its middle, sits on the record's coordinate.
The map can be panned and zoomed (geo.roam). Clicking a marker emits
pointClick, which chart-card forwards as an onAction click
carrying the record — but only when the widget has actions configured.
Runtime Filter Contract
Dashboard filters have two independent scopes:
Page/general filter definitions live in the dashboard configuration's
filterscollection. The viewer supplies their applied values to every item; request templates consume the keys they reference.ignoreQueryFiltersuppresses ambient global query values but retains explicit viewer/dialog filter context for compatibility with saved pages. The viewer opens a default-closed shared MT drawer from its in-flow Manage Filters action, so the host does not need a fixed sidebar or overlay CSS. Apply commits the merged values to the runtime store synchronously, then mirrors them to the URL as the shareable source of truth. Every mounted item observes the forwarded query- param signal and requests data again when it changes, so Apply, Clear, browser navigation, and externally-authored query params update the dashboard without a page reload. The later route echo has the same serialized state and does not duplicate the request.Chart filter definitions live canonically in
clientConfig.dynamicFilters. LegacyclientConfig.extraFilters.dynamicFiltersis read and migrated when the chart is edited. Runtime values are stored per widget identity and override a page value with the same key for that widget only.Viewer
extraFiltersare host-owned context (for example, an informative workspace'scurrentId). They are merged after page and chart values, so a same-key runtime value cannot replace or clear the host's scope.The builder's manage canvas and chart configuration preview read the current route query parameters through the same runtime filter store. A change to the URL while manage mode is open refreshes mounted charts, including charts in groups. Its Preview toggle keeps that filter context; leaving the builder clears the route values from the shared store.
Builder and viewer hosts pass the same client-form metadata through
[lookups],[statuses], and[levelsSchema]. Lookup configuration labels are normalized from the host lookup names, runtime Status options accept canonical nested status definitions or legacy flat level statuses, and Status/Phase Gate schema configuration is derived from the shared hierarchical metadata tree'sLevelmodule.[levelsSchema]is the host's levels collection; runtime Phase Gate and Schema Settings options read it, and it replaces the legacypplus.settingslocalStorage cache for hosts that never wrote one.A Status filter's "Log" options are scoped to the row's selected level, so two Status filters on different levels never share one list. They resolve in this order:
The host
[levelLogsResolver], when one is registered. Hosts that already reach the module catalog through their own authenticated client should supply one — it removes every base-URL and permission unknown from the package's side. Without it the package requestsGET Modules?LevelId={levelId}itself. Either way the result is cached per level for the session, and a failed load is reported throughconsole.warnso a 401/403/404 is never mistaken for "no logs".Note that endpoint returns the WHOLE module catalog and marks the level's own modules with
isActiverather than filtering, so a level's log isisActive === trueandisLogClassification(classification)(ModuleClassificationEnum.Log;Processis not a log). That endpoint serializes the enum as its name ("Log"), while others emit the ordinal0— compare throughisLogClassification, never a bare=== 0, which silently drops every module and empties the dropdown.The level's own
logscollection, when the host levels schema carries one (the legacy PPlus4 settings contract).Status metadata: every definition scoped to that level that names a
moduleId. This derivation is the exact inverse of the runtime read (getStatuses(logId, 'Log', schemaLevelId)matches the level and module), so every offered option resolves to real status values. Labels come from the metadata tree'sModulevalues, falling back to the status property name.
Sources 2 and 3 need no request, so the select stays populated for hosts that cannot reach the
Modulesendpoint.Runtime Status values and saved-default validation use the same scope.
getStatuses(logId, 'Log', schemaLevelId)prefers the selected level's module definitions, including an explicitly empty definition; otherwise it uses only the module defaults (levelIdandlevelModuleIdunset). It never collects overrides from other levels. Without a Log, only level-owned definitions matchingschemaLevelIdapply; module and level-module ids are separate identities. Options are unique by statuskey(the submitted value), while distinct keys sharing a label remain selectable. Legacy flatbasicLevelsStatusesretain theirmodule+moduleIdscope (#1631).
Both drawers edit a private draft. Apply commits and refreshes the relevant
scope; closing without Apply discards the draft. Clear replaces only the keys
owned by that drawer's field set: optional values are removed, required
non-date values remain selected, and static chart conditions plus the other
runtime scope are preserved. false and 0 are valid applied values rather
than empty values. Drawer ownership follows the viewer/widget lifecycle, so an
open body-appended drawer is closed when its owner is destroyed and cannot
commit into a stale chart store.
Compatibility key mapping follows the original dashboard builder while keeping each scope's natural runtime types:
- page/query values serialize multi-selects as comma-delimited strings, users
as the id under
keyplus the display name under<key>Name, checkboxes as"true"/"false", and quarters under<key>Splus<key>E; - chart item-store values keep multi-selects as arrays, checkboxes as booleans,
users as
{ userId, displayName }objects (or arrays of those objects), and quarters as the combinedstart,endvalue under the base key; - Clear owns every compatibility key for its field: users own
keyand<key>Name, while quarters ownkey,<key>S, and<key>E.
Request substitution accepts both representations. Page values are merged first and a chart's typed item-store values win only for that widget.
The parent report id (pageId) is the request execution context. A chart's
serviceConfig.dashboardId remains its widget/store/cache identity and must not
replace the report id. Route query parameters, including a legitimate filter
named id, are never used as the report identity.
Dialog actions retain that report id and expose the merged page/chart context
both under parentFilter and as flat keys. This keeps current
{{parentFilter.year}} templates and legacy {{year}} templates compatible.
Compatibility Notes
- Saved selectors remain backward-compatible with legacy
moduleType/moduleIdvalues and comma-based module ids. - Existing multilingual title objects are preserved. Plain string titles are normalized to the active title object shape.
- Removing a persisted chart group unlinks its saved group container before clearing the local group metadata. Its child charts remain on the dashboard and the deleted empty group cannot return after Save and reload.
- Style Studio resolves Header and Topbar widgets without a
chartTypeIdthrough their registeredclientConfig.componentName. This selects their layout-specific controls (without chart legend, label, palette, or number formatting options) and previews draft styles before Apply. Cancel continues to discard the working draft. - Style Studio shares one localized section list between its rail and form;
missing optional label translations use readable fallbacks without console
warnings. Its isolated
mt-dashboard-itempreview usesuseCachedDataOnlyto render host-supplied data without refetching on every draft edit. Multi-level timeline previews reuse the processed resource hierarchy and render a bounded 16-resource sample, avoiding a second full Gantt layout while preserving live style feedback. Closing the studio clears its isolated store (#1632). - The Header and Topbar layout sections keep the appearance controls their own
renderers paint: background, text color, border color, radius, padding, font
size, and — once an icon is picked — icon color and icon background. Dropping
the generic Card style section from the layout schema must not drop these; a
header with no background control is issue #1556. Background writes the
canonical
configAsType.cardStyleConfig.backgroundColorand mirrors into the legacydisplayConfig.StyleConfig.background-colorthat the retired "Topbar background" control wrote, so topbars saved earlier keep their color. - Style Studio preview/runtime parity includes Header and Topbar icon placement,
bar and stacked-bar widths,
configured number precision and explicit currency visibility, pie legend
type/font controls, bilingual donut center labels, table/timeline legend
position and icon shape, and statistic-card icon placement. Stacked-bar
totals are emitted only when
configAsType.label.showTotalInTopis true; the generic data-label toggle cannot override that dedicated setting. - Properties Card and legacy Level Cards use
mt-card-contentfor their outer title and card appearance. The savedclientConfig.title,configAsType.headerCardConfig,displayConfig.StyleConfig,configAsType.cardStyleConfig, density, and padding now render through the same path as other cards (#1666). Style Studio now edits each selected property's label and appearance together in one Property settings row: width, color source, border, hide name, label position, alignment, optional English/Arabic label and info, and User display fields. The label map remains keyed by property atconfigAsType.overrideSelectedPropertiesLabels; layout stays positional inpropsConfigAsIndex. The redundant Hidden control is not offered for these cards; legacy stored values remain intact. Rows are collapsed until opened, optional text editors mount only when enabled, and unknown legacy keys survive edits. All changes remain drafts until Apply. Advanced mode opens without constructing the fullclientConfigJSON editor; that escape hatch is available on demand in its own section. - Performance Gauge Card treats the
Statuswrapper returned invalues.*.extraValuesas authoritative for the active status key, localized label, and color. Component status ranges still define the gauge scale and segments, and remain the fallback when the response has no status wrapper. - The builder does not show save toasts directly. Save response messages belong
to the host HTTP interceptor; automatic/internal save requests are sent with
noMessage: true. - Aggregation requests normalize legacy
DisticntCountvalues toDistinctCount; backends should accept both names and execute distinct-count semantics. - Table-card status legends support both grouped and flat label payloads,
display legacy
labeltext alongside the color indicator, and expose visibility plus per-entry order/color controls in Style Studio for Table, Dialog Table, and PhaseGate Table items. - Table and Dialog Table user cells resolve the canonical
photoUrlvalue before the legacyimageandavataraliases, then load the avatar through the shared authenticatedsecureImagepipeline instead of binding the backend value directly to<img src>. - Manage Item filter value fields treat properties whose
normalizedKeyisIdas literal identifiers. They do not call the finite-listproperty-itemsendpoint for those properties; lookup and status properties continue loading their selectable values normally. Checkbox/boolean properties synthesize localized Yes/No values locally. Existing filters may restore their property key before async property metadata arrives; the field reclassifies that key when metadata resolves and ignores any obsolete remote response, so edit mode retains the boolean Values tab (#1237). ChartDataService.clearCache(dashboardId?)clearsLimitedHttpServicecache entries by chart item id (serviceConfig.dashboardId) when an id is passed, or clears all chart responses when omitted. Scoped clears also bypass matching in-flight requests so the next fetch cannot reuse stale data after a host create/edit/delete action.DashboardViewer.reloadPage()is a host-facing mutation refresh API: it clears the dashboard chart HTTP cache before triggering all chart items, so calls such asviewer()?.reloadPage()always send fresh backend requests. Items that have not come near the visible area yet stay pending (below) and request once, with the state current at that moment.- Viewer items request their data only once they come within 300px of the
visible area, measured against their nearest vertically scrolling ancestor
(or the viewport). Until then they keep the loading skeleton; every request
that arrives meanwhile — page filters,
reloadPage(), config updates — collapses into one, sent when the item is reached. A page therefore opens with the requests of what is on screen, not of every chart at once. The builder canvas always loads immediately. Items that never request data (groups, headers, unconfigured widgets) render without waiting. Hosts that need every item loaded up front (print, export) provide{ provide: DASHBOARD_ITEM_VISIBILITY, useValue: loadImmediately }. - Page-filter defaults that the viewer writes into the URL on load replace the current history entry instead of pushing one, so Back leaves the dashboard rather than first stepping to the same URL without its defaults. Filters the user applies still push an entry each.
- Grouped bars place each bar by its category key, not its position. The axis
is the response's
categoriesin their order, plus any category that has bars but no label (e.g. "NA"); a bar type a category lacks leaves an empty slot. Each data point carries the stacked bar's click keys:key(bar type),xAxisKey/xAxisKeyFilter(category key) andtitle(category label). - Ring gauges clip each ring at the end of the 0–100 scale; the value label
still shows the real number. A response with no row returns
noData: true, so the card shows its empty state instead of a bare gauge. - The Gauge Chart's "Show progress ring" (
configAsType.showGaugeProgress) is opt-in: onlytruedraws the ring, so gauges saved without it look as they always did. - Table cells read Checkbox values the backend sends as text (
"True"/"False"), so a false value shows "No".
Development
# Build only the dashboard-builder package
pnpm build:db
# Focused tests for changed specs
pnpm --filter @masterteam/dashboard-builder test -- --watch=false --include=<path>.spec.tsDo not use the full @masterteam/components build for routine component work
unless the caller explicitly approves it.
License
MIT
