Skip to main content

js-features

End-to-end checklist for adding a front-end feature — new widget types, GridStack tiles, localStorage-persisted data (merge_deep/merge_layout whitelists), undo/redo snapshots, modal dialogs, and the layout-user-action event. Use when implementing a new dashboard feature rather than fixing an existing one.

Source facts

Repository
MarketSquare/robotframework-dashboard
Last source activity
October 5, 2026 at 21:22
Detected SKILL.md language
English
Stars
89
Forks
21

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
js-features
description
End-to-end checklist for adding a front-end feature — new widget types, GridStack tiles, localStorage-persisted data (merge_deep/merge_layout whitelists), undo/redo snapshots, modal dialogs, and the layout-user-action event. Use when implementing a new dashboard feature rather than fixing an existing one.
# JavaScript Feature Patterns This skill covers the recurring patterns used when adding new features to the dashboard front-end. Reading this file (plus the `coding-standards` skill for structure and style rules) is sufficient to implement most features without additional codebase exploration. --- ## 1. The "Custom Widget" Pattern (GridStack tile + localStorage + modal) Every custom widget type in the dashboard follows the same 6-part pattern. Existing examples: **stat widget** (`statwidgets.js`), **link widget** (`linkwidgets.js`) and **custom graph** (`customgraphs/widgets.js` + `customgraphs/builder.js`; delegated button listener instead of per-render wiring). > The add buttons now live in the section headers (`.add-<name>-header`, toggled in `setup_section_order`, opened from the delegated click handler in `setup_dashboard_section_layout_buttons`), not in add-tiles inside the grid. Chart instances of widgets with generated ids must not be kept on `window[canvasId]`: that resolves to the canvas element itself (named element access) — custom graphs keep theirs in a `Map`. ### Part 1 — New JS module (`js/<name>widgets.js`) Must contain: | Function | Description | |---|---| | `render_custom_<name>_widgets(gridStack, sectionKey, editMode)` | Filters `settings.<name>Widgets` by `section === sectionKey`, creates `grid-stack-item` elements with `data-gs-id="custom<Name>Widget-<id>"`, calls `gridStack.makeWidget(item)`. Reads saved positions from `settings.layouts[grid<Section>]`. | | `build_<name>_widget_html(widget, editMode)` | Returns the inner HTML string for one widget card. Includes a delete button only when `editMode === true`. | | `add_custom_<name>_widget(section, ...fields)` | Generates a random ID with `generate_id()`, builds the widget object, appends to `settings.<name>Widgets`, calls `set_local_storage_item('<name>Widgets', list)`. Returns the widget object. | | `remove_custom_<name>_widget(id)` | Filters `settings.<name>Widgets` to exclude `id`, persists. | | `render_add_<name>_widget_tile(gridStack, sectionKey)` | Adds a 2×2 non-resizable dashed-border "add" tile. Uses `data-gs-id="add<Name>WidgetTile-<section>"`. Click opens the modal. | | `wire_<name>_delete_buttons(gridStack, sectionKey)` | After `render_custom_<name>_widgets` in edit mode, wires `click` on all `.delete-custom-<name>-widget` buttons. | | `handle_delete_<name>_widget(id, gridStack)` | Calls `gridStack.removeWidget(el)`, then `remove_custom_<name>_widget(id)`, then `document.dispatchEvent(new CustomEvent("layout-user-action"))`. | | `setup_add_<name>_widget_modal()` | Populates all modal form elements, wires the confirm button (reads modal state → calls `add_custom_<name>_widget` → calls `grid.makeWidget` live → dispatches `layout-user-action` → closes modal). Called **once** from `setup_dashboard_section_layout_buttons()` in `layout.js`. | | `open_add_<name>_widget_modal(sectionKey)` | Stores `sectionKey` in `modal.dataset.pendingSection` then calls `bootstrap.Modal.getOrCreateInstance(modal).show()`. | **Import the new module from `layout.js`** — the bundler discovers it via the import graph from `main.js`. ### Part 2 — CSS (`css/components/03-widgets.css`) Required CSS classes: | Class | Description | |---|---| | `.add-<name>-widget-tile` | Dashed-border clickable tile (copy from `.add-stat-widget-tile`). | | `.add-<name>-widget-tile-inner` / `.add-<name>-widget-plus` / `.add-<name>-widget-tile-label` | Inner content layout (copy from stat equivalents). | | `.<name>-widget-inner` | `position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 0.25rem; height: 100%; overflow: hidden;` | | `.<name>-widget-title` (or label) | `margin: 0; font-weight: 600; font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: calc(100% - 20px);` | | `.delete-custom-<name>-widget` | `position: absolute; top: 5px; right: 5px; z-index: 1;` | | `.grid-stack-item-content:has(.<name>-widget-inner)` | `padding: 8px 12px;` (same as stat widget) | | h=1 compact row rules | Mirror the `gs-h="1"` and `data-gs-h="1"` row-layout overrides from stat widget. | ### Part 3 — localStorage whitelist (`js/localstorage.js`) Add the new key name to the `LOCAL_ONLY_SETTINGS` set that `merge_deep()` checks: ```js const LOCAL_ONLY_SETTINGS = new Set([ "layouts", "libraries", "theme", "filterProfiles", "statWidgets", "customSections", "linkWidgets", "customGraphs", "<name>Widgets", ]); ``` **Also** add the `data-gs-id` prefix to the `merge_layout()` allowlist in the same file, or saved widget positions will be silently dropped on every page load: ```js // In merge_layout(), inside the arr.filter(...) call: (typeof item.id === 'string' && item.id.startsWith('custom<Name>Widget-')) || ``` Without this second whitelist entry, `merge_layout` strips any layout position whose ID doesn't match a known graph — so widgets always revert to their default GridStack position after refresh. ### Part 4 — Undo/Redo snapshots (`js/layout.js`) Add `<name>Widgets` to **all three** snapshot/restore functions: ```js // capture_settings_snapshot() <name>Widgets: JSON.parse(JSON.stringify(settings.<name>Widgets || [])), // capture_dom_snapshot() <name>Widgets: JSON.parse(JSON.stringify(settings.<name>Widgets || [])), // apply_layout_snapshot(snapshot) set_local_storage_item('<name>Widgets', JSON.parse(JSON.stringify(snapshot.<name>Widgets || []))); ``` Missing any one of these three causes widgets to be lost or not properly undone. ### Part 5 — Render calls (`js/layout.js` → `setup_graph_order`) In the `setup_graph_order` function, immediately after the stat-widget block: ```js // Render custom <name> widgets for this section render_custom_<name>_widgets(window[grid], sectionKey, gridEditMode); if (gridEditMode) { wire_<name>_delete_buttons(window[grid], sectionKey); render_add_<name>_widget_tile(window[grid], sectionKey); } ``` This block runs for all sections **except "Compare"**. Unified section uses `sectionKey = "unified"` and `window[grid] = window.gridUnified`. ### Part 6 — Modal HTML (`templates/dashboard.html`) Add the modal `<div>` in the `<!-- modals -->` area, before `<!-- placeholder_javascript -->`. Use Bootstrap 5 modal structure (same as `#addStatWidgetModal`). Wire it via `setup_add_<name>_widget_modal()` called in `setup_dashboard_section_layout_buttons()`. --- ## 2. Section Key → GridStack Instance Mapping The `sectionKey` (lowercase string) maps to: | `sectionKey` | `window[grid]` | Layout key | |---|---|---| | `"run"` | `window.gridRun` | `settings.layouts.gridRun` | | `"suite"` | `window.gridSuite` | `settings.layouts.gridSuite` | | `"test"` | `window.gridTest` | `settings.layouts.gridTest` | | `"keyword"` | `window.gridKeyword` | `settings.layouts.gridKeyword` | | `"unified"` | `window.gridUnified` | `settings.layouts.gridUnified` | | `"compare"` | (excluded from custom widgets) | `settings.layouts.gridCompare` | The `window[grid]` pattern uses `"grid" + section.charAt(0).toUpperCase() + section.slice(1)`. --- ## 3. GridStack Item Lifecycle ### Creating an item ```js const item = document.createElement('div'); item.classList.add('grid-stack-item'); item.setAttribute('gs-w', w); item.setAttribute('gs-h', h); item.setAttribute('gs-min-w', 1); item.setAttribute('gs-min-h', 1); item.setAttribute('gs-max-w', 12); item.setAttribute('gs-max-h', 12); item.setAttribute('data-gs-id', 'myWidget-<id>'); // must be unique item.innerHTML = `<div class="grid-stack-item-content">${innerHtml}</div>`; gridStack.makeWidget(item); ``` ### Restoring saved position Before `makeWidget`, check `settings.layouts[gridId]` (a JSON string), parse it, find the entry whose `.id` matches `data-gs-id`, and set `gs-x`/`gs-y`/`gs-w`/`gs-h` from the saved entry. ### Removing an item ```js const el = gridStack.el.querySelector('[data-gs-id="myWidget-<id>"]'); if (el) gridStack.removeWidget(el); ``` ### Saving layout (automatic) `capture_dom_snapshot()` in `layout.js` reads `gridStack.engine.nodes` for all grids and builds a JSON string of `[{id, x, y, w, h}]` entries. This is called automatically on every drag/resize (`dragstop`/`resizestop`) and on every `layout-user-action` event. --- ## 4. The `layout-user-action` Event Dispatching `document.dispatchEvent(new CustomEvent("layout-user-action"))` after any state-changing operation: - Takes a DOM snapshot and pushes it onto the undo history stack. - Is the only mechanism that captures widget add/delete in undo history. - Must be dispatched after **both** the DOM change and the localStorage change are complete. --- ## 5. localStorage-Only Keys Pattern Some keys only exist in localStorage (not in the `settings` defaults object). They must be listed in `LOCAL_ONLY_SETTINGS` in `localstorage.js` or `merge_deep()` silently drops them on every page load. Current list: | Key | Module that owns it | |---|---| | `layouts` | `layout.js` | | `libraries` | keyword library toggles | | `theme` | theme switcher | | `filterProfiles` | `filter/profiles.js` | | `statWidgets` | `statwidgets.js` | | `linkWidgets` | `linkwidgets.js` | | `customSections` | `customsections.js` | | `customGraphs` | `customgraphs/widgets.js` | **Rule**: Any new key you persist with `set_local_storage_item` that is not in `settings` defaults must be added to this whitelist, or it will be deleted by `merge_deep` on the next page load. --- ## 6. Adding a New Settings Toggle / Persisted Value If the new value *is* in `settings` defaults (not localStorage-only): 1. Add it to the `settings` object in `js/variables/settings.js`. 2. Persist changes via `set_local_storage_item('path.to.key', value)`. 3. No whitelist change needed — it's in the defaults, so `merge_deep` handles it. --- ## 7. Color Pickers (Reusing Stat Widget Colors) The `STAT_WIDGET_COLORS` and `STAT_WIDGET_BG_COLORS` arrays from `js/variables/statwidgetdefs.js` can be reused by any widget type. Use `fill_color_picker(pickerEl, colors, defaultValue)` (or a local copy of it) to populate the buttons: ```js import { STAT_WIDGET_COLORS, STAT_WIDGET_BG_COLORS } from './variables/statwidgetdefs.js'; ``` The color CSS classes they reference (`.white-text`, `.blue-text`, `.blue-bg`, etc.) are defined in `css/components/03-widgets.css`, `css/components/04-status-colors.css` and `css/base.css`. --- ## 8. The `gridEditMode` Guard `gridEditMode` (in `js/variables/globals.js`) is `true` while the user is in Customize View mode. Use it to suppress interactive behaviour (e.g. link navigation) in edit mode: ```js if (gridEditMode) return; ``` Do not use an `<a>` tag as the outermost clickable wrapper for GridStack items — it interferes with drag. Use a `<div>` with a `data-href` attribute and a `click` event listener that checks `gridEditMode` before navigating. --- ## 9. Quick-Reference: Files to Touch for a New Widget Type | File | Change | |---|---|
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub