Wt Entity Creator
When to use this skill
When you need to add a new entity to the Wt web UI layer in ORE Studio. This skill guides you through creating all the necessary Wt components to display, edit, and manage a domain entity, following established patterns in the codebase.
Prerequisites:
- The domain type must already exist (see domain-type-creator skill)
- The service layer for the entity must exist in the relevant domain component
(e.g.,
ores.refdata,ores.iam)
How to use this skill
- Gather entity requirements (name, fields, features needed).
- Follow the detailed instructions to create components in order.
- Each phase ends with a PR checkpoint - raise PR, wait for review, merge.
- Create a fresh branch from main for the next phase (see feature-branch-manager).
- Build and test after each step within a phase.
PR Strategy
This skill is structured into three phases, each resulting in a separate PR. This keeps PRs reviewable and allows incremental integration.
| Phase | Steps | PR Title Template |
|---|---|---|
| 1 | Steps 1-2 | [wt] Add <Entity> list widget |
| 2 | Steps 3-4 | [wt] Add <Entity> dialog |
| 3 | Steps 5-6 | [wt] Integrate <Entity> into application |
After each PR is merged, use the feature-branch-manager skill to transition to the next phase. This ensures clean git history by creating fresh branches from main rather than rebasing.
Detailed instructions
The following sections describe the step-by-step process for creating a complete Wt entity.
Gather Requirements
Before starting, gather the following information:
- Entity name: The name of the entity (e.g.,
currency,country,account). - Component location: Which domain the entity belongs to (e.g.,
ores.refdata,ores.iam). - Service class: The service that provides CRUD operations (e.g.,
currency_service). - Display fields: Which fields to show in the list view table.
- Editable fields: Which fields can be edited in the detail dialog.
- Primary key: The unique identifier field (e.g.,
iso_code,id,alpha2_code). - Features needed:
[ ]List view with table[ ]Detail dialog (create/edit)[ ]Delete confirmation[ ]Field validation
Phase 1: List Widget
This phase creates the list widget that displays entities in a table. After completing Steps 1-2, raise a PR.
Suggested PR title: [wt] Add <Entity> list widget
Step 1: Create Data Structures
Create the data transfer structures for form binding and table display.
File locations
Add to or create: projects/ores.wt/include/ores.wt/app/<entity>_list_widget.hpp
Row structure for table display
struct <entity>_row { std::string primary_key; // e.g., iso_code, alpha2_code std::string name; // Add display fields int version = 0; };
This structure contains only the fields needed for table display, keeping the table lightweight.
Commit message
[wt] Add <entity>_row structure for table display Create row structure containing display fields for the <entity> list table.
Step 2: Create List Widget
Create the list widget showing entities in a table with toolbar actions.
File locations
- Header:
projects/ores.wt/include/ores.wt/app/<entity>_list_widget.hpp - Implementation:
projects/ores.wt/src/app/<entity>_list_widget.cpp
Header structure
#ifndef ORES_WT_<ENTITY>_LIST_WIDGET_HPP #define ORES_WT_<ENTITY>_LIST_WIDGET_HPP #include <string> #include <vector> #include <Wt/WContainerWidget.h> #include <Wt/WTable.h> #include <Wt/WText.h> #include <Wt/WPushButton.h> #include <Wt/WSignal.h> namespace ores::wt::app { struct <entity>_row { std::string primary_key; std::string name; // Add other display fields int version = 0; }; class <entity>_list_widget final : public Wt::WContainerWidget { public: <entity>_list_widget(); // Data population void set_<entity>s(const std::vector<<entity>_row>& entries); void refresh(); // Signals for parent to handle Wt::Signal<>& add_requested() { return add_requested_; } Wt::Signal<>& refresh_requested() { return refresh_requested_; } Wt::Signal<std::string>& edit_requested() { return edit_requested_; } Wt::Signal<std::string>& delete_requested() { return delete_requested_; } private: void setup_toolbar(); void setup_table(); void populate_table(); Wt::WTable* table_ = nullptr; Wt::WText* status_text_ = nullptr; std::vector<<entity>_row> entries_; Wt::Signal<> add_requested_; Wt::Signal<> refresh_requested_; Wt::Signal<std::string> edit_requested_; Wt::Signal<std::string> delete_requested_; }; } #endif
Implementation patterns
- Constructor setup sequence
<entity>_list_widget::<entity>_list_widget() { addStyleClass("container-fluid p-3"); setup_toolbar(); setup_table(); } - Toolbar setup
void <entity>_list_widget::setup_toolbar() { auto* toolbar = addWidget(std::make_unique<Wt::WContainerWidget>()); toolbar->addStyleClass("d-flex gap-2 mb-3 align-items-center"); // Add button auto* add_btn = toolbar->addWidget( std::make_unique<Wt::WPushButton>("Add <Entity>")); add_btn->addStyleClass("btn btn-primary"); add_btn->clicked().connect([this] { add_requested_.emit(); }); // Refresh button auto* refresh_btn = toolbar->addWidget( std::make_unique<Wt::WPushButton>("Refresh")); refresh_btn->addStyleClass("btn btn-secondary"); refresh_btn->clicked().connect([this] { refresh(); // Updates status text to "Refreshing..." refresh_requested_.emit(); // Signal parent to reload data }); // Status text status_text_ = toolbar->addWidget(std::make_unique<Wt::WText>()); status_text_->addStyleClass("text-muted ms-auto"); }
- Table setup
void <entity>_list_widget::setup_table() { table_ = addWidget(std::make_unique<Wt::WTable>()); table_->addStyleClass("table table-striped table-hover"); table_->setHeaderCount(1); // Header row int col = 0; table_->elementAt(0, col++)->addWidget( std::make_unique<Wt::WText>("Primary Key")); table_->elementAt(0, col++)->addWidget( std::make_unique<Wt::WText>("Name")); // Add other column headers table_->elementAt(0, col++)->addWidget( std::make_unique<Wt::WText>("Version")); table_->elementAt(0, col++)->addWidget( std::make_unique<Wt::WText>("Actions")); // Make header sticky for (int i = 0; i < col; ++i) { table_->elementAt(0, i)->addStyleClass("sticky-top bg-light"); } }
- Table population
void <entity>_list_widget::populate_table() { // Clear existing rows (keep header) while (table_->rowCount() > 1) { table_->deleteRow(1); } for (const auto& entry : entries_) { int row = table_->rowCount(); int col = 0; table_->elementAt(row, col++)->addWidget( std::make_unique<Wt::WText>(entry.primary_key)); table_->elementAt(row, col++)->addWidget( std::make_unique<Wt::WText>(entry.name)); // Add other fields table_->elementAt(row, col++)->addWidget( std::make_unique<Wt::WText>(std::to_string(entry.version))); // Actions cell auto* actions = table_->elementAt(row, col++); actions->addStyleClass("d-flex gap-1"); auto* edit_btn = actions->addWidget( std::make_unique<Wt::WPushButton>("Edit")); edit_btn->addStyleClass("btn btn-sm btn-outline-primary"); edit_btn->clicked().connect([this, pk = entry.primary_key] { edit_requested_.emit(pk); }); auto* delete_btn = actions->addWidget( std::make_unique<Wt::WPushButton>("Delete")); delete_btn->addStyleClass("btn btn-sm btn-outline-danger"); delete_btn->clicked().connect([this, pk = entry.primary_key] { delete_requested_.emit(pk); }); } status_text_->setText(std::to_string(entries_.size()) + " <entity>(s)"); } void <entity>_list_widget::set_<entity>s( const std::vector<<entity>_row>& entries) { entries_ = entries; populate_table(); } void <entity>_list_widget::refresh() { status_text_->setText("Refreshing..."); }
CMakeLists.txt update
Add the new source file to projects/ores.wt/CMakeLists.txt:
set(ores_wt_sources
# ... existing sources
src/app/<entity>_list_widget.cpp
)
Commit message
[wt] Add <entity>_list_widget for <entity> list view Create list widget with table, toolbar actions (add, refresh), and row actions (edit, delete).
Phase 1 Checkpoint: Raise PR
At this point:
- Build and verify:
cmake --build --preset linux-clang-debug - Verify the widget compiles correctly.
- Commit all changes.
- Push branch and raise PR.
PR Title: [wt] Add <Entity> list widget
PR Description:
## Summary - Add <entity>_row structure for table display - Add <entity>_list_widget with table and toolbar
Wait for review feedback and merge before continuing to Phase 2.
Phase 2: Detail Dialog
After Phase 1 PR is merged, use feature-branch-manager to transition to Phase 2.
Suggested PR title: [wt] Add <Entity> dialog
Step 3: Create Data Structure for Form
Create the data structure for form binding.
File location
Add to: projects/ores.wt/include/ores.wt/app/<entity>_dialog.hpp
Data structure for form binding
struct <entity>_data { std::string primary_key; std::string name; // Add all editable fields with appropriate types // Use std::string for text, int for numbers, bool for checkboxes int version = 0; };
This structure mirrors all editable fields in the entity, used for form data binding.
Commit message
[wt] Add <entity>_data structure for form binding Create data structure containing all editable fields for the <entity> dialog form.
Step 4: Create Detail Dialog
Create the dialog for creating and editing entities.
File locations
- Header:
projects/ores.wt/include/ores.wt/app/<entity>_dialog.hpp - Implementation:
projects/ores.wt/src/app/<entity>_dialog.cpp
Header structure
#ifndef ORES_WT_<ENTITY>_DIALOG_HPP #define ORES_WT_<ENTITY>_DIALOG_HPP #include <string> #include <Wt/WDialog.h> #include <Wt/WLineEdit.h> #include <Wt/WSpinBox.h> #include <Wt/WComboBox.h> #include <Wt/WText.h> #include <Wt/WSignal.h> namespace ores::wt::app { struct <entity>_data { std::string primary_key; std::string name; // Add editable fields int version = 0; }; class <entity>_dialog final : public Wt::WDialog { public: enum class mode { add, edit }; explicit <entity>_dialog(mode m); void set_<entity>(const <entity>_data& data); <entity>_data get_<entity>() const; Wt::Signal<>& saved() { return saved_; } private: void setup_form(); void setup_buttons(); bool validate(); mode mode_; // Form fields Wt::WLineEdit* primary_key_edit_ = nullptr; Wt::WLineEdit* name_edit_ = nullptr; // Add other field widgets Wt::WText* status_text_ = nullptr; Wt::Signal<> saved_; }; } #endif
Implementation patterns
- Constructor
<entity>_dialog::<entity>_dialog(mode m) : Wt::WDialog(m == mode::add ? "Add <Entity>" : "Edit <Entity>"), mode_(m) { setModal(true); setResizable(true); setClosable(true); setWidth(500); setup_form(); setup_buttons(); } - Form setup
void <entity>_dialog::setup_form() { auto* content = contents(); content->addStyleClass("p-3"); auto* form = content->addWidget(std::make_unique<Wt::WContainerWidget>()); // Primary key field (read-only in edit mode) auto* pk_group = form->addWidget(std::make_unique<Wt::WContainerWidget>()); pk_group->addStyleClass("mb-3"); pk_group->addWidget(std::make_unique<Wt::WText>("Primary Key")) ->addStyleClass("form-label"); primary_key_edit_ = pk_group->addWidget(std::make_unique<Wt::WLineEdit>()); primary_key_edit_->addStyleClass("form-control"); primary_key_edit_->setMaxLength(32); // Adjust as needed if (mode_ == mode::edit) { primary_key_edit_->setReadOnly(true); primary_key_edit_->addStyleClass("bg-light"); } // Name field auto* name_group = form->addWidget(std::make_unique<Wt::WContainerWidget>()); name_group->addStyleClass("mb-3"); name_group->addWidget(std::make_unique<Wt::WText>("Name")) ->addStyleClass("form-label"); name_edit_ = name_group->addWidget(std::make_unique<Wt::WLineEdit>()); name_edit_->addStyleClass("form-control"); // Add other fields following same pattern // For spinbox: std::make_unique<Wt::WSpinBox>() // For combobox: std::make_unique<Wt::WComboBox>() // Status text for validation errors status_text_ = form->addWidget(std::make_unique<Wt::WText>()); status_text_->addStyleClass("text-danger mt-2"); }
- Buttons setup
void <entity>_dialog::setup_buttons() { auto* footer = this->footer(); footer->addStyleClass("d-flex gap-2 justify-content-end"); auto* save_btn = footer->addWidget( std::make_unique<Wt::WPushButton>("Save")); save_btn->addStyleClass("btn btn-primary"); save_btn->clicked().connect([this] { if (validate()) { saved_.emit(); accept(); } }); auto* cancel_btn = footer->addWidget( std::make_unique<Wt::WPushButton>("Cancel")); cancel_btn->addStyleClass("btn btn-secondary"); cancel_btn->clicked().connect([this] { reject(); }); }
- Validation
bool <entity>_dialog::validate() { status_text_->setText(""); // Primary key validation auto pk = primary_key_edit_->text().toUTF8(); if (pk.empty()) { status_text_->setText("Primary key is required"); return false; } // Add length/format validation as needed // Name validation auto name = name_edit_->text().toUTF8(); if (name.empty()) { status_text_->setText("Name is required"); return false; } // Add other validations return true; }
- Data getters/setters
void <entity>_dialog::set_<entity>(const <entity>_data& data) { primary_key_edit_->setText(data.primary_key); name_edit_->setText(data.name); // Set other fields } <entity>_data <entity>_dialog::get_<entity>() const { <entity>_data data; data.primary_key = primary_key_edit_->text().toUTF8(); data.name = name_edit_->text().toUTF8(); // Get other fields return data; }
CMakeLists.txt update
Add the new source file to projects/ores.wt/CMakeLists.txt:
set(ores_wt_sources
# ... existing sources
src/app/<entity>_dialog.cpp
)
Commit message
[wt] Add <entity>_dialog for <entity> create/edit Create dialog with form fields, validation, and add/edit mode support.
Phase 2 Checkpoint: Raise PR
At this point:
- Build and verify:
cmake --build --preset linux-clang-debug - Verify the dialog compiles correctly.
- Commit all changes.
- Push branch and raise PR.
PR Title: [wt] Add <Entity> dialog
PR Description:
## Summary - Add <entity>_data structure for form binding - Add <entity>_dialog with add/edit modes and validation
Wait for review feedback and merge before continuing to Phase 3.
Phase 3: Application Integration
After Phase 2 PR is merged, use feature-branch-manager to transition to Phase 3.
Suggested PR title: [wt] Integrate <Entity> into application
Step 5: Create Conversion Functions
Create functions to convert between domain objects and UI structures.
File location
Add to: projects/ores.wt/src/app/ore_application.cpp
Conversion functions
namespace { // Domain -> Row (for list display) <entity>_row to_row(const <component>::domain::<entity>& e) { <entity>_row row; row.primary_key = e.primary_key(); row.name = e.name(); // Map other display fields row.version = e.version(); return row; } // Form -> Domain (for saving) <component>::domain::<entity> to_domain( const <entity>_data& d, const std::string& username) { <component>::domain::<entity> entity; entity.set_primary_key(d.primary_key); entity.set_name(d.name); // Map other editable fields entity.set_recorded_by(username); entity.set_version(d.version); return entity; } // Domain -> Form (for editing) <entity>_data to_data(const <component>::domain::<entity>& e) { <entity>_data data; data.primary_key = e.primary_key(); data.name = e.name(); // Map other editable fields data.version = e.version(); return data; } }
Commit message
[wt] Add <entity> conversion functions Add to_row, to_domain, and to_data functions for converting between domain and UI structures.
Step 6: Integrate into ore_application
Wire up the entity handlers in the main application.
Files to modify
projects/ores.wt/include/ores.wt/app/ore_application.hppprojects/ores.wt/src/app/ore_application.cpp
Header changes
Add to ore_application.hpp:
class <entity>_list_widget; class ore_application : public Wt::WApplication { // ... existing code private: // Add member <entity>_list_widget* <entity>_list_widget_ = nullptr; // Add handlers void setup_<entity>_handlers(); void load_<entity>s(); void show_add_<entity>_dialog(); void show_edit_<entity>_dialog(const std::string& primary_key); void confirm_delete_<entity>(const std::string& primary_key); };
Implementation patterns
- Handler setup
void ore_application::setup_<entity>_handlers() { <entity>_list_widget_->add_requested().connect([this] { show_add_<entity>_dialog(); }); <entity>_list_widget_->refresh_requested().connect([this] { load_<entity>s(); }); <entity>_list_widget_->edit_requested().connect([this](const std::string& pk) { show_edit_<entity>_dialog(pk); }); <entity>_list_widget_->delete_requested().connect([this](const std::string& pk) { confirm_delete_<entity>(pk); }); }
- Load entities
void ore_application::load_<entity>s() { auto& ctx = application_context::instance(); auto entries = ctx.<entity>_service().list_<entity>s(0, max_<entity>s_to_load); std::vector<<entity>_row> rows; rows.reserve(entries.size()); for (const auto& e : entries) { rows.push_back(to_row(e)); } <entity>_list_widget_->set_<entity>s(rows); }
- Show add dialog
void ore_application::show_add_<entity>_dialog() { auto dialog = std::make_unique<<entity>_dialog>(<entity>_dialog::mode::add); auto* dlg = dialog.get(); dlg->saved().connect([this, dlg] { auto data = dlg->get_<entity>(); auto entity = to_domain(data, username_); auto& ctx = application_context::instance(); ctx.<entity>_service().save_<entity>(entity); load_<entity>s(); }); // Cleanup dialog when closed dlg->finished().connect([this, dlg] { removeChild(dlg); }); dlg->show(); addChild(std::move(dialog)); }
- Show edit dialog
void ore_application::show_edit_<entity>_dialog(const std::string& primary_key) { auto& ctx = application_context::instance(); auto entity = ctx.<entity>_service().get_<entity>(primary_key); if (!entity) { // Show error message return; } auto dialog = std::make_unique<<entity>_dialog>(<entity>_dialog::mode::edit); auto* dlg = dialog.get(); dlg->set_<entity>(to_data(*entity)); dlg->saved().connect([this, dlg] { auto data = dlg->get_<entity>(); auto entity = to_domain(data, username_); auto& ctx = application_context::instance(); ctx.<entity>_service().save_<entity>(entity); load_<entity>s(); }); // Cleanup dialog when closed dlg->finished().connect([this, dlg] { removeChild(dlg); }); dlg->show(); addChild(std::move(dialog)); }
- Delete confirmation
void ore_application::confirm_delete_<entity>(const std::string& primary_key) { auto* msgbox = addChild(std::make_unique<Wt::WMessageBox>( "Confirm Delete", "Are you sure you want to delete this <entity>?", Wt::Icon::Question, Wt::StandardButton::Yes | Wt::StandardButton::No)); msgbox->buttonClicked().connect([this, msgbox, primary_key]( Wt::StandardButton btn) { if (btn == Wt::StandardButton::Yes) { auto& ctx = application_context::instance(); ctx.<entity>_service().delete_<entity>(primary_key); load_<entity>s(); } removeChild(msgbox); }); msgbox->show(); }
- Main view integration
In
show_main_view(), add the widget to a tab or navigation:// Create widget <entity>_list_widget_ = container->addWidget( std::make_unique<<entity>_list_widget>()); // Setup handlers setup_<entity>_handlers(); // Load initial data load_<entity>s();
Commit message
[wt] Integrate <entity> into ore_application Add handlers for add/edit/delete operations, conversion functions, and widget to main navigation.
Phase 3 Checkpoint: Raise PR
At this point:
- Build and verify:
cmake --build --preset linux-clang-debug - Run full end-to-end test of the entity.
- Test add, edit, and delete operations.
- Commit all changes.
- Push branch and raise PR.
PR Title: [wt] Integrate <Entity> into application
PR Description:
## Summary - Add conversion functions (to_row, to_domain, to_data) - Integrate <entity>_list_widget into main view - Add handlers for add/edit/delete operations
Common patterns reference
Bootstrap 5 CSS classes
Common classes used in ores.wt:
| Purpose | Classes |
|---|---|
| Container | container-fluid p-3 |
| Toolbar | d-flex gap-2 mb-3 align-items-center |
| Primary button | btn btn-primary |
| Secondary button | btn btn-secondary |
| Small button | btn btn-sm |
| Outline button | btn btn-outline-primary, btn-outline-danger |
| Table | table table-striped table-hover |
| Form control | form-control |
| Form label | form-label |
| Margin bottom | mb-3 |
| Text muted | text-muted |
| Error text | text-danger |
| Read-only background | bg-light |
Form field types
| Field Type | Wt Widget | Example |
|---|---|---|
| Text | WLineEdit |
Name, code |
| Number | WSpinBox |
Version, count |
| Decimal | WDoubleSpinBox |
Amount, rate |
| Selection | WComboBox |
Type, category |
| Boolean | WCheckBox |
Active, enabled |
| Date | WDateEdit |
Start date |
| DateTime | WDateTimeEdit |
Timestamp |
| Large text | WTextArea |
Description |
Signal patterns
// Define signal Wt::Signal<> simple_signal_; Wt::Signal<std::string> string_signal_; Wt::Signal<int, std::string> multi_param_signal_; // Expose signal Wt::Signal<>& simple_signal() { return simple_signal_; } // Emit signal simple_signal_.emit(); string_signal_.emit("value"); multi_param_signal_.emit(42, "text"); // Connect to signal widget->simple_signal().connect([this] { /* handler */ }); widget->string_signal().connect([this](const std::string& val) { /* handler */ });
Related skills
- feature-branch-manager - For transitioning between phases
- Domain Type Creator - For creating the underlying domain type
- Qt Entity Creator - Similar skill for Qt UI layer