diff --git a/src/personal_mcp/docs/skills/nicegui/SKILL.md b/src/personal_mcp/docs/skills/nicegui/SKILL.md index 998ed24..1a4fee2 100644 --- a/src/personal_mcp/docs/skills/nicegui/SKILL.md +++ b/src/personal_mcp/docs/skills/nicegui/SKILL.md @@ -11,6 +11,7 @@ Use this skill as a progressive reference for NiceGUI applications built with Fa - Planning or reviewing NiceGUI application structure and FastAPI composition. - Building or refactoring pages, components, layouts, and static assets. +- Creating editable tables with Python-authoritative state, validation, and persistence. - Modeling UI state with bindings or bindable dataclasses. - Implementing forms, uploads, refreshes, live updates, or background work. - Diagnosing UI state, concurrency, navigation, or asset problems. @@ -71,6 +72,16 @@ Load [component mechanics and customization](./references/component-mechanics-an - `ui.select` and `ui.icon` mechanics and caveats - dialog scaling when detached popup geometry must be preserved +### Editable Tables + +Load [editable tables](./references/tables.md) for: + +- Python-authoritative editable `ui.table` state +- stable row identity across sorting, filtering, and pagination +- NiceGUI editors in Quasar `body-cell-*` scoped slots +- validation, persistence, rejection, and canonical row refresh +- the full `body` slot required when escalating to `QPopupEdit` + ### Bindable State Load [bindable dataclasses](./references/binding-dataclasses.md) for: @@ -118,7 +129,8 @@ Load [source documentation](./references/source-documentation.md) when: 1. Load [application architecture](./references/architecture.md) for page and component ownership decisions. 2. Load [styling and customization](./references/styling-and-customization.md) for themes, layout, responsive presentation, utility classes, or CSS. 3. Load [component mechanics and customization](./references/component-mechanics-and-customization.md) when behavior must be mapped across NiceGUI, Quasar, and Vue, or when detached content and component-specific behavior are involved. -4. Add [interaction patterns](./references/interaction-patterns.md) or [bindable dataclasses](./references/binding-dataclasses.md) according to the page behavior. +4. Load [editable tables](./references/tables.md) when table cells accept user changes or `QPopupEdit` is being considered. +5. Add [interaction patterns](./references/interaction-patterns.md) or [bindable dataclasses](./references/binding-dataclasses.md) according to the page behavior. ### Debugging Or Production Review @@ -133,6 +145,7 @@ Load [source documentation](./references/source-documentation.md) when: - Avoid blocking I/O and CPU-heavy work in the UI event loop. - Prefer event-driven updates and explicit refreshes over unrelated polling. - Discover component capabilities through NiceGUI docs and constructors, then the wrapped Quasar API. +- Keep editable table records authoritative in Python; send stable row keys with edit proposals and reassert canonical rows after validation. - Research the current NiceGUI and Quasar source documentation before generating component-specific code or CSS. - Prefer constructor arguments and native Quasar features through NiceGUI; use Tailwind for structure and scoped static CSS for stable fine tuning. - Provide loading, success, and failure states for user-triggered work. diff --git a/src/personal_mcp/docs/skills/nicegui/references/tables.md b/src/personal_mcp/docs/skills/nicegui/references/tables.md new file mode 100644 index 0000000..343cf9b --- /dev/null +++ b/src/personal_mcp/docs/skills/nicegui/references/tables.md @@ -0,0 +1,255 @@ +# Editable Tables + +Use this reference when a `ui.table` must accept cell edits while Python remains the authoritative owner of row state. Start with NiceGUI elements in named QTable cell slots. Escalate to raw Quasar row templates only when a requirement, such as `QPopupEdit`, cannot work in a cell slot. + +## Version Baseline + +This reference was verified against the latest released NiceGUI stack at the time of research: + +| Layer | Version | Version evidence | +| --- | --- | --- | +| NiceGUI | `3.16.0` | [NiceGUI `v3.16.0` release](https://github.com/zauberzeug/nicegui/releases/tag/v3.16.0) | +| Quasar | `2.18.5` | [NiceGUI `v3.16.0` frontend dependencies](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json) | +| Vue | `3.5.22` | [NiceGUI `v3.16.0` frontend dependencies](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json) | + +Recheck the dependency manifest and tagged sources when the target application uses another NiceGUI release. Do not infer the Quasar or Vue version from their latest independent releases; use the versions bundled by NiceGUI. + +## Ownership Model + +Treat an edit as a proposal, not a browser-side state mutation: + +1. Python projects canonical records into `table.rows`. +2. A NiceGUI editor displays `props.value` from a QTable scoped slot. +3. The editor emits stable row identity, the field name, and the proposed value. +4. Python locates the canonical record, validates and coerces the proposal, and persists the accepted record. +5. Python sends a fresh canonical projection back with `table.update_rows(...)` whether the proposal was accepted or rejected. + +```mermaid +flowchart LR + A[Python records] -->|project rows| B[QTable] + B -->|props.value| C[NiceGUI editor] + C -->|row key, field, proposed value| D[Python handler] + D --> E{validate} + E -->|accept| F[persist] + E -->|reject| G[notify] + F --> A + G --> A +``` + +The browser may hold temporary editor state, but it is never the source of truth. Do not mutate `props.row` and mistake Vue reactivity for persistence. Do not use a visual row index as identity: sorting, filtering, and pagination can all change it. Set `row_key` to an immutable, unique field and send that value with every edit proposal. + +## Recommended Cell-Slot Pattern + +[NiceGUI `ui.table`](https://nicegui.io/documentation/table) supports NiceGUI elements in scoped slots since `3.5.0`. The tagged [`Table.cell` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.py) creates the corresponding Quasar `QTd`, while the tagged [table client component](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.js) forwards QTable's scoped slot props. + +The following example keeps canonical records in Python, uses a stable `id`, allows only known fields, validates every proposed value, and reasserts canonical rows after each event. `save_item` is the persistence boundary; replace its in-memory assignment with the application's service or repository call. + +```python +from dataclasses import dataclass, replace + +from nicegui import events, ui + + +@dataclass(frozen=True, slots=True) +class Item: + id: int + name: str + quantity: int + status: str + + +STATUS_OPTIONS = ["draft", "active", "archived"] +items_by_id = { + 101: Item(id=101, name="Desk", quantity=4, status="active"), + 102: Item(id=102, name="Lamp", quantity=12, status="draft"), +} + + +def canonical_rows() -> list[dict[str, str | int]]: + return [ + { + "id": item.id, + "name": item.name, + "quantity": item.quantity, + "status": item.status, + } + for item in items_by_id.values() + ] + + +def validate_edit(item: Item, field: str, raw_value: object) -> Item: + match field: + case "name": + if not isinstance(raw_value, str) or not (name := raw_value.strip()): + raise ValueError("Name is required") + return replace(item, name=name) + case "quantity": + if isinstance(raw_value, bool) or not isinstance(raw_value, (int, float, str)): + raise ValueError("Quantity must be an integer") + if isinstance(raw_value, float) and not raw_value.is_integer(): + raise ValueError("Quantity must be an integer") + try: + quantity = int(raw_value) + except (ValueError, OverflowError) as error: + raise ValueError("Quantity must be an integer") from error + if not 0 <= quantity <= 1_000: + raise ValueError("Quantity must be between 0 and 1000") + return replace(item, quantity=quantity) + case "status": + if not isinstance(raw_value, str) or raw_value not in STATUS_OPTIONS: + raise ValueError("Unknown status") + return replace(item, status=raw_value) + case _: + raise ValueError(f"Field {field!r} is not editable") + + +def save_item(item: Item) -> None: + items_by_id[item.id] = item + + +def apply_edit(event: events.GenericEventArguments) -> None: + try: + raw_row_id, raw_field, raw_value = event.args + row_id = int(raw_row_id) + field = str(raw_field) + current = items_by_id.get(row_id) + if current is None: + raise ValueError("This row no longer exists") + save_item(validate_edit(current, field, raw_value)) + except (TypeError, ValueError) as error: + ui.notify(str(error), type="negative") + finally: + table.update_rows(canonical_rows(), clear_selection=False) + + +columns = [ + {"name": "name", "label": "Name", "field": "name", "align": "left"}, + {"name": "quantity", "label": "Quantity", "field": "quantity", "align": "right"}, + {"name": "status", "label": "Status", "field": "status", "align": "left"}, +] +table = ui.table( + columns=columns, + rows=canonical_rows(), + row_key="id", + selection="multiple", +).classes("w-full") + +with table.add_slot("body-cell-name"): + with table.cell("name"): + ui.input().props( + ':model-value="props.value" dense borderless debounce=400' + ).on( + "update:model-value", + handler=apply_edit, + js_handler="(e) => emit(props.row.id, props.col.name, e.value)", + ) + +with table.add_slot("body-cell-quantity"): + with table.cell("quantity"): + ui.number(min=0, max=1_000).props( + ':model-value="props.value" dense borderless debounce=400' + ).on( + "update:model-value", + handler=apply_edit, + js_handler="(e) => emit(props.row.id, props.col.name, e.value)", + ) + +with table.add_slot("body-cell-status"): + with table.cell("status"): + ui.select(STATUS_OPTIONS).props( + ':model-value="props.value" dense borderless options-dense' + ).on( + "update:model-value", + handler=apply_edit, + js_handler="(e) => emit(props.row.id, props.col.name, e.value)", + ) + +ui.run() +``` + +This uses the same transformed-event path documented by [NiceGUI's table selection example](https://nicegui.io/documentation/table): `.on("update:model-value", ...)` attaches directly to the editor, and `js_handler` emits only the serializable values Python needs. Vue component events [do not bubble](https://vuejs.org/guide/components/events.html), so listening on the table or cell instead of the editor will not capture the editor's model update. + +The `:model-value="props.value"` prop is deliberately one-way. In Vue, component `v-model` expands to a `modelValue` prop plus an `update:modelValue` listener, as shown in the [Vue component `v-model` guide](https://vuejs.org/guide/components/v-model.html) and its tagged [compiler transform](https://github.com/vuejs/core/blob/v3.5.22/packages/compiler-core/src/transforms/vModel.ts). Here the update listener sends an intent to Python rather than assigning into `props.row`. + +## Commit Policy + +Choose when edits cross the client-server boundary according to the editor: + +- Use `update:model-value` for discrete editors such as `ui.select`, switches, and checkboxes. +- For text and numeric inputs, use Quasar's documented `debounce` prop when accepting edits during typing. A trailing delay avoids one server round trip per keystroke. +- When the user must explicitly save or cancel a multi-field draft, keep the draft in a dialog or popup and emit one proposal on save. Python must still validate and reassert the canonical row. +- For asynchronous persistence, disable or mark the affected editor busy while saving. Add an entity version or other optimistic concurrency check when multiple clients can edit the same record. + +Do not rely on browser validation alone. Quasar editor constraints improve feedback, but the event payload is still untrusted input. The Python handler must enforce the editable-field allowlist, types, ranges, permissions, record existence, and persistence constraints. + +## Persistence And Refresh + +Keep `table.rows` as a projection, not the business model. On acceptance: + +1. validate and coerce into domain types +2. call the service or repository that owns persistence +3. fetch or derive the resulting canonical records +4. call `table.update_rows(canonical_rows(), clear_selection=False)` + +On rejection, perform step 4 anyway. This overwrites any temporary editor display with the last accepted value. Preserve selection only when the selected row identities remain valid; otherwise use the default `clear_selection=True`. + +For database-backed applications, make the handler `async`, await the service transaction, and refresh only after it commits. Catch the application's expected validation, conflict, and persistence exceptions separately so the user receives actionable feedback without hiding programming errors. + +## QTable And QPopupEdit Escalation + +The underlying [Quasar QTable guide](https://quasar.dev/vue-components/table) and tagged [`QTable` source](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTable.js) define the `body-cell-[name]` props used above, including `row`, `col`, `value`, and the key derived from `row-key`. + +Use [Quasar `QPopupEdit`](https://quasar.dev/vue-components/popup-edit) only when its local draft, validation, save, and cancel interaction is specifically required. Quasar documents that `QPopupEdit` does not work in QTable cell scoped slots; it must be placed under the full `body` slot. Its tagged [source implementation](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/popup-edit/QPopupEdit.js) keeps a cloned draft and emits `save` and `update:modelValue` only after validation. + +That restriction changes the implementation boundary: a full `body` slot must render every `QTr` and `QTd`, preserve QTable's scoped props and row keys, and host the popup. Before taking this path: + +1. confirm an ordinary NiceGUI editor or dialog cannot meet the interaction requirement +2. copy the row structure from the matching Quasar `2.18.5` QTable documentation, not another version +3. keep popup draft state local rather than assigning into `props.row` +4. emit the stable row key, field, and saved proposal to Python +5. validate, persist, and replace the table rows from Python exactly as in the cell-slot pattern +6. test keyboard focus, save, cancel, validation failure, sorting, filtering, pagination, and selection + +Replacing the full row template has a larger maintenance and accessibility surface. Keep the named cell-slot implementation as the default. + +## Source Map + +### NiceGUI `3.16.0` + +- [Table developer documentation](https://nicegui.io/documentation/table) +- [`Table` Python source](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.py) +- [QTable client wrapper source](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/table.js) +- [Pinned frontend dependency manifest](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json) + +### Quasar `2.18.5` + +- [QTable developer documentation](https://quasar.dev/vue-components/table) +- [`QTable` source](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTable.js) +- [`QTable` API definition](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/table/QTable.json) +- [QPopupEdit developer documentation](https://quasar.dev/vue-components/popup-edit) +- [`QPopupEdit` source](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/popup-edit/QPopupEdit.js) +- [`QPopupEdit` API definition](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/popup-edit/QPopupEdit.json) + +### Vue `3.5.22` + +- [Component `v-model`](https://vuejs.org/guide/components/v-model.html) +- [Component events](https://vuejs.org/guide/components/events.html) +- [Scoped slots](https://vuejs.org/guide/components/slots.html#scoped-slots) +- [`v-model` compiler transform](https://github.com/vuejs/core/blob/v3.5.22/packages/compiler-core/src/transforms/vModel.ts) +- [Component event runtime](https://github.com/vuejs/core/blob/v3.5.22/packages/runtime-core/src/componentEmits.ts) +- [Native `v-model` directives](https://github.com/vuejs/core/blob/v3.5.22/packages/runtime-dom/src/directives/vModel.ts) + +## Completion Check + +Before accepting an editable table: + +1. Pin the NiceGUI release and verify its bundled Quasar and Vue versions. +2. Use an immutable, unique `row_key`; never persist by view index. +3. Bind each editor's displayed value from QTable scoped props. +4. Attach the event listener directly to the editor and emit only row identity, field, and proposed value. +5. Validate field access, types, ranges, permissions, and record existence in Python. +6. Persist through the owning service or repository. +7. Reassert canonical rows after accepted and rejected proposals. +8. Test editing after sort, filter, pagination, and selection changes. +9. Test stale rows, invalid input, persistence failure, and concurrent edits. +10. Use a full `body` slot for `QPopupEdit`, never a `body-cell-*` slot. \ No newline at end of file