aboutsummaryrefslogtreecommitdiff
path: root/app/source/Cplt/Model
diff options
context:
space:
mode:
Diffstat (limited to 'app/source/Cplt/Model')
-rw-r--r--app/source/Cplt/Model/Assets.cpp306
-rw-r--r--app/source/Cplt/Model/Assets.hpp130
-rw-r--r--app/source/Cplt/Model/Database.cpp163
-rw-r--r--app/source/Cplt/Model/Database.hpp79
-rw-r--r--app/source/Cplt/Model/Filter.cpp1
-rw-r--r--app/source/Cplt/Model/Filter.hpp6
-rw-r--r--app/source/Cplt/Model/GlobalStates.cpp163
-rw-r--r--app/source/Cplt/Model/GlobalStates.hpp55
-rw-r--r--app/source/Cplt/Model/Items.cpp114
-rw-r--r--app/source/Cplt/Model/Items.hpp253
-rw-r--r--app/source/Cplt/Model/Project.cpp168
-rw-r--r--app/source/Cplt/Model/Project.hpp57
-rw-r--r--app/source/Cplt/Model/Template/TableTemplate.cpp591
-rw-r--r--app/source/Cplt/Model/Template/TableTemplate.hpp223
-rw-r--r--app/source/Cplt/Model/Template/TableTemplateIterator.cpp52
-rw-r--r--app/source/Cplt/Model/Template/TableTemplateIterator.hpp35
-rw-r--r--app/source/Cplt/Model/Template/Template.hpp68
-rw-r--r--app/source/Cplt/Model/Template/Template_Main.cpp214
-rw-r--r--app/source/Cplt/Model/Template/Template_RTTI.cpp29
-rw-r--r--app/source/Cplt/Model/Template/fwd.hpp11
-rw-r--r--app/source/Cplt/Model/Workflow/Evaluation.cpp174
-rw-r--r--app/source/Cplt/Model/Workflow/Evaluation.hpp67
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp18
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp13
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp94
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp44
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp231
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp53
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp32
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp23
-rw-r--r--app/source/Cplt/Model/Workflow/Nodes/fwd.hpp15
-rw-r--r--app/source/Cplt/Model/Workflow/Value.hpp94
-rw-r--r--app/source/Cplt/Model/Workflow/ValueInternals.hpp21
-rw-r--r--app/source/Cplt/Model/Workflow/Value_Main.cpp35
-rw-r--r--app/source/Cplt/Model/Workflow/Value_RTTI.cpp174
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Basic.cpp111
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Basic.hpp67
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Database.cpp88
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Database.hpp51
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Dictionary.cpp49
-rw-r--r--app/source/Cplt/Model/Workflow/Values/Dictionary.hpp25
-rw-r--r--app/source/Cplt/Model/Workflow/Values/List.cpp100
-rw-r--r--app/source/Cplt/Model/Workflow/Values/List.hpp50
-rw-r--r--app/source/Cplt/Model/Workflow/Values/fwd.hpp17
-rw-r--r--app/source/Cplt/Model/Workflow/Workflow.hpp316
-rw-r--r--app/source/Cplt/Model/Workflow/Workflow_Main.cpp846
-rw-r--r--app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp143
-rw-r--r--app/source/Cplt/Model/Workflow/fwd.hpp22
-rw-r--r--app/source/Cplt/Model/fwd.hpp35
49 files changed, 5726 insertions, 0 deletions
diff --git a/app/source/Cplt/Model/Assets.cpp b/app/source/Cplt/Model/Assets.cpp
new file mode 100644
index 0000000..0dfe847
--- /dev/null
+++ b/app/source/Cplt/Model/Assets.cpp
@@ -0,0 +1,306 @@
+#include "Assets.hpp"
+
+#include <Cplt/UI/UI.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/IO/DataStream.hpp>
+#include <Cplt/Utils/IO/StringIntegration.hpp>
+#include <Cplt/Utils/IO/UuidIntegration.hpp>
+
+#include <IconsFontAwesome.h>
+#include <imgui.h>
+#include <imgui_stdlib.h>
+#include <tsl/array_map.h>
+#include <string>
+#include <utility>
+
+using namespace std::literals::string_view_literals;
+namespace fs = std::filesystem;
+
+template <class TSavedAsset, class TStream>
+void OperateStreamForSavedAsset(TSavedAsset& cell, TStream& proxy)
+{
+ proxy.template ObjectAdapted<DataStreamAdapters::String>(cell.Name);
+ proxy.template ObjectAdapted<DataStreamAdapters::Uuid>(cell.Uuid);
+ proxy.Value(cell.Payload);
+}
+
+void SavedAsset::ReadFromDataStream(InputDataStream& stream)
+{
+ ::OperateStreamForSavedAsset(*this, stream);
+}
+
+void SavedAsset::WriteToDataStream(OutputDataStream& stream) const
+{
+ ::OperateStreamForSavedAsset(*this, stream);
+}
+
+Asset::Asset() = default;
+
+class AssetList::Private
+{
+public:
+ Project* ConnectedProject;
+ tsl::array_map<char, SavedAsset> Assets;
+ tsl::array_map<char, std::unique_ptr<Asset>> Cache;
+ int CacheSizeLimit = 0;
+
+ struct
+ {
+ std::string NewName;
+ NameSelectionError NewNameError = NameSelectionError::Empty;
+
+ void ShowErrors() const
+ {
+ switch (NewNameError) {
+ case NameSelectionError::None: break;
+ case NameSelectionError::Duplicated:
+ ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR));
+ break;
+ case NameSelectionError::Empty:
+ ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR));
+ break;
+ }
+ }
+
+ bool HasErrors() const
+ {
+ return NewNameError != NameSelectionError::None;
+ }
+
+ void Validate(const AssetList& self)
+ {
+ if (NewName.empty()) {
+ NewNameError = NameSelectionError::Empty;
+ return;
+ }
+
+ if (self.FindByName(NewName)) {
+ NewNameError = NameSelectionError::Duplicated;
+ return;
+ }
+
+ NewNameError = NameSelectionError::None;
+ }
+ } PopupPrivateState;
+};
+
+AssetList::AssetList(Project& project)
+ : mPrivate{ std::make_unique<Private>() }
+{
+ mPrivate->ConnectedProject = &project;
+}
+
+// Write an empty destructor here so std::unique_ptr's destructor can see AssetList::Private's implementation
+AssetList::~AssetList()
+{
+}
+
+Project& AssetList::GetConnectedProject() const
+{
+ return *mPrivate->ConnectedProject;
+}
+
+void AssetList::Reload()
+{
+ // TODO fix asset dicovery loading
+ mPrivate->Assets.clear();
+ mPrivate->Cache.clear();
+ DiscoverFiles([this](SavedAsset asset) -> void {
+ mPrivate->Assets.insert(asset.Name, std::move(asset));
+ });
+}
+
+int AssetList::GetCount() const
+{
+ return mPrivate->Assets.size();
+}
+
+const tsl::array_map<char, SavedAsset>& AssetList::GetAssets() const
+{
+ return mPrivate->Assets;
+}
+
+const SavedAsset* AssetList::FindByName(std::string_view name) const
+{
+ auto iter = mPrivate->Assets.find(name);
+ if (iter != mPrivate->Assets.end()) {
+ return &iter.value();
+ } else {
+ return nullptr;
+ }
+}
+
+const SavedAsset& AssetList::Create(SavedAsset asset)
+{
+ auto [iter, DISCARD] = mPrivate->Assets.insert(asset.Name, SavedAsset{});
+ auto& savedAsset = iter.value();
+
+ savedAsset = std::move(asset);
+ if (savedAsset.Uuid.is_nil()) {
+ savedAsset.Uuid = uuids::uuid_random_generator{}();
+ }
+
+ SaveInstance(savedAsset, nullptr);
+
+ return savedAsset;
+}
+
+std::unique_ptr<Asset> AssetList::CreateAndLoad(SavedAsset assetIn)
+{
+ auto& savedAsset = Create(std::move(assetIn));
+ auto asset = std::unique_ptr<Asset>(CreateInstance(savedAsset));
+ return asset;
+}
+
+std::unique_ptr<Asset> AssetList::Load(std::string_view name) const
+{
+ if (auto savedAsset = FindByName(name)) {
+ auto asset = Load(*savedAsset);
+ return asset;
+ } else {
+ return nullptr;
+ }
+}
+
+std::unique_ptr<Asset> AssetList::Load(const SavedAsset& asset) const
+{
+ return std::unique_ptr<Asset>(LoadInstance(asset));
+}
+
+const SavedAsset* AssetList::Rename(std::string_view oldName, std::string_view newName)
+{
+ auto iter = mPrivate->Assets.find(oldName);
+ if (iter == mPrivate->Assets.end()) return nullptr;
+
+ auto info = std::move(iter.value());
+ info.Name = newName;
+
+ RenameInstanceOnDisk(info, oldName);
+
+ mPrivate->Assets.erase(iter);
+ auto [newIter, DISCARD] = mPrivate->Assets.insert(newName, std::move(info));
+
+ return &newIter.value();
+}
+
+bool AssetList::Remove(std::string_view name)
+{
+ auto iter = mPrivate->Assets.find(name);
+ if (iter == mPrivate->Assets.end()) {
+ return false;
+ }
+ auto& asset = iter.value();
+
+ fs::remove(RetrievePathFromAsset(asset));
+ mPrivate->Assets.erase(iter);
+
+ return true;
+}
+
+int AssetList::GetCacheSizeLimit() const
+{
+ return mPrivate->CacheSizeLimit;
+}
+
+void AssetList::SetCacheSizeLimit(int limit)
+{
+ mPrivate->CacheSizeLimit = limit;
+}
+
+void AssetList::DisplayIconsList(ListState& state)
+{
+ // TODO
+}
+
+void AssetList::DisplayDetailsList(ListState& state)
+{
+ // Note: stub function remained here in case any state processing needs to be done before issuing to implementers
+ DisplayDetailsTable(state);
+}
+
+void AssetList::DisplayControls(ListState& state)
+{
+ auto& ps = mPrivate->PopupPrivateState;
+ bool openedDummy = true;
+
+ if (ImGui::Button(ICON_FA_PLUS " " I18N_TEXT("Add", L10N_ADD))) {
+ ImGui::OpenPopup(I18N_TEXT("Add asset wizard", L10N_ADD_ASSET_DIALOG_TITLE));
+ }
+ if (ImGui::BeginPopupModal(I18N_TEXT("Add asset wizard", L10N_ADD_ASSET_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) {
+ DisplayAssetCreator(state);
+ ImGui::EndPopup();
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button(ICON_FA_I_CURSOR " " I18N_TEXT("Rename", L10N_RENAME), state.SelectedAsset == nullptr)) {
+ ImGui::OpenPopup(I18N_TEXT("Rename asset wizard", L10N_RENAME_ASSET_DIALOG_TITLE));
+ }
+ if (ImGui::BeginPopupModal(I18N_TEXT("Rename asset wizard", L10N_RENAME_ASSET_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) {
+ if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &ps.NewName)) {
+ ps.Validate(*this);
+ }
+
+ if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM), ps.HasErrors())) {
+ ImGui::CloseCurrentPopup();
+
+ auto movedAsset = Rename(state.SelectedAsset->Name, ps.NewName);
+ // Update the selected pointer to the new location (we mutated the map, the pointer may be invalid now)
+ state.SelectedAsset = movedAsset;
+
+ ps = {};
+ }
+ ImGui::SameLine();
+ if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) {
+ ImGui::CloseCurrentPopup();
+ }
+
+ ps.ShowErrors();
+
+ ImGui::EndPopup();
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE), state.SelectedAsset == nullptr)) {
+ ImGui::OpenPopup(I18N_TEXT("Delete asset", L10N_DELETE_ASSET_DIALOG_TITLE));
+ }
+ if (ImGui::BeginPopupModal(I18N_TEXT("Delete asset", L10N_DELETE_ASSET_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) {
+ if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM))) {
+ ImGui::CloseCurrentPopup();
+
+ auto& assetName = state.SelectedAsset->Name;
+ Remove(assetName);
+
+ state.SelectedAsset = nullptr;
+ }
+ ImGui::SameLine();
+ if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) {
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::EndPopup();
+ }
+}
+
+void AssetList::DiscoverFilesByExtension(const std::function<void(SavedAsset)>& callback, const fs::path& containerDir, std::string_view extension) const
+{
+ for (auto entry : fs::directory_iterator(containerDir)) {
+ if (!entry.is_regular_file()) continue;
+
+ // If the caller provided an extension to match against, and it doesn't equal to current file extension, skip
+ if (!extension.empty() &&
+ entry.path().extension() != extension)
+ {
+ continue;
+ }
+
+ callback(SavedAsset{
+ .Name = RetrieveNameFromFile(entry.path()),
+ .Uuid = RetrieveUuidFromFile(entry.path()),
+ // TODO load payload
+ });
+ }
+}
+
+void AssetList::DiscoverFilesByHeader(const std::function<void(SavedAsset)>& callback, const fs::path& containerDir, const std::function<bool(std::istream&)>& validater) const
+{
+ // TODO
+}
diff --git a/app/source/Cplt/Model/Assets.hpp b/app/source/Cplt/Model/Assets.hpp
new file mode 100644
index 0000000..d2f8570
--- /dev/null
+++ b/app/source/Cplt/Model/Assets.hpp
@@ -0,0 +1,130 @@
+#pragma once
+
+#include <Cplt/Utils/UUID.hpp>
+#include <Cplt/fwd.hpp>
+
+#include <tsl/array_map.h>
+#include <filesystem>
+#include <iosfwd>
+#include <memory>
+#include <string_view>
+
+/// A structure representing a ready-to-be-loaded asset, locating on the disk.
+/// Each asset should be identified by a unique uuid within the asset category (i.e. a workflow and a template can share the same uuid),
+/// generated on insertion to an asset list if not given by the caller.
+struct SavedAsset
+{
+ std::string Name;
+ /// UUID of this asset. This field is generated as a random UUID v4 upon insertion into an AssetList, if not already provided by the caller (indicated by !is_nil()).
+ uuids::uuid Uuid;
+ /// Extra data to be used by the AssetList/Asset implementation.
+ uint64_t Payload;
+
+ void ReadFromDataStream(InputDataStream& stream);
+ void WriteToDataStream(OutputDataStream& stream) const;
+};
+
+class Asset
+{
+public:
+ Asset();
+ virtual ~Asset() = default;
+};
+
+enum class NameSelectionError
+{
+ None,
+ Duplicated,
+ Empty,
+};
+
+class AssetList
+{
+private:
+ class Private;
+ std::unique_ptr<Private> mPrivate;
+
+public:
+ AssetList(Project& project);
+ virtual ~AssetList();
+
+ Project& GetConnectedProject() const;
+
+ // TODO support file watches
+ void Reload();
+
+ int GetCount() const;
+ // TODO convert to custom iterable
+ const tsl::array_map<char, SavedAsset>& GetAssets() const;
+
+ const SavedAsset* FindByName(std::string_view name) const;
+ const SavedAsset& Create(SavedAsset asset);
+ std::unique_ptr<Asset> CreateAndLoad(SavedAsset asset);
+ /// Load the asset on disk by its name.
+ std::unique_ptr<Asset> Load(std::string_view name) const;
+ /// Load the asset on disk by a reference to its SavedAsset instance. This function assumes that the given SavedAsset
+ /// is stored in AssetList, otherwise the behavior is undefined.
+ std::unique_ptr<Asset> Load(const SavedAsset& asset) const;
+ const SavedAsset* Rename(std::string_view oldName, std::string_view newName);
+ bool Remove(std::string_view name);
+
+ int GetCacheSizeLimit() const;
+ void SetCacheSizeLimit(int limit);
+
+ struct ListState
+ {
+ const SavedAsset* SelectedAsset = nullptr;
+ };
+ void DisplayIconsList(ListState& state);
+ void DisplayDetailsList(ListState& state);
+ void DisplayControls(ListState& state);
+
+protected:
+ virtual void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const = 0;
+
+ // Helper
+ void DiscoverFilesByExtension(const std::function<void(SavedAsset)>& callback, const std::filesystem::path& containerDir, std::string_view extension) const;
+ void DiscoverFilesByHeader(const std::function<void(SavedAsset)>& callback, const std::filesystem::path& containerDir, const std::function<bool(std::istream&)>& validater) const;
+
+ /// Create an empty/default instance of this asset type on disk, potentially qualified by SavedAsset::Payload.
+ /// Return `true` on success and `false` on failure.
+ virtual bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const = 0;
+ /// The returned pointer indicate ownership to the object.
+ virtual Asset* LoadInstance(const SavedAsset& assetInfo) const = 0;
+ /// Create an empty/default instance of this asset type, potentially qualified by SavedAsset::Payload.
+ /// The returned pointer indicate ownership to the object.
+ virtual Asset* CreateInstance(const SavedAsset& assetInfo) const = 0;
+ virtual bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const = 0;
+
+ virtual std::string RetrieveNameFromFile(const std::filesystem::path& file) const = 0;
+ virtual uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const = 0;
+ virtual std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const = 0;
+
+ virtual void DisplayAssetCreator(ListState& state) = 0;
+ virtual void DisplayDetailsTable(ListState& state) const = 0;
+};
+
+template <class T>
+class AssetListTyped : public AssetList
+{
+public:
+ using AssetList::AssetList;
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "HidingNonVirtualFunction"
+ std::unique_ptr<T> CreateAndLoad(SavedAsset asset)
+ {
+ return std::unique_ptr<T>(static_cast<T*>(AssetList::CreateAndLoad(asset).release()));
+ }
+
+ std::unique_ptr<T> Load(std::string_view name) const
+ {
+ return std::unique_ptr<T>(static_cast<T*>(AssetList::Load(name).release()));
+ }
+
+ std::unique_ptr<T> Load(const SavedAsset& asset) const
+ {
+ return std::unique_ptr<T>(static_cast<T*>(AssetList::Load(asset).release()));
+ }
+#pragma clang diagnostic pop
+};
diff --git a/app/source/Cplt/Model/Database.cpp b/app/source/Cplt/Model/Database.cpp
new file mode 100644
index 0000000..07c6e36
--- /dev/null
+++ b/app/source/Cplt/Model/Database.cpp
@@ -0,0 +1,163 @@
+#include "Database.hpp"
+
+#include <Cplt/Model/Project.hpp>
+
+#include <filesystem>
+#include <stdexcept>
+
+namespace fs = std::filesystem;
+
+SalesTable::SalesTable(MainDatabase& db)
+ // language=SQLite
+ : GetRowCount(db.GetSQLite(), "SELECT Count(*) FROM Sales")
+ // language=SQLite
+ , GetRows(db.GetSQLite(), "SELECT * FROM Sales LIMIT ? OFFSET ?")
+ // language=SQLite
+ , GetItems(db.GetSQLite(), "SELECT * FROM SalesItems WHERE SaleId == ?")
+{
+}
+
+PurchasesTable::PurchasesTable(MainDatabase& db)
+ // language=SQLite
+ : GetRowCount(db.GetSQLite(), "SELECT Count(*) FROM Purchases")
+ // language=SQLite
+ , GetRows(db.GetSQLite(), "SELECT * FROM Purchases LIMIT ? OFFSET ?")
+ // language=SQLite
+ , GetItems(db.GetSQLite(), "SELECT * FROM PurchasesItems WHERE PurchaseId == ?")
+{
+}
+
+DeliveryTable::DeliveryTable(MainDatabase& db)
+ // language=SQLite
+ : FilterByTypeAndId(db.GetSQLite(), "SELECT * FROM Deliveries WHERE AssociatedOrder == ? AND Outgoing = ?")
+ // language=SQLite
+ , GetItems(db.GetSQLite(), "SELECT * FROM DeliveriesItems WHERE DeliveryId == ?")
+{
+}
+
+static std::string GetDatabaseFilePath(const Project& project)
+{
+ auto dbsDir = project.GetPath() / "databases";
+ fs::create_directories(dbsDir);
+
+ auto dbFile = dbsDir / "transactions.sqlite3";
+ return dbFile.string();
+}
+
+/// Wrapper for SQLite::Database that creates the default tables
+MainDatabase::DatabaseWrapper::DatabaseWrapper(MainDatabase& self)
+ : mSqlite(GetDatabaseFilePath(*self.mProject), SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE)
+{
+ // If this table doesn't exist, the database probably just got initialized
+ if (mSqlite.tableExists("Sales")) {
+ return;
+ }
+
+ // 'Sales' schema
+ // - Customer: the customer item ID
+ // - Deadline: unix epoch time of order deadline
+ // - DeliveryTime: the time this order was completed (through a set of deliveries)
+
+ // 'Purchases' schema
+ // - Factory: the factory id,
+ // - OrderTime: the time this order was made
+ // - DeliveryTime: the time this order was completed (through a set of deliveries)
+
+ // 'Deliveries' schema
+ // - ShipmentTime: unix epoch time stamp of sending to delivery
+ // - ArrivalTime: unix epoch time stamp of delivery arrived at warehouse; 0 if not arrived yet
+ // - AssociatedOrder: Id of the order that this delivery is completing (which table: Outgoing=true -> Sales, Outgoing=false -> Purchases)
+ // - Outgoing: true if the delivery is from warehouse to customer; false if the delivery is from factory to warehouse
+
+ // Note: the 'Id' key would be unique (not recycled after row deletion) because it's explicit
+ // https://www.sqlite.org/rowidtable.html
+
+ // language=SQLite
+ mSqlite.exec(R"""(
+CREATE TABLE IF NOT EXISTS Sales(
+ Id INT PRIMARY KEY,
+ Customer INT,
+ Deadline DATETIME,
+ DeliveryTime DATETIME
+);
+CREATE TABLE IF NOT EXISTS SalesItems(
+ SaleId INT,
+ ItemId INT,
+ Count INT
+);
+
+CREATE TABLE IF NOT EXISTS Purchases(
+ Id INT PRIMARY KEY,
+ Factory INT,
+ OrderTime DATETIME,
+ DeliveryTime DATETIME
+);
+CREATE TABLE IF NOT EXISTS PurchasesItems(
+ PurchaseId INT,
+ ItemId INT,
+ Count INT
+);
+
+CREATE TABLE IF NOT EXISTS Deliveries(
+ Id INT PRIMARY KEY,
+ ShipmentTime DATETIME,
+ ArrivalTime DATETIME,
+ AssociatedOrder INT,
+ Outgoing BOOLEAN
+);
+CREATE TABLE IF NOT EXISTS DeliveriesItems(
+ DeliveryId INT,
+ ItemId INT,
+ Count INT
+);
+)""");
+}
+
+MainDatabase::MainDatabase(Project& project)
+ : mProject{ &project }
+ , mDbWrapper(*this)
+ , mSales(*this)
+ , mPurchases(*this)
+ , mDeliveries(*this)
+{
+}
+
+const SQLite::Database& MainDatabase::GetSQLite() const
+{
+ return mDbWrapper.mSqlite;
+}
+
+SQLite::Database& MainDatabase::GetSQLite()
+{
+ return mDbWrapper.mSqlite;
+}
+
+const SalesTable& MainDatabase::GetSales() const
+{
+ return mSales;
+}
+
+SalesTable& MainDatabase::GetSales()
+{
+ return mSales;
+}
+
+const PurchasesTable& MainDatabase::GetPurchases() const
+{
+ return mPurchases;
+}
+
+PurchasesTable& MainDatabase::GetPurchases()
+{
+ return mPurchases;
+}
+
+const DeliveryTable& MainDatabase::GetDeliveries() const
+{
+ return mDeliveries;
+}
+
+DeliveryTable& MainDatabase::GetDeliveries()
+{
+ return mDeliveries;
+}
diff --git a/app/source/Cplt/Model/Database.hpp b/app/source/Cplt/Model/Database.hpp
new file mode 100644
index 0000000..222e43d
--- /dev/null
+++ b/app/source/Cplt/Model/Database.hpp
@@ -0,0 +1,79 @@
+#pragma once
+
+#include <Cplt/fwd.hpp>
+
+#include <SQLiteCpp/Database.h>
+#include <SQLiteCpp/Statement.h>
+#include <cstdint>
+
+enum class TableKind
+{
+ Sales,
+ SalesItems,
+ Purchases,
+ PurchasesItems,
+ Deliveries,
+ DeliveriesItems,
+};
+
+class SalesTable
+{
+public:
+ SQLite::Statement GetRowCount;
+ SQLite::Statement GetRows;
+ SQLite::Statement GetItems;
+
+public:
+ SalesTable(MainDatabase& db);
+};
+
+class PurchasesTable
+{
+public:
+ SQLite::Statement GetRowCount;
+ SQLite::Statement GetRows;
+ SQLite::Statement GetItems;
+
+public:
+ PurchasesTable(MainDatabase& db);
+};
+
+class DeliveryTable
+{
+public:
+ SQLite::Statement FilterByTypeAndId;
+ SQLite::Statement GetItems;
+
+public:
+ DeliveryTable(MainDatabase& db);
+};
+
+class MainDatabase
+{
+private:
+ class DatabaseWrapper
+ {
+ public:
+ SQLite::Database mSqlite;
+ DatabaseWrapper(MainDatabase& self);
+ };
+
+ Project* mProject;
+ DatabaseWrapper mDbWrapper;
+ SalesTable mSales;
+ PurchasesTable mPurchases;
+ DeliveryTable mDeliveries;
+
+public:
+ MainDatabase(Project& project);
+
+ const SQLite::Database& GetSQLite() const;
+ SQLite::Database& GetSQLite();
+
+ const SalesTable& GetSales() const;
+ SalesTable& GetSales();
+ const PurchasesTable& GetPurchases() const;
+ PurchasesTable& GetPurchases();
+ const DeliveryTable& GetDeliveries() const;
+ DeliveryTable& GetDeliveries();
+};
diff --git a/app/source/Cplt/Model/Filter.cpp b/app/source/Cplt/Model/Filter.cpp
new file mode 100644
index 0000000..1e4b31b
--- /dev/null
+++ b/app/source/Cplt/Model/Filter.cpp
@@ -0,0 +1 @@
+#include "Filter.hpp"
diff --git a/app/source/Cplt/Model/Filter.hpp b/app/source/Cplt/Model/Filter.hpp
new file mode 100644
index 0000000..1b923e1
--- /dev/null
+++ b/app/source/Cplt/Model/Filter.hpp
@@ -0,0 +1,6 @@
+#pragma once
+
+class TableRowsFilter
+{
+ // TODO
+};
diff --git a/app/source/Cplt/Model/GlobalStates.cpp b/app/source/Cplt/Model/GlobalStates.cpp
new file mode 100644
index 0000000..417514f
--- /dev/null
+++ b/app/source/Cplt/Model/GlobalStates.cpp
@@ -0,0 +1,163 @@
+#include "GlobalStates.hpp"
+
+#include <Cplt/Model/Project.hpp>
+#include <Cplt/Utils/StandardDirectories.hpp>
+
+#include <json/reader.h>
+#include <json/value.h>
+#include <json/writer.h>
+#include <cassert>
+#include <filesystem>
+#include <fstream>
+#include <memory>
+
+namespace fs = std::filesystem;
+
+static std::unique_ptr<GlobalStates> globalStateInstance;
+static fs::path globalDataPath;
+
+void GlobalStates::Init()
+{
+ Init(StandardDirectories::UserData() / "cplt");
+}
+
+void GlobalStates::Init(std::filesystem::path userDataDir)
+{
+ globalStateInstance = std::make_unique<GlobalStates>();
+ globalDataPath = userDataDir;
+ fs::create_directories(globalDataPath);
+
+ // Reading recent projects
+ {
+ std::ifstream ifs(globalDataPath / "recents.json");
+ if (!ifs) return;
+
+ Json::Value root;
+ ifs >> root;
+
+ if (!root.isObject()) return;
+ if (auto& recents = root["RecentProjects"]; recents.isArray()) {
+ for (auto& elm : recents) {
+ if (!elm.isString()) continue;
+
+ fs::path path(elm.asCString());
+ if (!fs::exists(path)) continue;
+
+ auto utf8String = path.string();
+ globalStateInstance->mRecentProjects.push_back(RecentProject{
+ .Path = std::move(path),
+ .CachedUtf8String = std::move(utf8String),
+ });
+ }
+ }
+ }
+}
+
+void GlobalStates::Shutdown()
+{
+ if (!globalStateInstance) return;
+
+ globalStateInstance->SetCurrentProject(nullptr);
+
+ if (globalStateInstance->mDirty) {
+ globalStateInstance->WriteToDisk();
+ }
+}
+
+GlobalStates& GlobalStates::GetInstance()
+{
+ return *globalStateInstance;
+}
+
+const std::filesystem::path& GlobalStates::GetUserDataPath()
+{
+ return globalDataPath;
+}
+
+const std::vector<GlobalStates::RecentProject>& GlobalStates::GetRecentProjects() const
+{
+ return mRecentProjects;
+}
+
+void GlobalStates::ClearRecentProjects()
+{
+ mRecentProjects.clear();
+ MarkDirty();
+}
+
+void GlobalStates::AddRecentProject(const Project& project)
+{
+ mRecentProjects.push_back(RecentProject{
+ .Path = project.GetPath(),
+ .CachedUtf8String = project.GetPath().string(),
+ });
+ MarkDirty();
+}
+
+void GlobalStates::MoveProjectToTop(const Project& project)
+{
+ for (auto it = mRecentProjects.begin(); it != mRecentProjects.end(); ++it) {
+ if (it->Path == project.GetPath()) {
+ std::rotate(it, it + 1, mRecentProjects.end());
+ MarkDirty();
+ return;
+ }
+ }
+ AddRecentProject(project);
+}
+
+void GlobalStates::RemoveRecentProject(int idx)
+{
+ assert(idx >= 0 && idx < mRecentProjects.size());
+
+ mRecentProjects.erase(mRecentProjects.begin() + idx);
+ MarkDirty();
+}
+
+bool GlobalStates::HasCurrentProject() const
+{
+ return mCurrentProject != nullptr;
+}
+
+Project* GlobalStates::GetCurrentProject() const
+{
+ return mCurrentProject.get();
+}
+
+void GlobalStates::SetCurrentProject(std::unique_ptr<Project> project)
+{
+ if (mCurrentProject) {
+ mCurrentProject->WriteToDisk();
+ mCurrentProject = nullptr;
+ }
+ if (project) {
+ MoveProjectToTop(*project);
+ }
+ mCurrentProject = std::move(project);
+}
+
+void GlobalStates::WriteToDisk() const
+{
+ Json::Value root;
+
+ auto& recentProjects = root["RecentProjects"] = Json::Value(Json::arrayValue);
+ for (auto& [path, _] : mRecentProjects) {
+ recentProjects.append(Json::Value(path.string()));
+ }
+
+ std::ofstream ofs(globalDataPath / "recents.json");
+ ofs << root;
+
+ mDirty = false;
+}
+
+bool GlobalStates::IsDirty() const
+{
+ return mDirty;
+}
+
+void GlobalStates::MarkDirty()
+{
+ mDirty = true;
+ OnModified();
+}
diff --git a/app/source/Cplt/Model/GlobalStates.hpp b/app/source/Cplt/Model/GlobalStates.hpp
new file mode 100644
index 0000000..1eb47fb
--- /dev/null
+++ b/app/source/Cplt/Model/GlobalStates.hpp
@@ -0,0 +1,55 @@
+#pragma once
+
+#include <Cplt/Utils/Sigslot.hpp>
+#include <Cplt/fwd.hpp>
+
+#include <filesystem>
+#include <string>
+#include <vector>
+
+class GlobalStates
+{
+public:
+ static void Init();
+ static void Init(std::filesystem::path userDataDir);
+ static void Shutdown();
+
+ static GlobalStates& GetInstance();
+ static const std::filesystem::path& GetUserDataPath();
+
+ struct RecentProject
+ {
+ std::filesystem::path Path;
+ std::string CachedUtf8String;
+ };
+
+public:
+ Signal<> OnModified;
+
+private:
+ std::vector<RecentProject> mRecentProjects;
+ std::unique_ptr<Project> mCurrentProject;
+ mutable bool mDirty = false;
+
+public:
+ const std::vector<RecentProject>& GetRecentProjects() const;
+ void ClearRecentProjects();
+ void AddRecentProject(const Project& project);
+ /// Move or add the project to end of the recent projects list.
+ /// If the project is not in the list of recently used projects, it will be appended, otherwise
+ /// it will be moved to the end.
+ void MoveProjectToTop(const Project& project);
+ void RemoveRecentProject(int idx);
+
+ bool HasCurrentProject() const;
+ Project* GetCurrentProject() const;
+ void SetCurrentProject(std::unique_ptr<Project> project);
+
+ // TODO async autosaving to prevent data loss on crash
+ void WriteToDisk() const;
+
+ bool IsDirty() const;
+
+private:
+ void MarkDirty();
+};
diff --git a/app/source/Cplt/Model/Items.cpp b/app/source/Cplt/Model/Items.cpp
new file mode 100644
index 0000000..9d2abc6
--- /dev/null
+++ b/app/source/Cplt/Model/Items.cpp
@@ -0,0 +1,114 @@
+#include "Items.hpp"
+
+const std::string& ProductItem::GetDescription() const
+{
+ return mDescription;
+}
+
+void ProductItem::SetDescription(std::string description)
+{
+ mDescription = std::move(description);
+}
+
+int ProductItem::GetPrice() const
+{
+ return mPrice;
+}
+void ProductItem::SetPrice(int price)
+{
+ mPrice = price;
+}
+
+int ProductItem::GetStock() const
+{
+ return mStock;
+}
+
+void ProductItem::SetStock(int stock)
+{
+ mStock = stock;
+}
+
+Json::Value ProductItem::Serialize() const
+{
+ Json::Value elm;
+ elm["Description"] = mDescription;
+ elm["Price"] = mPrice;
+ elm["Stock"] = mStock;
+ return elm;
+}
+
+void ProductItem::Deserialize(const Json::Value& elm)
+{
+ mDescription = elm["Description"].asString();
+ mPrice = elm["Price"].asInt();
+ mStock = elm["Stock"].asInt();
+}
+
+const std::string& FactoryItem::GetDescription() const
+{
+ return mDescription;
+}
+
+void FactoryItem::SetDescription(std::string description)
+{
+ mDescription = std::move(description);
+}
+
+const std::string& FactoryItem::GetEmail() const
+{
+ return mEmail;
+}
+
+void FactoryItem::SetEmail(std::string email)
+{
+ mEmail = std::move(email);
+}
+
+Json::Value FactoryItem::Serialize() const
+{
+ Json::Value elm;
+ elm["Description"] = mDescription;
+ elm["Email"] = mEmail;
+ return elm;
+}
+
+void FactoryItem::Deserialize(const Json::Value& elm)
+{
+ mDescription = elm["Description"].asString();
+ mEmail = elm["Email"].asString();
+}
+
+const std::string& CustomerItem::GetDescription() const
+{
+ return mDescription;
+}
+
+void CustomerItem::SetDescription(std::string description)
+{
+ mDescription = std::move(description);
+}
+
+const std::string& CustomerItem::GetEmail() const
+{
+ return mEmail;
+}
+
+void CustomerItem::SetEmail(std::string email)
+{
+ mEmail = std::move(email);
+}
+
+Json::Value CustomerItem::Serialize() const
+{
+ Json::Value elm;
+ elm["Description"] = mDescription;
+ elm["Email"] = mEmail;
+ return elm;
+}
+
+void CustomerItem::Deserialize(const Json::Value& elm)
+{
+ mDescription = elm["Description"].asString();
+ mEmail = elm["Email"].asString();
+}
diff --git a/app/source/Cplt/Model/Items.hpp b/app/source/Cplt/Model/Items.hpp
new file mode 100644
index 0000000..c00ee59
--- /dev/null
+++ b/app/source/Cplt/Model/Items.hpp
@@ -0,0 +1,253 @@
+#pragma once
+
+#include <Cplt/fwd.hpp>
+
+#include <json/reader.h>
+#include <json/value.h>
+#include <json/writer.h>
+#include <tsl/array_map.h>
+#include <cstddef>
+#include <limits>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+template <class T>
+class ItemList
+{
+private:
+ std::vector<T> mStorage;
+ tsl::array_map<char, size_t> mNameLookup;
+
+public:
+ template <class... Args>
+ T& Insert(std::string name, Args... args)
+ {
+ auto iter = mNameLookup.find(name);
+ if (iter != mNameLookup.end()) {
+ throw std::runtime_error("Duplicate key.");
+ }
+
+ for (size_t i = 0; i < mStorage.size(); ++i) {
+ if (mStorage[i].IsInvalid()) {
+ mStorage[i] = T(*this, i, std::move(name), std::forward<Args>(args)...);
+ mNameLookup.insert(name, i);
+ return mStorage[i];
+ }
+ }
+
+ size_t id = mStorage.size();
+ mNameLookup.insert(name, id);
+ mStorage.emplace_back(*this, id, std::move(name), std::forward<Args>(args)...);
+ return mStorage[id];
+ }
+
+ void Remove(size_t index)
+ {
+ auto& item = mStorage[index];
+ mNameLookup.erase(item.GetName());
+ mStorage[index] = T(*this);
+ }
+
+ T* Find(size_t id)
+ {
+ return &mStorage[id];
+ }
+
+ const T* Find(size_t id) const
+ {
+ return &mStorage[id];
+ }
+
+ const T* Find(std::string_view name) const
+ {
+ auto iter = mNameLookup.find(name);
+ if (iter != mNameLookup.end()) {
+ return &mStorage[iter.value()];
+ } else {
+ return nullptr;
+ }
+ }
+
+ Json::Value Serialize() const
+ {
+ Json::Value items(Json::arrayValue);
+ for (auto& item : mStorage) {
+ if (!item.IsInvalid()) {
+ auto elm = item.Serialize();
+ elm["Id"] = item.GetId();
+ elm["Name"] = item.GetName();
+ items.append(elm);
+ }
+ }
+
+ Json::Value root;
+ root["MaxItemId"] = mStorage.size();
+ root["Items"] = std::move(items);
+
+ return root;
+ }
+
+ ItemList() = default;
+
+ ItemList(const Json::Value& root)
+ {
+ constexpr const char* kMessage = "Failed to load item list from JSON.";
+
+ auto& itemCount = root["MaxItemId"];
+ if (!itemCount.isIntegral()) throw std::runtime_error(kMessage);
+
+ mStorage.resize(itemCount.asInt64(), T(*this));
+
+ auto& items = root["Items"];
+ if (!items.isArray()) throw std::runtime_error(kMessage);
+
+ for (auto& elm : items) {
+ if (!elm.isObject()) throw std::runtime_error(kMessage);
+
+ auto& id = elm["Id"];
+ if (!id.isIntegral()) throw std::runtime_error(kMessage);
+ auto& name = elm["Name"];
+ if (!name.isString()) throw std::runtime_error(kMessage);
+
+ size_t iid = id.asInt64();
+ mStorage[iid] = T(*this, iid, name.asString());
+ mStorage[iid].Deserialize(elm);
+ }
+ }
+
+ typename decltype(mStorage)::iterator begin()
+ {
+ return mStorage.begin();
+ }
+
+ typename decltype(mStorage)::iterator end()
+ {
+ return mStorage.end();
+ }
+
+ typename decltype(mStorage)::const_iterator begin() const
+ {
+ return mStorage.begin();
+ }
+
+ typename decltype(mStorage)::const_iterator end() const
+ {
+ return mStorage.end();
+ }
+
+private:
+ template <class TSelf>
+ friend class ItemBase;
+
+ void UpdateItemName(const T& item, const std::string& newName)
+ {
+ mNameLookup.erase(item.GetName());
+ mNameLookup.insert(newName, item.GetId());
+ }
+};
+
+template <class TSelf>
+class ItemBase
+{
+private:
+ ItemList<TSelf>* mList;
+ size_t mId;
+ std::string mName;
+
+public:
+ ItemBase(ItemList<TSelf>& list, size_t id = std::numeric_limits<size_t>::max(), std::string name = "")
+ : mList{ &list }
+ , mId{ id }
+ , mName{ std::move(name) }
+ {
+ }
+
+ bool IsInvalid() const
+ {
+ return mId == std::numeric_limits<size_t>::max();
+ }
+
+ ItemList<TSelf>& GetList() const
+ {
+ return *mList;
+ }
+
+ size_t GetId() const
+ {
+ return mId;
+ }
+
+ const std::string& GetName() const
+ {
+ return mName;
+ }
+
+ void SetName(std::string name)
+ {
+ mList->UpdateItemName(static_cast<TSelf&>(*this), name);
+ mName = std::move(name);
+ }
+};
+
+class ProductItem : public ItemBase<ProductItem>
+{
+private:
+ std::string mDescription;
+ int mPrice = 0;
+ int mStock = 0;
+
+public:
+ using ItemBase::ItemBase;
+
+ const std::string& GetDescription() const;
+ void SetDescription(std::string description);
+ /// Get the price of this item in US cents.
+ int GetPrice() const;
+ void SetPrice(int price);
+ /// Get the current number of this product in warehouse.
+ /// This is a housekeeping field and shouldn't be editable by the user from the UI.
+ int GetStock() const;
+ void SetStock(int stock);
+
+ Json::Value Serialize() const;
+ void Deserialize(const Json::Value& elm);
+};
+
+class FactoryItem : public ItemBase<FactoryItem>
+{
+private:
+ std::string mDescription;
+ std::string mEmail;
+
+public:
+ using ItemBase::ItemBase;
+
+ const std::string& GetDescription() const;
+ void SetDescription(std::string description);
+ const std::string& GetEmail() const;
+ void SetEmail(std::string email);
+
+ Json::Value Serialize() const;
+ void Deserialize(const Json::Value& elm);
+};
+
+class CustomerItem : public ItemBase<CustomerItem>
+{
+private:
+ std::string mDescription;
+ std::string mEmail;
+
+public:
+ using ItemBase::ItemBase;
+
+ const std::string& GetDescription() const;
+ void SetDescription(std::string description);
+ const std::string& GetEmail() const;
+ void SetEmail(std::string email);
+
+ Json::Value Serialize() const;
+ void Deserialize(const Json::Value& elm);
+};
diff --git a/app/source/Cplt/Model/Project.cpp b/app/source/Cplt/Model/Project.cpp
new file mode 100644
index 0000000..a1e9bab
--- /dev/null
+++ b/app/source/Cplt/Model/Project.cpp
@@ -0,0 +1,168 @@
+#include "Project.hpp"
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+#include <Cplt/Utils/Macros.hpp>
+
+#include <json/reader.h>
+#include <json/value.h>
+#include <json/writer.h>
+#include <filesystem>
+#include <fstream>
+#include <stdexcept>
+#include <utility>
+
+namespace fs = std::filesystem;
+
+template <class T>
+static void ReadItemList(ItemList<T>& list, const fs::path& filePath)
+{
+ std::ifstream ifs(filePath);
+ if (ifs) {
+ Json::Value root;
+ ifs >> root;
+
+ list = ItemList<T>(root);
+ }
+}
+
+static void CreateProjectSubfolders(const Project& project)
+{
+ fs::create_directory(project.GetDatabasesDirectory());
+ fs::create_directory(project.GetItemsDirectory());
+ fs::create_directory(project.GetWorkflowsDirectory());
+ fs::create_directory(project.GetTemplatesDirectory());
+}
+
+Project::Project(fs::path rootPath)
+ : mRootPath{ std::move(rootPath) }
+ , mRootPathString{ mRootPath.string() }
+ , Workflows(*this)
+ , Templates(*this)
+ , Database(*this)
+{
+ // TODO better diagnostic
+ const char* kInvalidFormatErr = "Failed to load project: invalid format.";
+
+ std::ifstream ifs(mRootPath / "cplt_project.json");
+ if (!ifs) {
+ std::string message;
+ message += "Failed to load project file at '";
+ message += mRootPath.string();
+ message += "'.";
+ throw std::runtime_error(message);
+ }
+
+ {
+ Json::Value root;
+ ifs >> root;
+
+ const auto& croot = root; // Use const reference so that accessors default to returning a null if not found, instead of silently creating new elements
+ if (!croot.isObject()) {
+ throw std::runtime_error(kInvalidFormatErr);
+ }
+
+ if (auto& name = croot["Name"]; name.isString()) {
+ mName = name.asString();
+ } else {
+ throw std::runtime_error(kInvalidFormatErr);
+ }
+ }
+
+ CreateProjectSubfolders(*this);
+
+ auto itemsDir = mRootPath / "items";
+ ReadItemList(Products, itemsDir / "products.json");
+ ReadItemList(Factories, itemsDir / "factories.json");
+ ReadItemList(Customers, itemsDir / "customers.json");
+
+ Workflows.Reload();
+ Templates.Reload();
+}
+
+Project::Project(fs::path rootPath, std::string name)
+ : mRootPath{ std::move(rootPath) }
+ , mRootPathString{ mRootPath.string() }
+ , mName{ std::move(name) }
+ , Workflows(*this)
+ , Templates(*this)
+ , Database(*this)
+{
+ CreateProjectSubfolders(*this);
+}
+
+const fs::path& Project::GetPath() const
+{
+ return mRootPath;
+}
+
+const std::string& Project::GetPathString() const
+{
+ return mRootPathString;
+}
+
+fs::path Project::GetDatabasesDirectory() const
+{
+ return mRootPath / "databases";
+}
+
+fs::path Project::GetItemsDirectory() const
+{
+ return mRootPath / "items";
+}
+
+fs::path Project::GetWorkflowsDirectory() const
+{
+ return mRootPath / "workflows";
+}
+
+fs::path Project::GetWorkflowPath(std::string_view name) const
+{
+ return (mRootPath / "workflows" / name).concat(".cplt-workflow");
+}
+
+fs::path Project::GetTemplatesDirectory() const
+{
+ return mRootPath / "templates";
+}
+
+fs::path Project::GetTemplatePath(std::string_view name) const
+{
+ return (mRootPath / "templates" / name).concat(".cplt-template");
+}
+
+const std::string& Project::GetName() const
+{
+ return mName;
+}
+
+void Project::SetName(std::string name)
+{
+ mName = std::move(name);
+}
+
+Json::Value Project::Serialize()
+{
+ Json::Value root(Json::objectValue);
+
+ root["Name"] = mName;
+
+ return root;
+}
+
+template <class T>
+static void WriteItemList(ItemList<T>& list, const fs::path& filePath)
+{
+ std::ofstream ofs(filePath);
+ ofs << list.Serialize();
+}
+
+void Project::WriteToDisk()
+{
+ std::ofstream ofs(mRootPath / "cplt_project.json");
+ ofs << this->Serialize();
+
+ auto itemsDir = GetItemsDirectory();
+ WriteItemList(Products, itemsDir / "products.json");
+ WriteItemList(Factories, itemsDir / "factories.json");
+ WriteItemList(Customers, itemsDir / "customers.json");
+}
diff --git a/app/source/Cplt/Model/Project.hpp b/app/source/Cplt/Model/Project.hpp
new file mode 100644
index 0000000..8119a97
--- /dev/null
+++ b/app/source/Cplt/Model/Project.hpp
@@ -0,0 +1,57 @@
+#pragma once
+
+#include <Cplt/Model/Assets.hpp>
+#include <Cplt/Model/Database.hpp>
+#include <Cplt/Model/Items.hpp>
+#include <Cplt/Model/Template/Template.hpp>
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+#include <json/forwards.h>
+#include <tsl/array_map.h>
+#include <filesystem>
+#include <string>
+#include <string_view>
+
+class Project
+{
+private:
+ std::filesystem::path mRootPath;
+ std::string mRootPathString;
+ std::string mName;
+
+ // (Exception to style guidelines)
+ // This is put after the private fields, so that when XxxDatabase's constructor runs, all of them will be initialized
+public:
+ WorkflowAssetList Workflows;
+ TemplateAssetList Templates;
+ ItemList<ProductItem> Products;
+ ItemList<FactoryItem> Factories;
+ ItemList<CustomerItem> Customers;
+ MainDatabase Database;
+
+public:
+ /// Load the project from a directory containing the cplt_project.json file.
+ /// This only loads the main project file, the caller needs to
+ Project(std::filesystem::path rootPath);
+
+ /// Create a project with the given name in the given path. Note that the path should be a directory that will contain the project files once created.
+ /// This function assumes the given directory will exist and is empty.
+ Project(std::filesystem::path rootPath, std::string name);
+
+ /// Path to a *directory* that contains the project file.
+ const std::filesystem::path& GetPath() const;
+ const std::string& GetPathString() const;
+
+ std::filesystem::path GetDatabasesDirectory() const;
+ std::filesystem::path GetItemsDirectory() const;
+ std::filesystem::path GetWorkflowsDirectory() const;
+ std::filesystem::path GetWorkflowPath(std::string_view name) const;
+ std::filesystem::path GetTemplatesDirectory() const;
+ std::filesystem::path GetTemplatePath(std::string_view name) const;
+
+ const std::string& GetName() const;
+ void SetName(std::string name);
+
+ Json::Value Serialize();
+ void WriteToDisk();
+};
diff --git a/app/source/Cplt/Model/Template/TableTemplate.cpp b/app/source/Cplt/Model/Template/TableTemplate.cpp
new file mode 100644
index 0000000..5cd9ed8
--- /dev/null
+++ b/app/source/Cplt/Model/Template/TableTemplate.cpp
@@ -0,0 +1,591 @@
+#include "TableTemplate.hpp"
+
+#include <Cplt/Utils/IO/StringIntegration.hpp>
+#include <Cplt/Utils/IO/TslArrayIntegration.hpp>
+#include <Cplt/Utils/IO/VectorIntegration.hpp>
+
+#include <xlsxwriter.h>
+#include <algorithm>
+#include <charconv>
+#include <cstddef>
+#include <cstdint>
+#include <iostream>
+#include <map>
+
+bool TableCell::IsDataHoldingCell() const
+{
+ return IsPrimaryCell() || !IsMergedCell();
+}
+
+bool TableCell::IsPrimaryCell() const
+{
+ return PrimaryCellLocation == Location;
+}
+
+bool TableCell::IsMergedCell() const
+{
+ return PrimaryCellLocation.x == -1 || PrimaryCellLocation.y == -1;
+}
+
+template <class TTableCell, class TStream>
+void OperateStreamForTableCell(TTableCell& cell, TStream& proxy)
+{
+ proxy.template ObjectAdapted<DataStreamAdapters::String>(cell.Content);
+ proxy.Object(cell.Location);
+ proxy.Object(cell.PrimaryCellLocation);
+ proxy.Value(cell.SpanX);
+ proxy.Value(cell.SpanY);
+ proxy.Enum(cell.HorizontalAlignment);
+ proxy.Enum(cell.VerticalAlignment);
+ proxy.Enum(cell.Type);
+ proxy.Value(cell.DataId);
+}
+
+void TableCell::ReadFromDataStream(InputDataStream& stream)
+{
+ ::OperateStreamForTableCell(*this, stream);
+}
+
+void TableCell::WriteToDataStream(OutputDataStream& stream) const
+{
+ ::OperateStreamForTableCell(*this, stream);
+}
+
+Vec2i TableArrayGroup::GetLeftCell() const
+{
+ return { Row, LeftCell };
+}
+
+Vec2i TableArrayGroup::GetRightCell() const
+{
+ return { Row, RightCell };
+}
+
+int TableArrayGroup::GetCount() const
+{
+ return RightCell - LeftCell + 1;
+}
+
+Vec2i TableArrayGroup::FindCell(std::string_view name)
+{
+ // TODO
+ return Vec2i{};
+}
+
+template <class TMap>
+static bool UpdateElementName(TMap& map, std::string_view oldName, std::string_view newName)
+{
+ auto iter = map.find(oldName);
+ if (iter == map.end()) {
+ return false;
+ }
+
+ auto elm = iter.value();
+ auto [DISCARD, inserted] = map.insert(newName, elm);
+ if (!inserted) {
+ return false;
+ }
+
+ map.erase(iter);
+ return true;
+}
+
+bool TableArrayGroup::UpdateCellName(std::string_view oldName, std::string_view newName)
+{
+ return ::UpdateElementName(mName2Cell, oldName, newName);
+}
+
+template <class TTableArrayGroup, class TStream>
+void OperateStreamForTableArrayGroup(TTableArrayGroup& group, TStream& stream)
+{
+ stream.Value(group.Row);
+ stream.Value(group.LeftCell);
+ stream.Value(group.RightCell);
+}
+
+void TableArrayGroup::ReadFromDataStream(InputDataStream& stream)
+{
+ ::OperateStreamForTableArrayGroup(*this, stream);
+}
+
+void TableArrayGroup::WriteToDataStream(OutputDataStream& stream) const
+{
+ ::OperateStreamForTableArrayGroup(*this, stream);
+}
+
+TableInstantiationParameters::TableInstantiationParameters(const TableTemplate& table)
+ : mTable{ &table }
+{
+}
+
+TableInstantiationParameters& TableInstantiationParameters::ResetTable(const TableTemplate& newTable)
+{
+ mTable = &newTable;
+ return *this;
+}
+
+TableInstantiationParameters TableInstantiationParameters::RebindTable(const TableTemplate& newTable) const
+{
+ TableInstantiationParameters result(newTable);
+ result.SingularCells = this->SingularCells;
+ result.ArrayGroups = this->ArrayGroups;
+ return result;
+}
+
+const TableTemplate& TableInstantiationParameters::GetTable() const
+{
+ return *mTable;
+}
+
+bool TableTemplate::IsInstance(const Template* tmpl)
+{
+ return tmpl->GetKind() == KD_Table;
+}
+
+TableTemplate::TableTemplate()
+ : Template(KD_Table)
+{
+}
+
+int TableTemplate::GetTableWidth() const
+{
+ return mColumnWidths.size();
+}
+
+int TableTemplate::GetTableHeight() const
+{
+ return mRowHeights.size();
+}
+
+void TableTemplate::Resize(int newWidth, int newHeight)
+{
+ // TODO this doesn't gracefully handle resizing to a smaller size which trims some merged cells
+
+ std::vector<TableCell> cells;
+ cells.reserve(newWidth * newHeight);
+
+ int tableWidth = GetTableWidth();
+ int tableHeight = GetTableHeight();
+
+ for (int y = 0; y < newHeight; ++y) {
+ if (y >= tableHeight) {
+ for (int x = 0; x < newWidth; ++x) {
+ cells.push_back(TableCell{});
+ }
+ continue;
+ }
+
+ for (int x = 0; x < newWidth; ++x) {
+ if (x >= tableWidth) {
+ cells.push_back(TableCell{});
+ } else {
+ auto& cell = GetCell({ x, y });
+ cells.push_back(std::move(cell));
+ }
+ }
+ }
+
+ mCells = std::move(cells);
+ mColumnWidths.resize(newWidth, 80);
+ mRowHeights.resize(newHeight, 20);
+}
+
+int TableTemplate::GetRowHeight(int row) const
+{
+ return mRowHeights[row];
+}
+
+void TableTemplate::SetRowHeight(int row, int height)
+{
+ mRowHeights[row] = height;
+}
+
+int TableTemplate::GetColumnWidth(int column) const
+{
+ return mColumnWidths[column];
+}
+
+void TableTemplate::SetColumnWidth(int column, int width)
+{
+ mColumnWidths[column] = width;
+}
+
+const TableCell& TableTemplate::GetCell(Vec2i pos) const
+{
+ int tableWidth = GetTableWidth();
+ return mCells[pos.y * tableWidth + pos.x];
+}
+
+TableCell& TableTemplate::GetCell(Vec2i pos)
+{
+ return const_cast<TableCell&>(const_cast<const TableTemplate*>(this)->GetCell(pos));
+}
+
+void TableTemplate::SetCellType(Vec2i pos, TableCell::CellType type)
+{
+ auto& cell = GetCell(pos);
+ if (cell.Type == type) {
+ return;
+ }
+
+ switch (cell.Type) {
+ // Nothing to change
+ case TableCell::ConstantCell: break;
+
+ case TableCell::SingularParametricCell:
+ mName2Parameters.erase(cell.Content);
+ break;
+
+ case TableCell::ArrayParametricCell: {
+ auto& ag = mArrayGroups[cell.DataId];
+ if (pos.x == ag.LeftCell) {
+ ag.LeftCell++;
+ } else if (pos.x == ag.RightCell) {
+ ag.RightCell--;
+ } else {
+ }
+ } break;
+ }
+
+ switch (type) {
+ // Nothing to do
+ case TableCell::ConstantCell: break;
+
+ case TableCell::SingularParametricCell: {
+ int idx = pos.y * GetTableWidth() + pos.x;
+ auto [DISCARD, inserted] = mName2Parameters.insert(cell.Content, idx);
+
+ // Duplicate name
+ if (!inserted) {
+ return;
+ }
+ } break;
+
+ case TableCell::ArrayParametricCell: {
+ auto ptr = AddArrayGroup(pos.y, pos.x, pos.x);
+
+ // Duplicate name
+ if (ptr == nullptr) {
+ return;
+ }
+ } break;
+ }
+
+ cell.Type = type;
+}
+
+bool TableTemplate::UpdateParameterName(std::string_view oldName, std::string_view newName)
+{
+ return ::UpdateElementName(mName2Parameters, oldName, newName);
+}
+
+int TableTemplate::GetArrayGroupCount() const
+{
+ return mArrayGroups.size();
+}
+
+const TableArrayGroup& TableTemplate::GetArrayGroup(int id) const
+{
+ return mArrayGroups[id];
+}
+
+TableArrayGroup& TableTemplate::GetArrayGroup(int id)
+{
+ return mArrayGroups[id];
+}
+
+TableArrayGroup* TableTemplate::AddArrayGroup(int row, int left, int right)
+{
+ // size_t max value: 18446744073709551615
+ // ^~~~~~~~~~~~~~~~~~~~ 20 chars
+ char name[20];
+ auto res = std::to_chars(std::begin(name), std::end(name), mArrayGroups.size());
+ std::string_view nameStr(name, res.ptr - name);
+
+ return AddArrayGroup(nameStr, row, left, right);
+}
+
+TableArrayGroup* TableTemplate::AddArrayGroup(std::string_view name, int row, int left, int right)
+{
+ assert(row >= 0 && row < GetTableHeight());
+ assert(left >= 0 && left < GetTableWidth());
+ assert(right >= 0 && right < GetTableWidth());
+
+ // TODO check for overlap
+
+ if (left > right) {
+ std::swap(left, right);
+ }
+
+ auto [DISCARD, inserted] = mName2ArrayGroups.insert(name, (int)mArrayGroups.size());
+ if (!inserted) {
+ return nullptr;
+ }
+
+ mArrayGroups.push_back(TableArrayGroup{
+ .Row = row,
+ .LeftCell = left,
+ .RightCell = right,
+ });
+ auto& ag = mArrayGroups.back();
+
+ for (int x = left; x <= right; x++) {
+ auto& cell = GetCell({ x, row });
+
+ // Update type
+ cell.Type = TableCell::ArrayParametricCell;
+
+ // Insert parameter name lookup
+ while (true) {
+ auto [DISCARD, inserted] = ag.mName2Cell.insert(cell.Content, x);
+ if (inserted) {
+ break;
+ }
+
+ cell.Content += "-";
+ }
+ }
+
+ return &ag;
+}
+
+bool TableTemplate::UpdateArrayGroupName(std::string_view oldName, std::string_view newName)
+{
+ return ::UpdateElementName(mName2ArrayGroups, oldName, newName);
+}
+
+bool TableTemplate::ExtendArrayGroupLeft(int id, int n)
+{
+ assert(n > 0);
+
+ auto& ag = mArrayGroups[id];
+ ag.LeftCell -= n;
+
+ return false;
+}
+
+bool TableTemplate::ExtendArrayGroupRight(int id, int n)
+{
+ assert(n > 0);
+
+ auto& ag = mArrayGroups[id];
+ ag.RightCell += n;
+
+ return false;
+}
+
+TableCell* TableTemplate::FindCell(std::string_view name)
+{
+ auto iter = mName2Parameters.find(name);
+ if (iter != mName2Parameters.end()) {
+ return &mCells[iter.value()];
+ } else {
+ return nullptr;
+ }
+}
+
+TableArrayGroup* TableTemplate::FindArrayGroup(std::string_view name)
+{
+ auto iter = mName2ArrayGroups.find(name);
+ if (iter != mName2ArrayGroups.end()) {
+ return &mArrayGroups[iter.value()];
+ } else {
+ return nullptr;
+ }
+}
+
+TableTemplate::MergeCellsResult TableTemplate::MergeCells(Vec2i topLeft, Vec2i bottomRight)
+{
+ auto SortTwo = [](int& a, int& b) {
+ if (a > b) {
+ std::swap(a, b);
+ }
+ };
+ SortTwo(topLeft.x, bottomRight.x);
+ SortTwo(topLeft.y, bottomRight.y);
+
+ auto ResetProgress = [&]() {
+ for (int y = topLeft.y; y < bottomRight.y; ++y) {
+ for (int x = topLeft.x; x < bottomRight.x; ++x) {
+ auto& cell = GetCell({ x, y });
+ cell.PrimaryCellLocation = { -1, -1 };
+ }
+ }
+ };
+
+ for (int y = topLeft.y; y < bottomRight.y; ++y) {
+ for (int x = topLeft.x; x < bottomRight.x; ++x) {
+ auto& cell = GetCell({ x, y });
+ if (cell.IsMergedCell()) {
+ ResetProgress();
+ return MCR_CellAlreadyMerged;
+ }
+
+ cell.PrimaryCellLocation = topLeft;
+ }
+ }
+
+ auto& primaryCell = GetCell(topLeft);
+ primaryCell.SpanX = bottomRight.x - topLeft.x;
+ primaryCell.SpanY = bottomRight.y - topLeft.y;
+
+ return MCR_Success;
+}
+
+TableTemplate::BreakCellsResult TableTemplate::BreakCells(Vec2i topLeft)
+{
+ auto& primaryCell = GetCell(topLeft);
+ if (!primaryCell.IsMergedCell()) {
+ return BCR_CellNotMerged;
+ }
+
+ for (int dy = 0; dy < primaryCell.SpanY; ++dy) {
+ for (int dx = 0; dx < primaryCell.SpanX; ++dx) {
+ auto& cell = GetCell({ topLeft.x + dx, topLeft.y + dy });
+ cell.PrimaryCellLocation = { -1, -1 };
+ }
+ }
+
+ primaryCell.SpanX = 1;
+ primaryCell.SpanY = 1;
+
+ return BCR_Success;
+}
+
+lxw_workbook* TableTemplate::InstantiateToExcelWorkbook(const TableInstantiationParameters& params) const
+{
+ auto workbook = workbook_new("Table.xlsx");
+ InstantiateToExcelWorksheet(workbook, params);
+ return workbook;
+}
+
+lxw_worksheet* TableTemplate::InstantiateToExcelWorksheet(lxw_workbook* workbook, const TableInstantiationParameters& params) const
+{
+ auto worksheet = workbook_add_worksheet(workbook, "CpltExport.xlsx");
+
+ // Map: row number -> length of generated ranges
+ std::map<int, int> generatedRanges;
+
+ for (size_t i = 0; i < mArrayGroups.size(); ++i) {
+ auto& info = mArrayGroups[i];
+ auto& param = params.ArrayGroups[i];
+
+ auto iter = generatedRanges.find(i);
+ if (iter != generatedRanges.end()) {
+ int available = iter->second;
+ if (available >= param.size()) {
+ // Current space is enough to fit in this array group, skip
+ continue;
+ }
+ }
+
+ // Not enough space to fit in this array group, update (or insert) the appropriate amount of generated rows
+ int row = i;
+ int count = param.size();
+ generatedRanges.try_emplace(row, count);
+ }
+
+ auto GetOffset = [&](int y) -> int {
+ // std::find_if <values less than y>
+ int verticalOffset = 0;
+ for (auto it = generatedRanges.begin(); it != generatedRanges.end() && it->first < y; ++it) {
+ verticalOffset += it->second;
+ }
+ return verticalOffset;
+ };
+
+ auto WriteCell = [&](int row, int col, const TableCell& cell, const char* text) -> void {
+ if (cell.IsPrimaryCell()) {
+ int lastRow = row + cell.SpanY - 1;
+ int lastCol = col + cell.SpanX - 1;
+ // When both `string` and `format` are null, the top-left cell contents are untouched (what we just wrote in the above switch)
+ worksheet_merge_range(worksheet, row, col, lastRow, lastCol, text, nullptr);
+ } else {
+ worksheet_write_string(worksheet, row, col, text, nullptr);
+ }
+ };
+
+ // Write/instantiate all array groups
+ for (size_t i = 0; i < mArrayGroups.size(); ++i) {
+ auto& groupInfo = mArrayGroups[i];
+ auto& groupParams = params.ArrayGroups[i];
+
+ int rowCellCount = groupInfo.GetCount();
+ int rowCount = groupParams.size();
+ int baseRowIdx = groupInfo.Row + GetOffset(groupInfo.Row);
+
+ // For each row that would be generated
+ for (int rowIdx = 0; rowIdx < rowCount; ++rowIdx) {
+ auto& row = groupParams[rowIdx];
+
+ // For each cell in the row
+ for (int rowCellIdx = 0; rowCellIdx < rowCellCount; ++rowCellIdx) {
+ // TODO support merged cells in array groups
+ worksheet_write_string(worksheet, baseRowIdx + rowIdx, rowCellIdx, row[rowCellIdx].c_str(), nullptr);
+ }
+ }
+ }
+
+ int tableWidth = GetTableWidth();
+ int tableHeight = GetTableHeight();
+
+ // Write all regular and singular parameter cells
+ for (int y = 0; y < tableHeight; ++y) {
+ for (int x = 0; x < tableWidth; ++x) {
+ auto& cell = GetCell({ x, y });
+
+ if (!cell.IsDataHoldingCell()) {
+ continue;
+ }
+
+ switch (cell.Type) {
+ case TableCell::ConstantCell: {
+ int row = y + GetOffset(y);
+ int col = x;
+
+ WriteCell(row, col, cell, cell.Content.c_str());
+ } break;
+
+ case TableCell::SingularParametricCell: {
+ int row = y + GetOffset(y);
+ int col = x;
+
+ auto iter = params.SingularCells.find({ x, y });
+ if (iter != params.SingularCells.end()) {
+ WriteCell(row, col, cell, iter.value().c_str());
+ }
+ } break;
+
+ // See loop above that processes whole array groups at the same time
+ case TableCell::ArrayParametricCell: break;
+ }
+ }
+ }
+
+ return worksheet;
+}
+
+class TableTemplate::Private
+{
+public:
+ template <class TTableTemplate, class TProxy>
+ static void OperateStream(TTableTemplate& table, TProxy& proxy)
+ {
+ proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mColumnWidths);
+ proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mRowHeights);
+ proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mCells);
+ proxy.template ObjectAdapted<DataStreamAdapters::Vector<>>(table.mArrayGroups);
+ proxy.template ObjectAdapted<DataStreamAdapters::TslArrayMap<>>(table.mName2Parameters);
+ proxy.template ObjectAdapted<DataStreamAdapters::TslArrayMap<>>(table.mName2ArrayGroups);
+ }
+};
+
+void TableTemplate::ReadFromDataStream(InputDataStream& stream)
+{
+ Private::OperateStream(*this, stream);
+}
+
+void TableTemplate::WriteToDataStream(OutputDataStream& stream) const
+{
+ Private::OperateStream(*this, stream);
+}
diff --git a/app/source/Cplt/Model/Template/TableTemplate.hpp b/app/source/Cplt/Model/Template/TableTemplate.hpp
new file mode 100644
index 0000000..3e931d4
--- /dev/null
+++ b/app/source/Cplt/Model/Template/TableTemplate.hpp
@@ -0,0 +1,223 @@
+#pragma once
+
+#include <Cplt/Model/Template/Template.hpp>
+#include <Cplt/Utils/Vector.hpp>
+#include <Cplt/Utils/VectorHash.hpp>
+#include <Cplt/fwd.hpp>
+
+#include <tsl/array_map.h>
+#include <tsl/robin_map.h>
+#include <string>
+#include <string_view>
+#include <vector>
+
+class TableCell
+{
+public:
+ enum TextAlignment
+ {
+ /// For horizontal alignment, this means align left. For vertical alignment, this means align top.
+ AlignAxisMin,
+ /// Align middle of the text to the middle of the axis.
+ AlignCenter,
+ /// For horizontal alignment, this means align right. For vertical alignment, this means align bottom.
+ AlignAxisMax,
+ };
+
+ enum CellType
+ {
+ ConstantCell,
+ SingularParametricCell,
+ ArrayParametricCell,
+ };
+
+public:
+ /// Display content of this cell. This doesn't necessarily have to line up with the parameter name (if this cell is one).
+ std::string Content;
+ Vec2i Location;
+ /// Location of the primary (top left) cell, if this cell is a part of a merged group.
+ /// Otherwise, either component of this field shall be -1.
+ Vec2i PrimaryCellLocation{ -1, -1 };
+ int SpanX = 0;
+ int SpanY = 0;
+ TextAlignment HorizontalAlignment = AlignCenter;
+ TextAlignment VerticalAlignment = AlignCenter;
+ CellType Type = ConstantCell;
+ /// The id of the group description object, if this cell isn't a constant or singular parameter cell. Otherwise, this value is -1.
+ int DataId = -1;
+
+public:
+ /// Return whether this cell holds meaningful data, i.e. true when this cell is either unmerged or the primary cell of a merged range.
+ bool IsDataHoldingCell() const;
+ /// Return whether this cell is the primary (i.e. top left) cell of a merged range or not.
+ bool IsPrimaryCell() const;
+ /// Return whether this cell is a part of a merged range or not. Includes the primary cell.
+ bool IsMergedCell() const;
+
+ void ReadFromDataStream(InputDataStream& stream);
+ void WriteToDataStream(OutputDataStream& stream) const;
+};
+
+// TODO support reverse (bottom to top) filling order
+// TODO support horizontal filling order
+
+/// Parameter group information for a grouped array of cells. When instantiated, an array of 0 or more
+/// elements shall be provided by the user, which will replace the group of templated cells with a list
+/// of rows, each instantiated with the n-th element in the provided array.
+/// \code
+/// [["foo", "bar", "foobar"],
+/// ["a", "b", c"],
+/// ["1", "2", "3"],
+/// ["x", "y", "z"]]
+/// // ... may be more
+/// \endcode
+/// This would create 4 rows of data in the place of the original parameter group.
+///
+/// If more than one array parameter groups are on the same row, they would share space between each other:
+/// \code
+/// | 2 elements was fed to it
+/// | | 1 element was fed to it
+/// V V
+/// {~~~~~~~~~~~~~~~~}{~~~~~~~~~~~~~~}
+/// +------+---------+---------------+
+/// | Foo | Example | Another group |
+/// +------+---------+---------------+
+/// | Cool | Example | |
+/// +------+---------+---------------+
+/// \endcode
+///
+/// \see TableCell
+/// \see TableInstantiationParameters
+/// \see TableTemplate
+class TableArrayGroup
+{
+public:
+ /// Parameter name mapped to cell location (index from LeftCell).
+ tsl::array_map<char, int> mName2Cell;
+ int Row;
+ /// Leftmost cell in this group
+ int LeftCell;
+ /// Rightmost cell in this group
+ int RightCell;
+
+public:
+ Vec2i GetLeftCell() const;
+ Vec2i GetRightCell() const;
+ int GetCount() const;
+
+ /// Find the location of the cell within this array group that has the given name.
+ Vec2i FindCell(std::string_view name);
+ bool UpdateCellName(std::string_view oldName, std::string_view newName);
+
+ void ReadFromDataStream(InputDataStream& stream);
+ void WriteToDataStream(OutputDataStream& stream) const;
+};
+
+// Forward declaration of libxlsxwriter structs
+struct lxw_workbook;
+struct lxw_worksheet;
+
+/// An object containing the necessary information to instantiate a table template.
+/// \see TableTemplate
+class TableInstantiationParameters
+{
+private:
+ const TableTemplate* mTable;
+
+public:
+ tsl::robin_map<Vec2i, std::string> SingularCells;
+
+ using ArrayGroupRow = std::vector<std::string>;
+ using ArrayGroupData = std::vector<ArrayGroupRow>;
+ std::vector<ArrayGroupData> ArrayGroups;
+
+public:
+ TableInstantiationParameters(const TableTemplate& table);
+
+ TableInstantiationParameters& ResetTable(const TableTemplate& newTable);
+ TableInstantiationParameters RebindTable(const TableTemplate& newTable) const;
+
+ const TableTemplate& GetTable() const;
+};
+
+/// A table template, where individual cells can be filled by workflows instantiating this template. Merged cells,
+/// parametric rows/columns, and grids are also supported.
+///
+/// This current supports exporting to xlsx files.
+class TableTemplate : public Template
+{
+ friend class TableSingleParamsIter;
+ friend class TableArrayGroupsIter;
+ class Private;
+
+private:
+ /// Map from parameter name to index of the parameter cell (stored in mCells).
+ tsl::array_map<char, int> mName2Parameters;
+ /// Map from array group name to the index of the array group (stored in mArrayGroups).
+ tsl::array_map<char, int> mName2ArrayGroups;
+ std::vector<TableCell> mCells;
+ std::vector<TableArrayGroup> mArrayGroups;
+ std::vector<int> mRowHeights;
+ std::vector<int> mColumnWidths;
+
+public:
+ static bool IsInstance(const Template* tmpl);
+ TableTemplate();
+
+ int GetTableWidth() const;
+ int GetTableHeight() const;
+ void Resize(int newWidth, int newHeight);
+
+ int GetRowHeight(int row) const;
+ void SetRowHeight(int row, int height);
+ int GetColumnWidth(int column) const;
+ void SetColumnWidth(int column, int width);
+
+ const TableCell& GetCell(Vec2i pos) const;
+ TableCell& GetCell(Vec2i pos);
+ /// <ul>
+ /// <li> In case of becoming a SingularParametricCell: the parameter name is filled with TableCell::Content.
+ /// <li> In case of becoming a ArrayGroupParametricCell: the array group name is automatically generated as the nth group it would be come.
+ /// i.e., if there aRe currently 3 groups, the newly created group would be named "4".
+ /// If this name collides with an existing group, hyphens \c - will be append to the name until no collision happens.
+ /// </ul>
+ void SetCellType(Vec2i pos, TableCell::CellType type);
+
+ /// Updates the parameter cell to a new name. Returns true on success and false on failure (param not found or name duplicates).
+ bool UpdateParameterName(std::string_view oldName, std::string_view newName);
+
+ int GetArrayGroupCount() const;
+ const TableArrayGroup& GetArrayGroup(int id) const;
+ TableArrayGroup& GetArrayGroup(int id);
+ TableArrayGroup* AddArrayGroup(int row, int left, int right);
+ TableArrayGroup* AddArrayGroup(std::string_view name, int row, int left, int right);
+ bool UpdateArrayGroupName(std::string_view oldName, std::string_view newName);
+ bool ExtendArrayGroupLeft(int id, int n);
+ bool ExtendArrayGroupRight(int id, int n);
+
+ /// Find a singular parameter cell by its name. This does not include cells within an array group.
+ TableCell* FindCell(std::string_view name);
+
+ /// Find an array group by its name.
+ TableArrayGroup* FindArrayGroup(std::string_view name);
+
+ enum MergeCellsResult
+ {
+ MCR_CellAlreadyMerged,
+ MCR_Success,
+ };
+ MergeCellsResult MergeCells(Vec2i topLeft, Vec2i bottomRight);
+
+ enum BreakCellsResult
+ {
+ BCR_CellNotMerged,
+ BCR_Success,
+ };
+ BreakCellsResult BreakCells(Vec2i topLeft);
+
+ lxw_workbook* InstantiateToExcelWorkbook(const TableInstantiationParameters& params) const;
+ lxw_worksheet* InstantiateToExcelWorksheet(lxw_workbook* workbook, const TableInstantiationParameters& params) const;
+
+ void ReadFromDataStream(InputDataStream& stream) override;
+ void WriteToDataStream(OutputDataStream& stream) const override;
+};
diff --git a/app/source/Cplt/Model/Template/TableTemplateIterator.cpp b/app/source/Cplt/Model/Template/TableTemplateIterator.cpp
new file mode 100644
index 0000000..19e30b9
--- /dev/null
+++ b/app/source/Cplt/Model/Template/TableTemplateIterator.cpp
@@ -0,0 +1,52 @@
+#include "TableTemplateIterator.hpp"
+
+TableSingleParamsIter::TableSingleParamsIter(TableTemplate& tmpl)
+ : mTemplate{ &tmpl }
+ , mIter{ tmpl.mName2Parameters.begin() }
+{
+}
+
+bool TableSingleParamsIter::HasNext() const
+{
+ return mIter != mTemplate->mName2Parameters.end();
+}
+
+TableCell& TableSingleParamsIter::Next()
+{
+ int id = mIter.value();
+ ++mIter;
+
+ return mTemplate->mCells[id];
+}
+
+TableArrayGroupsIter::TableArrayGroupsIter(TableTemplate& tmpl)
+ : mTemplate{ &tmpl }
+ , mIter{ tmpl.mName2ArrayGroups.begin() }
+{
+}
+
+bool TableArrayGroupsIter::HasNext() const
+{
+ return mIter != mTemplate->mName2ArrayGroups.end();
+}
+
+TableArrayGroup& TableArrayGroupsIter::Peek() const
+{
+ int id = mIter.value();
+ return mTemplate->mArrayGroups[id];
+}
+
+std::string_view TableArrayGroupsIter::PeekName() const
+{
+ return mIter.key_sv();
+}
+
+const char* TableArrayGroupsIter::PeekNameCStr() const
+{
+ return mIter.key();
+}
+
+void TableArrayGroupsIter::Next()
+{
+ ++mIter;
+}
diff --git a/app/source/Cplt/Model/Template/TableTemplateIterator.hpp b/app/source/Cplt/Model/Template/TableTemplateIterator.hpp
new file mode 100644
index 0000000..c4b5bf9
--- /dev/null
+++ b/app/source/Cplt/Model/Template/TableTemplateIterator.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <Cplt/Model/Template/TableTemplate.hpp>
+#include <Cplt/Model/Template/Template.hpp>
+
+#include <string_view>
+
+class TableSingleParamsIter
+{
+private:
+ TableTemplate* mTemplate;
+ tsl::array_map<char, int>::iterator mIter;
+
+public:
+ TableSingleParamsIter(TableTemplate& tmpl);
+
+ bool HasNext() const;
+ TableCell& Next();
+};
+
+class TableArrayGroupsIter
+{
+private:
+ TableTemplate* mTemplate;
+ tsl::array_map<char, int>::iterator mIter;
+
+public:
+ TableArrayGroupsIter(TableTemplate& tmpl);
+
+ bool HasNext() const;
+ TableArrayGroup& Peek() const;
+ std::string_view PeekName() const;
+ const char* PeekNameCStr() const;
+ void Next();
+};
diff --git a/app/source/Cplt/Model/Template/Template.hpp b/app/source/Cplt/Model/Template/Template.hpp
new file mode 100644
index 0000000..cf926d0
--- /dev/null
+++ b/app/source/Cplt/Model/Template/Template.hpp
@@ -0,0 +1,68 @@
+#pragma once
+
+#include <Cplt/Model/Assets.hpp>
+#include <Cplt/fwd.hpp>
+
+#include <filesystem>
+#include <iosfwd>
+#include <memory>
+#include <string>
+
+class Template : public Asset
+{
+public:
+ enum Kind
+ {
+ KD_Table,
+
+ InvalidKind,
+ KindCount = InvalidKind,
+ };
+
+ using CategoryType = TemplateAssetList;
+
+private:
+ Kind mKind;
+
+public:
+ static const char* FormatKind(Kind kind);
+ static std::unique_ptr<Template> CreateByKind(Kind kind);
+
+ static bool IsInstance(const Template* tmpl);
+
+ Template(Kind kind);
+ ~Template() override = default;
+
+ Kind GetKind() const;
+
+ virtual void ReadFromDataStream(InputDataStream& stream) = 0;
+ virtual void WriteToDataStream(OutputDataStream& stream) const = 0;
+};
+
+class TemplateAssetList final : public AssetListTyped<Template>
+{
+private:
+ // AC = Asset Creator
+ std::string mACNewName;
+ NameSelectionError mACNewNameError = NameSelectionError::Empty;
+ Template::Kind mACNewKind = Template::InvalidKind;
+
+public:
+ // Inherit constructors
+ using AssetListTyped::AssetListTyped;
+
+protected:
+ void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const override;
+
+ std::string RetrieveNameFromFile(const std::filesystem::path& file) const override;
+ uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const override;
+ std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const override;
+
+ bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const override;
+ Template* LoadInstance(const SavedAsset& assetInfo) const override;
+ Template* CreateInstance(const SavedAsset& assetInfo) const override;
+ bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const override;
+
+ void DisplayAssetCreator(ListState& state) override;
+ void DisplayDetailsTable(ListState& state) const override;
+};
diff --git a/app/source/Cplt/Model/Template/Template_Main.cpp b/app/source/Cplt/Model/Template/Template_Main.cpp
new file mode 100644
index 0000000..d658231
--- /dev/null
+++ b/app/source/Cplt/Model/Template/Template_Main.cpp
@@ -0,0 +1,214 @@
+#include "Template.hpp"
+
+#include <Cplt/Model/GlobalStates.hpp>
+#include <Cplt/Model/Project.hpp>
+#include <Cplt/UI/UI.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/IO/Archive.hpp>
+#include <Cplt/Utils/UUID.hpp>
+
+#include <imgui.h>
+#include <imgui_stdlib.h>
+#include <algorithm>
+#include <cstdint>
+#include <fstream>
+
+using namespace std::literals::string_view_literals;
+namespace fs = std::filesystem;
+
+Template::Template(Kind kind)
+ : mKind{ kind }
+{
+}
+
+Template::Kind Template::GetKind() const
+{
+ return mKind;
+}
+
+void TemplateAssetList::DiscoverFiles(const std::function<void(SavedAsset)>& callback) const
+{
+ auto dir = GetConnectedProject().GetTemplatesDirectory();
+ DiscoverFilesByExtension(callback, dir, ".cplt-template"sv);
+}
+
+std::string TemplateAssetList::RetrieveNameFromFile(const fs::path& file) const
+{
+ auto res = DataArchive::LoadFile(file);
+ if (!res) return "";
+ auto& stream = res.value();
+
+ SavedAsset assetInfo;
+ stream.ReadObject(assetInfo);
+
+ return assetInfo.Name;
+}
+
+uuids::uuid TemplateAssetList::RetrieveUuidFromFile(const fs::path& file) const
+{
+ return uuids::uuid::from_string(file.stem().string());
+}
+
+fs::path TemplateAssetList::RetrievePathFromAsset(const SavedAsset& asset) const
+{
+ auto fileName = uuids::to_string(asset.Uuid);
+ return GetConnectedProject().GetTemplatePath(fileName);
+}
+
+bool TemplateAssetList::SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const
+{
+ auto path = RetrievePathFromAsset(assetInfo);
+ auto res = DataArchive::SaveFile(path);
+ if (!res) return false;
+ auto& stream = res.value();
+
+ stream.WriteObject(assetInfo);
+ // This cast is fine: calls to this class will always be wrapped in TypedAssetList<T>, which will ensure `asset` points to some Template
+ if (auto tmpl = static_cast<const Template*>(asset)) { // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
+ stream.WriteObject(*tmpl);
+ }
+
+ return true;
+}
+
+static std::unique_ptr<Template> LoadTemplateFromFile(const fs::path& path)
+{
+ auto res = DataArchive::LoadFile(path);
+ if (!res) return nullptr;
+ auto& stream = res.value();
+
+ SavedAsset assetInfo;
+ stream.ReadObject(assetInfo);
+
+ auto kind = static_cast<Template::Kind>(assetInfo.Payload);
+ auto tmpl = Template::CreateByKind(kind);
+ stream.ReadObject(*tmpl);
+
+ return tmpl;
+}
+
+Template* TemplateAssetList::LoadInstance(const SavedAsset& assetInfo) const
+{
+ return ::LoadTemplateFromFile(RetrievePathFromAsset(assetInfo)).release();
+}
+
+Template* TemplateAssetList::CreateInstance(const SavedAsset& assetInfo) const
+{
+ auto kind = static_cast<Template::Kind>(assetInfo.Payload);
+ return Template::CreateByKind(kind).release();
+}
+
+bool TemplateAssetList::RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const
+{
+ // Get asset path, which is only dependent on UUID
+ auto path = RetrievePathFromAsset(assetInfo);
+
+ auto tmpl = ::LoadTemplateFromFile(path);
+ if (!tmpl) return false;
+
+ // Rewrite the asset with the updated name (note the given assetInfo already has the update name)
+ SaveInstance(assetInfo, tmpl.get());
+
+ return true;
+}
+
+void TemplateAssetList::DisplayAssetCreator(ListState& state)
+{
+ auto ValidateNewName = [&]() -> void {
+ if (mACNewName.empty()) {
+ mACNewNameError = NameSelectionError::Empty;
+ return;
+ }
+
+ if (FindByName(mACNewName)) {
+ mACNewNameError = NameSelectionError::Duplicated;
+ return;
+ }
+
+ mACNewNameError = NameSelectionError::None;
+ };
+
+ auto ShowNewNameErrors = [&]() -> void {
+ switch (mACNewNameError) {
+ case NameSelectionError::None: break;
+ case NameSelectionError::Duplicated:
+ ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR));
+ break;
+ case NameSelectionError::Empty:
+ ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR));
+ break;
+ }
+ };
+
+ auto ShowNewKindErrors = [&]() -> void {
+ if (mACNewKind == Template::InvalidKind) {
+ ImGui::ErrorMessage(I18N_TEXT("Invalid template type", L10N_TEMPLATE_INVALID_TYPE_ERROR));
+ }
+ };
+
+ auto IsInputValid = [&]() -> bool {
+ return mACNewNameError == NameSelectionError::None &&
+ mACNewKind != Template::InvalidKind;
+ };
+
+ auto ResetState = [&]() -> void {
+ mACNewName.clear();
+ mACNewKind = Template::InvalidKind;
+ ValidateNewName();
+ };
+
+ if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &mACNewName)) {
+ ValidateNewName();
+ }
+
+ if (ImGui::BeginCombo(I18N_TEXT("Type", L10N_TYPE), Template::FormatKind(mACNewKind))) {
+ for (int i = 0; i < Template::KindCount; ++i) {
+ auto kind = static_cast<Template::Kind>(i);
+ if (ImGui::Selectable(Template::FormatKind(kind), mACNewKind == kind)) {
+ mACNewKind = kind;
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ ShowNewNameErrors();
+ ShowNewKindErrors();
+
+ if (ImGui::Button(I18N_TEXT("OK", L10N_CONFIRM), !IsInputValid())) {
+ ImGui::CloseCurrentPopup();
+
+ Create(SavedAsset{
+ .Name = mACNewName,
+ .Payload = static_cast<uint64_t>(mACNewKind),
+ });
+ ResetState();
+ }
+ ImGui::SameLine();
+ if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) {
+ ImGui::CloseCurrentPopup();
+ }
+}
+
+void TemplateAssetList::DisplayDetailsTable(ListState& state) const
+{
+ ImGui::BeginTable("AssetDetailsTable", 2, ImGuiTableFlags_Borders);
+
+ ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_NAME));
+ ImGui::TableSetupColumn(I18N_TEXT("Type", L10N_TYPE));
+ ImGui::TableHeadersRow();
+
+ for (auto& asset : this->GetAssets()) {
+ ImGui::TableNextRow();
+
+ ImGui::TableNextColumn();
+ if (ImGui::Selectable(asset.Name.c_str(), state.SelectedAsset == &asset, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_DontClosePopups)) {
+ state.SelectedAsset = &asset;
+ }
+
+ ImGui::TableNextColumn();
+ auto kind = static_cast<Template::Kind>(asset.Payload);
+ ImGui::TextUnformatted(Template::FormatKind(kind));
+ }
+
+ ImGui::EndTable();
+}
diff --git a/app/source/Cplt/Model/Template/Template_RTTI.cpp b/app/source/Cplt/Model/Template/Template_RTTI.cpp
new file mode 100644
index 0000000..a96680b
--- /dev/null
+++ b/app/source/Cplt/Model/Template/Template_RTTI.cpp
@@ -0,0 +1,29 @@
+#include "Template.hpp"
+
+#include <Cplt/Model/Template/TableTemplate.hpp>
+#include <Cplt/Utils/I18n.hpp>
+
+const char* Template::FormatKind(Kind kind)
+{
+ switch (kind) {
+ case KD_Table: return I18N_TEXT("Table template", L10N_TEMPLATE_TABLE);
+
+ case InvalidKind: break;
+ }
+ return "";
+}
+
+std::unique_ptr<Template> Template::CreateByKind(Kind kind)
+{
+ switch (kind) {
+ case KD_Table: return std::make_unique<TableTemplate>();
+
+ case InvalidKind: break;
+ }
+ return nullptr;
+}
+
+bool Template::IsInstance(const Template* tmpl)
+{
+ return true;
+}
diff --git a/app/source/Cplt/Model/Template/fwd.hpp b/app/source/Cplt/Model/Template/fwd.hpp
new file mode 100644
index 0000000..8378871
--- /dev/null
+++ b/app/source/Cplt/Model/Template/fwd.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+// TableTemplate.hpp
+class TableCell;
+class TableArrayGroup;
+class TableInstantiationParameters;
+class TableTemplate;
+
+// Template.hpp
+class Template;
+class TemplateAssetList;
diff --git a/app/source/Cplt/Model/Workflow/Evaluation.cpp b/app/source/Cplt/Model/Workflow/Evaluation.cpp
new file mode 100644
index 0000000..7035bf9
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Evaluation.cpp
@@ -0,0 +1,174 @@
+#include "Evaluation.hpp"
+
+#include <queue>
+
+const char* WorkflowEvaluationError::FormatMessageType(enum MessageType messageType)
+{
+ switch (messageType) {
+ case Error: return "Error";
+ case Warning: return "Warning";
+ }
+}
+
+const char* WorkflowEvaluationError::FormatPinType(enum PinType pinType)
+{
+ switch (pinType) {
+ case NoPin: return nullptr;
+ case InputPin: return "Input pin";
+ case OutputPin: return "Output pin";
+ }
+}
+
+std::string WorkflowEvaluationError::Format() const
+{
+ // TODO convert to std::format
+
+ std::string result;
+ result += FormatMessageType(this->Type);
+ result += " at ";
+ result += NodeId;
+ if (auto pinText = FormatPinType(this->PinType)) {
+ result += "/";
+ result += pinText;
+ result += " ";
+ result += PinId;
+ }
+ result += ": ";
+ result += this->Message;
+
+ return result;
+}
+
+struct WorkflowEvaluationContext::RuntimeNode
+{
+ enum EvaluationStatus
+ {
+ ST_Unevaluated,
+ ST_Success,
+ ST_Failed,
+ };
+
+ EvaluationStatus Status = ST_Unevaluated;
+};
+
+struct WorkflowEvaluationContext::RuntimeConnection
+{
+ std::unique_ptr<BaseValue> Value;
+
+ bool IsAvailableValue() const
+ {
+ return Value != nullptr;
+ }
+};
+
+WorkflowEvaluationContext::WorkflowEvaluationContext(Workflow& workflow)
+ : mWorkflow{ &workflow }
+{
+ mRuntimeNodes.resize(workflow.mNodes.size());
+ mRuntimeConnections.resize(workflow.mConnections.size());
+}
+
+BaseValue* WorkflowEvaluationContext::GetConnectionValue(size_t id, bool constant)
+{
+ if (constant) {
+ return mWorkflow->GetConstantById(id);
+ } else {
+ return mRuntimeConnections[id].Value.get();
+ }
+}
+
+BaseValue* WorkflowEvaluationContext::GetConnectionValue(const WorkflowNode::InputPin& inputPin)
+{
+ if (inputPin.IsConnected()) {
+ return GetConnectionValue(inputPin.Connection, inputPin.IsConstantConnection());
+ } else {
+ return nullptr;
+ }
+}
+
+void WorkflowEvaluationContext::SetConnectionValue(size_t id, std::unique_ptr<BaseValue> value)
+{
+ mRuntimeConnections[id].Value = std::move(value);
+}
+
+void WorkflowEvaluationContext::SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value)
+{
+ if (outputPin.IsConnected()) {
+ SetConnectionValue(outputPin.Connection, std::move(value));
+ }
+}
+
+void WorkflowEvaluationContext::Run()
+{
+ int evaluatedCount = 0;
+ int erroredCount = 0;
+
+ for (auto& depthGroup : mWorkflow->GetDepthGroups()) {
+ for (size_t idx : depthGroup) {
+ auto& rn = mRuntimeNodes[idx];
+ auto& n = *mWorkflow->mNodes[idx];
+
+ // TODO
+
+ int preEvalErrors = mErrors.size();
+ n.Evaluate(*this);
+ if (preEvalErrors != mErrors.size()) {
+ erroredCount++;
+ } else {
+ evaluatedCount++;
+ }
+ }
+ }
+
+ for (size_t i = 0; i < mRuntimeNodes.size(); ++i) {
+ auto& rn = mRuntimeNodes[i];
+ auto& n = *mWorkflow->mNodes[i];
+ if (n.GetType() == WorkflowNode::OutputType) {
+ // TODO record outputs
+ }
+ }
+}
+
+void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin)
+{
+ mErrors.push_back(WorkflowEvaluationError{
+ .Message = std::move(message),
+ .NodeId = node.GetId(),
+ .PinId = pinId,
+ .PinType = inputPin ? WorkflowEvaluationError::InputPin : WorkflowEvaluationError::OutputPin,
+ .Type = WorkflowEvaluationError::Error,
+ });
+}
+
+void WorkflowEvaluationContext::ReportError(std::string message, const WorkflowNode& node)
+{
+ mErrors.push_back(WorkflowEvaluationError{
+ .Message = std::move(message),
+ .NodeId = node.GetId(),
+ .PinId = -1,
+ .PinType = WorkflowEvaluationError::NoPin,
+ .Type = WorkflowEvaluationError::Error,
+ });
+}
+
+void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin)
+{
+ mErrors.push_back(WorkflowEvaluationError{
+ .Message = std::move(message),
+ .NodeId = node.GetId(),
+ .PinId = pinId,
+ .PinType = inputPin ? WorkflowEvaluationError::InputPin : WorkflowEvaluationError::OutputPin,
+ .Type = WorkflowEvaluationError::Warning,
+ });
+}
+
+void WorkflowEvaluationContext::ReportWarning(std::string message, const WorkflowNode& node)
+{
+ mErrors.push_back(WorkflowEvaluationError{
+ .Message = std::move(message),
+ .NodeId = node.GetId(),
+ .PinId = -1,
+ .PinType = WorkflowEvaluationError::NoPin,
+ .Type = WorkflowEvaluationError::Warning,
+ });
+}
diff --git a/app/source/Cplt/Model/Workflow/Evaluation.hpp b/app/source/Cplt/Model/Workflow/Evaluation.hpp
new file mode 100644
index 0000000..5b8c6cc
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Evaluation.hpp
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+#include <cstddef>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+class WorkflowEvaluationError
+{
+public:
+ enum MessageType : int16_t
+ {
+ Error,
+ Warning,
+ };
+
+ enum PinType : int16_t
+ {
+ NoPin,
+ InputPin,
+ OutputPin,
+ };
+
+public:
+ std::string Message;
+ size_t NodeId;
+ int PinId;
+ PinType PinType;
+ MessageType Type;
+
+public:
+ static const char* FormatMessageType(enum MessageType messageType);
+ static const char* FormatPinType(enum PinType pinType);
+
+ std::string Format() const;
+};
+
+class WorkflowEvaluationContext
+{
+private:
+ struct RuntimeNode;
+ struct RuntimeConnection;
+
+ Workflow* mWorkflow;
+ std::vector<RuntimeNode> mRuntimeNodes;
+ std::vector<RuntimeConnection> mRuntimeConnections;
+ std::vector<WorkflowEvaluationError> mErrors;
+ std::vector<WorkflowEvaluationError> mWarnings;
+
+public:
+ WorkflowEvaluationContext(Workflow& workflow);
+
+ BaseValue* GetConnectionValue(size_t id, bool constant);
+ BaseValue* GetConnectionValue(const WorkflowNode::InputPin& inputPin);
+ void SetConnectionValue(size_t id, std::unique_ptr<BaseValue> value);
+ void SetConnectionValue(const WorkflowNode::OutputPin& outputPin, std::unique_ptr<BaseValue> value);
+
+ void ReportError(std::string message, const WorkflowNode& node, int pinId, bool inputPin);
+ void ReportError(std::string message, const WorkflowNode& node);
+ void ReportWarning(std::string message, const WorkflowNode& node, int pinId, bool inputPin);
+ void ReportWarning(std::string message, const WorkflowNode& node);
+
+ /// Run until all possible paths have been evaluated.
+ void Run();
+};
diff --git a/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp
new file mode 100644
index 0000000..df4a8bb
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.cpp
@@ -0,0 +1,18 @@
+#include "DocumentNodes.hpp"
+
+#include <Cplt/Model/Workflow/Evaluation.hpp>
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+
+bool DocumentTemplateExpansionNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() == KD_DocumentTemplateExpansion;
+}
+
+DocumentTemplateExpansionNode::DocumentTemplateExpansionNode()
+ : WorkflowNode(KD_DocumentTemplateExpansion, false)
+{
+}
+
+void DocumentTemplateExpansionNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+}
diff --git a/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp
new file mode 100644
index 0000000..a266b2c
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/DocumentNodes.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+class DocumentTemplateExpansionNode : public WorkflowNode
+{
+public:
+ static bool IsInstance(const WorkflowNode* node);
+ DocumentTemplateExpansionNode();
+
+ // TODO
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+};
diff --git a/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp
new file mode 100644
index 0000000..f8b29bb
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.cpp
@@ -0,0 +1,94 @@
+#include "NumericNodes.hpp"
+
+#include <Cplt/Model/Workflow/Evaluation.hpp>
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/Macros.hpp>
+#include <Cplt/Utils/RTTI.hpp>
+
+#include <cassert>
+#include <utility>
+
+WorkflowNode::Kind NumericOperationNode::OperationTypeToNodeKind(OperationType type)
+{
+ switch (type) {
+ case Addition: return KD_NumericAddition;
+ case Subtraction: return KD_NumericSubtraction;
+ case Multiplication: return KD_NumericMultiplication;
+ case Division: return KD_NumericDivision;
+ default: return InvalidKind;
+ }
+}
+
+NumericOperationNode::OperationType NumericOperationNode::NodeKindToOperationType(Kind kind)
+{
+ switch (kind) {
+ case KD_NumericAddition: return Addition;
+ case KD_NumericSubtraction: return Subtraction;
+ case KD_NumericMultiplication: return Multiplication;
+ case KD_NumericDivision: return Division;
+ default: return InvalidType;
+ }
+}
+
+bool NumericOperationNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() >= KD_NumericAddition && node->GetKind() <= KD_NumericDivision;
+}
+
+NumericOperationNode::NumericOperationNode(OperationType type)
+ : WorkflowNode(OperationTypeToNodeKind(type), false)
+ , mType{ type }
+{
+ mInputs.resize(2);
+ mInputs[0].MatchingType = BaseValue::KD_Numeric;
+ mInputs[1].MatchingType = BaseValue::KD_Numeric;
+
+ mOutputs.resize(1);
+ mOutputs[0].MatchingType = BaseValue::KD_Numeric;
+}
+
+void NumericOperationNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+ auto lhsVal = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[0]));
+ if (!lhsVal) return;
+ double lhs = lhsVal->GetValue();
+
+ auto rhsVal = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[1]));
+ if (!rhsVal) return;
+ double rhs = rhsVal->GetValue();
+
+ double res;
+ switch (mType) {
+ case Addition: res = lhs + rhs; break;
+ case Subtraction: res = lhs - rhs; break;
+ case Multiplication: res = lhs * rhs; break;
+ case Division: {
+ if (rhs == 0.0) {
+ ctx.ReportError(I18N_TEXT("Error: division by 0", L10N_WORKFLOW_RTERROR_DIV_BY_0), *this);
+ return;
+ }
+ res = lhs / rhs;
+ } break;
+
+ default: return;
+ }
+
+ auto value = std::make_unique<NumericValue>();
+ value->SetValue(res);
+ ctx.SetConnectionValue(mOutputs[0], std::move(value));
+}
+
+bool NumericExpressionNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() == KD_NumericExpression;
+}
+
+NumericExpressionNode::NumericExpressionNode()
+ : WorkflowNode(KD_NumericExpression, false)
+{
+}
+
+void NumericExpressionNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+}
diff --git a/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp
new file mode 100644
index 0000000..3c89708
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/NumericNodes.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+#include <cstddef>
+#include <memory>
+#include <variant>
+#include <vector>
+
+class NumericOperationNode : public WorkflowNode
+{
+public:
+ enum OperationType
+ {
+ Addition,
+ Subtraction,
+ Multiplication,
+ Division,
+
+ InvalidType,
+ TypeCount = InvalidType,
+ };
+
+private:
+ OperationType mType;
+
+public:
+ static Kind OperationTypeToNodeKind(OperationType type);
+ static OperationType NodeKindToOperationType(Kind kind);
+ static bool IsInstance(const WorkflowNode* node);
+ NumericOperationNode(OperationType type);
+
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+};
+
+class NumericExpressionNode : public WorkflowNode
+{
+public:
+ static bool IsInstance(const WorkflowNode* node);
+ NumericExpressionNode();
+
+ // TODO
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+}; \ No newline at end of file
diff --git a/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp
new file mode 100644
index 0000000..9b31f7a
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.cpp
@@ -0,0 +1,231 @@
+#include "TextNodes.hpp"
+
+#include <Cplt/Model/Workflow/Evaluation.hpp>
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+#include <Cplt/Utils/Macros.hpp>
+#include <Cplt/Utils/RTTI.hpp>
+#include <Cplt/Utils/Variant.hpp>
+
+#include <cassert>
+#include <utility>
+#include <variant>
+#include <vector>
+
+class TextFormatterNode::Impl
+{
+public:
+ template <class TFunction>
+ static void ForArguments(std::vector<Element>::iterator begin, std::vector<Element>::iterator end, const TFunction& func)
+ {
+ for (auto it = begin; it != end; ++it) {
+ auto& elm = *it;
+ if (auto arg = std::get_if<Argument>(&elm)) {
+ func(*arg);
+ }
+ }
+ }
+
+ /// Find the pin index that the \c elmIdx -th element should have, based on the elements coming before it.
+ static int FindPinForElement(const std::vector<Element>& vec, int elmIdx)
+ {
+ for (int i = elmIdx; i >= 0; --i) {
+ auto& elm = vec[i];
+ if (auto arg = std::get_if<Argument>(&elm)) {
+ return arg->PinIdx + 1;
+ }
+ }
+ return 0;
+ }
+};
+
+BaseValue::Kind TextFormatterNode::ArgumentTypeToValueKind(TextFormatterNode::ArgumentType arg)
+{
+ switch (arg) {
+ case NumericArgument: return BaseValue::KD_Numeric;
+ case TextArgument: return BaseValue::KD_Text;
+ case DateTimeArgument: return BaseValue::KD_DateTime;
+ }
+}
+
+bool TextFormatterNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() == KD_TextFormatting;
+}
+
+TextFormatterNode::TextFormatterNode()
+ : WorkflowNode(KD_TextFormatting, false)
+{
+}
+
+int TextFormatterNode::GetElementCount() const
+{
+ return mElements.size();
+}
+
+const TextFormatterNode::Element& TextFormatterNode::GetElement(int idx) const
+{
+ return mElements[idx];
+}
+
+void TextFormatterNode::SetElement(int idx, std::string text)
+{
+ assert(idx >= 0 && idx < mElements.size());
+
+ std::visit(
+ Overloaded{
+ [&](const std::string& original) { mMinOutputChars -= original.size(); },
+ [&](const Argument& original) { PreRemoveElement(idx); },
+ },
+ mElements[idx]);
+
+ mMinOutputChars += text.size();
+ mElements[idx] = std::move(text);
+}
+
+void TextFormatterNode::SetElement(int idx, ArgumentType argument)
+{
+ assert(idx >= 0 && idx < mElements.size());
+
+ std::visit(
+ Overloaded{
+ [&](const std::string& original) {
+ mMinOutputChars -= original.size();
+
+ mElements[idx] = Argument{
+ .Type = argument,
+ .PinIdx = Impl::FindPinForElement(mElements, idx),
+ };
+ /* `original` is invalid from this point */
+ },
+ [&](const Argument& original) {
+ int pinIdx = original.PinIdx;
+
+ // Create pin
+ auto& pin = mInputs[pinIdx];
+ pin.MatchingType = ArgumentTypeToValueKind(argument);
+
+ // Create element
+ mElements[idx] = Argument{
+ .Type = argument,
+ .PinIdx = pinIdx,
+ };
+ /* `original` is invalid from this point */
+ },
+ },
+ mElements[idx]);
+}
+
+void TextFormatterNode::InsertElement(int idx, std::string text)
+{
+ assert(idx >= 0);
+ if (idx >= mElements.size()) AppendElement(std::move(text));
+
+ mMinOutputChars += text.size();
+ mElements.insert(mElements.begin() + idx, std::move(text));
+}
+
+void TextFormatterNode::InsertElement(int idx, ArgumentType argument)
+{
+ assert(idx >= 0);
+ if (idx >= mElements.size()) AppendElement(argument);
+
+ int pinIdx = Impl::FindPinForElement(mElements, idx);
+
+ // Create pin
+ auto& pin = InsertInputPin(pinIdx);
+ pin.MatchingType = ArgumentTypeToValueKind(argument);
+
+ // Create element
+ mElements.insert(
+ mElements.begin() + idx,
+ Argument{
+ .Type = argument,
+ .PinIdx = pinIdx,
+ });
+}
+
+void TextFormatterNode::AppendElement(std::string text)
+{
+ mMinOutputChars += text.size();
+ mElements.push_back(std::move(text));
+}
+
+void TextFormatterNode::AppendElement(ArgumentType argument)
+{
+ int pinIdx = mInputs.size();
+ // Create pin
+ mInputs.push_back(InputPin{});
+ mInputs.back().MatchingType = ArgumentTypeToValueKind(argument);
+ // Creat eelement
+ mElements.push_back(Argument{
+ .Type = argument,
+ .PinIdx = pinIdx,
+ });
+}
+
+void TextFormatterNode::RemoveElement(int idx)
+{
+ assert(idx >= 0 && idx < mElements.size());
+
+ PreRemoveElement(idx);
+ if (auto arg = std::get_if<Argument>(&mElements[idx])) {
+ RemoveInputPin(arg->PinIdx);
+ }
+ mElements.erase(mElements.begin() + idx);
+}
+
+void TextFormatterNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+ std::string result;
+ result.reserve((size_t)(mMinOutputChars * 1.5f));
+
+ auto HandleText = [&](const std::string& str) {
+ result += str;
+ };
+ auto HandleArgument = [&](const Argument& arg) {
+ switch (arg.Type) {
+ case NumericArgument: {
+ if (auto val = dyn_cast<NumericValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) {
+ result += val->GetString();
+ } else {
+ // TODO localize
+ ctx.ReportError("Non-numeric value connected to a numeric text format parameter.", *this);
+ }
+ } break;
+ case TextArgument: {
+ if (auto val = dyn_cast<TextValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) {
+ result += val->GetValue();
+ } else {
+ // TODO localize
+ ctx.ReportError("Non-text value connected to a textual text format parameter.", *this);
+ }
+ } break;
+ case DateTimeArgument: {
+ if (auto val = dyn_cast<DateTimeValue>(ctx.GetConnectionValue(mInputs[arg.PinIdx]))) {
+ result += val->GetString();
+ } else {
+ // TODO localize
+ ctx.ReportError("Non-date/time value connected to a date/time text format parameter.", *this);
+ }
+ } break;
+ }
+ };
+
+ for (auto& elm : mElements) {
+ std::visit(Overloaded{ HandleText, HandleArgument }, elm);
+ }
+}
+
+void TextFormatterNode::PreRemoveElement(int idx)
+{
+ auto& elm = mElements[idx];
+ if (auto arg = std::get_if<Argument>(&elm)) {
+ RemoveInputPin(arg->PinIdx);
+ Impl::ForArguments(
+ mElements.begin() + idx + 1,
+ mElements.end(),
+ [&](Argument& arg) {
+ arg.PinIdx--;
+ });
+ }
+}
diff --git a/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp
new file mode 100644
index 0000000..4689931
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/TextNodes.hpp
@@ -0,0 +1,53 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+#include <cstddef>
+#include <memory>
+#include <variant>
+#include <vector>
+
+class TextFormatterNode : public WorkflowNode
+{
+public:
+ enum ArgumentType
+ {
+ NumericArgument,
+ TextArgument,
+ DateTimeArgument,
+ };
+
+private:
+ class Impl;
+
+ struct Argument
+ {
+ ArgumentType Type;
+ int PinIdx;
+ };
+ using Element = std::variant<std::string, Argument>;
+
+ std::vector<Element> mElements;
+ int mMinOutputChars;
+
+public:
+ static BaseValue::Kind ArgumentTypeToValueKind(ArgumentType arg);
+ static bool IsInstance(const WorkflowNode* node);
+ TextFormatterNode();
+
+ int GetElementCount() const;
+ const Element& GetElement(int idx) const;
+
+ void SetElement(int idx, std::string text);
+ void SetElement(int idx, ArgumentType argument);
+ void InsertElement(int idx, std::string text);
+ void InsertElement(int idx, ArgumentType argument);
+ void AppendElement(std::string text);
+ void AppendElement(ArgumentType argument);
+ void RemoveElement(int idx);
+
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+
+private:
+ void PreRemoveElement(int idx);
+};
diff --git a/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp
new file mode 100644
index 0000000..93d458c
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.cpp
@@ -0,0 +1,32 @@
+#include "UserInputNodes.hpp"
+
+#include <Cplt/Model/Workflow/Evaluation.hpp>
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+
+bool FormInputNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() == KD_FormInput;
+}
+
+FormInputNode::FormInputNode()
+ : WorkflowNode(KD_FormInput, false)
+{
+}
+
+void FormInputNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+}
+
+bool DatabaseRowsInputNode::IsInstance(const WorkflowNode* node)
+{
+ return node->GetKind() == KD_DatabaseRowsInput;
+}
+
+DatabaseRowsInputNode::DatabaseRowsInputNode()
+ : WorkflowNode(KD_DatabaseRowsInput, false)
+{
+}
+
+void DatabaseRowsInputNode::Evaluate(WorkflowEvaluationContext& ctx)
+{
+}
diff --git a/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp
new file mode 100644
index 0000000..f0b923c
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/UserInputNodes.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Workflow.hpp>
+
+class FormInputNode : public WorkflowNode
+{
+public:
+ static bool IsInstance(const WorkflowNode* node);
+ FormInputNode();
+
+ // TODO
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+};
+
+class DatabaseRowsInputNode : public WorkflowNode
+{
+public:
+ static bool IsInstance(const WorkflowNode* node);
+ DatabaseRowsInputNode();
+
+ // TODO
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) override;
+};
diff --git a/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp b/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp
new file mode 100644
index 0000000..4153825
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Nodes/fwd.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+// DocumentNodes.hpp
+class DocumentTemplateExpansionNode;
+
+// InputNodes.hpp
+class FormInputNode;
+class DatabaseRowsInputNode;
+
+// NumericNodes.hpp
+class NumericOperationNode;
+class NumericExpressionNode;
+
+// TextNodes.hpp
+class TextFormatterNode;
diff --git a/app/source/Cplt/Model/Workflow/Value.hpp b/app/source/Cplt/Model/Workflow/Value.hpp
new file mode 100644
index 0000000..70fcb57
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Value.hpp
@@ -0,0 +1,94 @@
+#pragma once
+
+#include <Cplt/Utils/Color.hpp>
+#include <Cplt/fwd.hpp>
+
+#include <iosfwd>
+#include <memory>
+#include <string>
+#include <vector>
+
+class BaseValue
+{
+public:
+ enum Kind
+ {
+ KD_Numeric,
+ KD_Text,
+ KD_DateTime,
+ KD_DatabaseRowId,
+ KD_List,
+ KD_Dictionary,
+
+ KD_BaseObject,
+ KD_SaleDatabaseRow,
+ KD_PurchaseDatabaseRow,
+ KD_BaseObjectLast = KD_PurchaseDatabaseRow,
+
+ /// An unspecified type, otherwise known as "any" in some contexts.
+ InvalidKind,
+ KindCount = InvalidKind,
+ };
+
+ struct KindInfo
+ {
+ ImGui::IconType PinIcon;
+ RgbaColor PinColor;
+ };
+
+private:
+ Kind mKind;
+
+public:
+ static const KindInfo& QueryInfo(Kind kind);
+ static const char* Format(Kind kind);
+ static std::unique_ptr<BaseValue> CreateByKind(Kind kind);
+
+ static bool IsInstance(const BaseValue* value);
+
+ BaseValue(Kind kind);
+ virtual ~BaseValue() = default;
+
+ BaseValue(const BaseValue&) = delete;
+ BaseValue& operator=(const BaseValue&) = delete;
+ BaseValue(BaseValue&&) = default;
+ BaseValue& operator=(BaseValue&&) = default;
+
+ Kind GetKind() const;
+
+ // TODO get constant editor
+
+ /// The functions \c ReadFrom, \c WriteTo will only be valid to call if this function returns true.
+ virtual bool SupportsConstant() const;
+ virtual void ReadFrom(std::istream& stream);
+ virtual void WriteTo(std::ostream& stream);
+};
+
+class BaseObjectDescription
+{
+public:
+ struct Property
+ {
+ std::string Name;
+ BaseValue::Kind Kind;
+ bool Mutatable = true;
+ };
+
+public:
+ std::vector<Property> Properties;
+};
+
+class BaseObjectValue : public BaseValue
+{
+public:
+ /// \param kind A value kind enum, within the range of KD_BaseObject and KD_BaseObjectLast (both inclusive).
+ static const BaseObjectDescription& QueryObjectInfo(Kind kind);
+
+ static bool IsInstance(const BaseValue* value);
+ BaseObjectValue(Kind kind);
+
+ const BaseObjectDescription& GetObjectDescription() const;
+
+ virtual const BaseValue* GetProperty(int idx) const = 0;
+ virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value) = 0;
+};
diff --git a/app/source/Cplt/Model/Workflow/ValueInternals.hpp b/app/source/Cplt/Model/Workflow/ValueInternals.hpp
new file mode 100644
index 0000000..45842db
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/ValueInternals.hpp
@@ -0,0 +1,21 @@
+// This file contains utility classes and macros for implementing values
+// As consumers, you should not include this header as it contains unnecessary symbols and can pollute your files
+// for this reason, classes here aren't forward-declared in fwd.hpp either.
+
+#pragma once
+
+#include <Cplt/Utils/RTTI.hpp>
+
+#include <utility>
+
+#define CHECK_VALUE_TYPE(Type, value) \
+ if (!is_a<Type>(value)) { \
+ return false; \
+ }
+
+#define CHECK_VALUE_TYPE_AND_MOVE(Type, dest, value) \
+ if (auto ptr = dyn_cast<Type>(value)) { \
+ dest = std::move(*ptr); \
+ } else { \
+ return false; \
+ }
diff --git a/app/source/Cplt/Model/Workflow/Value_Main.cpp b/app/source/Cplt/Model/Workflow/Value_Main.cpp
new file mode 100644
index 0000000..ca972c4
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Value_Main.cpp
@@ -0,0 +1,35 @@
+#include "Value.hpp"
+
+BaseValue::BaseValue(Kind kind)
+ : mKind{ kind }
+{
+}
+
+BaseValue::Kind BaseValue::GetKind() const
+{
+ return mKind;
+}
+
+bool BaseValue::SupportsConstant() const
+{
+ return false;
+}
+
+void BaseValue::ReadFrom(std::istream& stream)
+{
+}
+
+void BaseValue::WriteTo(std::ostream& stream)
+{
+}
+
+BaseObjectValue::BaseObjectValue(Kind kind)
+ : BaseValue(kind)
+{
+ assert(kind >= KD_BaseObject && kind <= KD_BaseObjectLast);
+}
+
+const BaseObjectDescription& BaseObjectValue::GetObjectDescription() const
+{
+ return QueryObjectInfo(this->GetKind());
+}
diff --git a/app/source/Cplt/Model/Workflow/Value_RTTI.cpp b/app/source/Cplt/Model/Workflow/Value_RTTI.cpp
new file mode 100644
index 0000000..a2a6960
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Value_RTTI.cpp
@@ -0,0 +1,174 @@
+#include "Value.hpp"
+
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+#include <Cplt/Model/Workflow/Values/Database.hpp>
+#include <Cplt/Model/Workflow/Values/Dictionary.hpp>
+#include <Cplt/Model/Workflow/Values/List.hpp>
+#include <Cplt/UI/UI.hpp>
+#include <Cplt/Utils/I18n.hpp>
+
+constexpr BaseValue::KindInfo kEmptyInfo{
+ .PinIcon = ImGui::IconType::Circle,
+ .PinColor = RgbaColor(),
+};
+
+constexpr BaseValue::KindInfo kNumericInfo{
+ .PinIcon = ImGui::IconType::Circle,
+ .PinColor = RgbaColor(147, 226, 74),
+};
+
+constexpr BaseValue::KindInfo kTextInfo{
+ .PinIcon = ImGui::IconType::Circle,
+ .PinColor = RgbaColor(124, 21, 153),
+};
+
+constexpr BaseValue::KindInfo kDateTimeInfo{
+ .PinIcon = ImGui::IconType::Circle,
+ .PinColor = RgbaColor(147, 226, 74),
+};
+
+constexpr BaseValue::KindInfo kDatabaseRowIdInfo{
+ .PinIcon = ImGui::IconType::Circle,
+ .PinColor = RgbaColor(216, 42, 221),
+};
+
+constexpr BaseValue::KindInfo kListInfo{
+ .PinIcon = ImGui::IconType::Diamond,
+ .PinColor = RgbaColor(58, 154, 214),
+};
+
+constexpr BaseValue::KindInfo kDictionaryInfo{
+ .PinIcon = ImGui::IconType::Diamond,
+ .PinColor = RgbaColor(240, 240, 34),
+};
+
+constexpr BaseValue::KindInfo kDatabaseRowInfo{
+ .PinIcon = ImGui::IconType::Square,
+ .PinColor = RgbaColor(15, 124, 196),
+};
+
+constexpr BaseValue::KindInfo kObjectInfo{
+ .PinIcon = ImGui::IconType::Square,
+ .PinColor = RgbaColor(161, 161, 161),
+};
+
+const BaseValue::KindInfo& BaseValue::QueryInfo(BaseValue::Kind kind)
+{
+ switch (kind) {
+ case KD_Numeric: return kNumericInfo;
+ case KD_Text: return kTextInfo;
+ case KD_DateTime: return kDateTimeInfo;
+ case KD_DatabaseRowId: return kDatabaseRowIdInfo;
+ case KD_List: return kListInfo;
+ case KD_Dictionary: return kDictionaryInfo;
+
+ case KD_BaseObject: return kObjectInfo;
+ case KD_SaleDatabaseRow:
+ case KD_PurchaseDatabaseRow:
+ return kDatabaseRowInfo;
+
+ case InvalidKind: break;
+ }
+ return kEmptyInfo;
+}
+
+const char* BaseValue::Format(Kind kind)
+{
+ switch (kind) {
+ case KD_Numeric: return I18N_TEXT("Numeric", L10N_VALUE_NUMERIC);
+ case KD_Text: return I18N_TEXT("Text", L10N_VALUE_TEXT);
+ case KD_DateTime: return I18N_TEXT("Date/time", L10N_VALUE_DATE_TIME);
+ case KD_DatabaseRowId: return I18N_TEXT("Row id", L10N_VALUE_ROW_ID);
+ case KD_List: return I18N_TEXT("List", L10N_VALUE_LIST);
+ case KD_Dictionary: return I18N_TEXT("Dictionary", L10N_VALUE_DICT);
+
+ case KD_BaseObject: return I18N_TEXT("Object", L10N_VALUE_OBJECT);
+ case KD_SaleDatabaseRow: return I18N_TEXT("Sale record", L10N_VALUE_SALE_RECORD);
+ case KD_PurchaseDatabaseRow: return I18N_TEXT("Purchase record", L10N_VALUE_PURCHASE_RECORD);
+
+ case InvalidKind: break;
+ }
+ return "";
+}
+
+std::unique_ptr<BaseValue> BaseValue::CreateByKind(BaseValue::Kind kind)
+{
+ switch (kind) {
+ case KD_Numeric: return std::make_unique<NumericValue>();
+ case KD_Text: return std::make_unique<TextValue>();
+ case KD_DateTime: return std::make_unique<DateTimeValue>();
+ case KD_DatabaseRowId: return std::make_unique<DatabaseRowIdValue>();
+ case KD_List: return std::make_unique<ListValue>();
+ case KD_Dictionary: return std::make_unique<DictionaryValue>();
+
+ case KD_BaseObject: return nullptr;
+ case KD_SaleDatabaseRow: return std::make_unique<SaleDatabaseRowValue>();
+ case KD_PurchaseDatabaseRow: return std::make_unique<PurchaseDatabaseRowValue>();
+
+ case InvalidKind: break;
+ }
+ return nullptr;
+}
+
+bool BaseValue::IsInstance(const BaseValue* value)
+{
+ return true;
+}
+
+const BaseObjectDescription kEmptyObjectInfo{
+ .Properties = {},
+};
+
+const BaseObjectDescription kSaleDbRowObject{
+ .Properties = {
+ {
+ .Name = I18N_TEXT("Customer", L10N_VALUE_PROPERTY_CUSTOMER),
+ .Kind = BaseValue::KD_Text,
+ .Mutatable = false,
+ },
+ {
+ .Name = I18N_TEXT("Deadline", L10N_VALUE_PROPERTY_DEADLINE),
+ .Kind = BaseValue::KD_DateTime,
+ },
+ {
+ .Name = I18N_TEXT("Delivery time", L10N_VALUE_PROPERTY_DELIVERY_TIME),
+ .Kind = BaseValue::KD_DateTime,
+ },
+ },
+};
+
+const BaseObjectDescription kPurchaseDbRowObject{
+ .Properties = {
+ {
+ .Name = I18N_TEXT("Factory", L10N_VALUE_PROPERTY_FACTORY),
+ .Kind = BaseValue::KD_Text,
+ .Mutatable = false,
+ },
+ {
+ .Name = I18N_TEXT("Order time", L10N_VALUE_PROPERTY_ORDER_TIME),
+ .Kind = BaseValue::KD_DateTime,
+ },
+ {
+ .Name = I18N_TEXT("Delivery time", L10N_VALUE_PROPERTY_DELIVERY_TIME),
+ .Kind = BaseValue::KD_DateTime,
+ },
+ },
+};
+
+const BaseObjectDescription& BaseObjectValue::QueryObjectInfo(Kind kind)
+{
+ switch (kind) {
+ case KD_BaseObject: return kEmptyObjectInfo;
+ case KD_SaleDatabaseRow: return kSaleDbRowObject;
+ case KD_PurchaseDatabaseRow: return kPurchaseDbRowObject;
+
+ default: break;
+ }
+ return kEmptyObjectInfo;
+}
+
+bool BaseObjectValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() >= KD_BaseObject &&
+ value->GetKind() <= KD_BaseObjectLast;
+}
diff --git a/app/source/Cplt/Model/Workflow/Values/Basic.cpp b/app/source/Cplt/Model/Workflow/Values/Basic.cpp
new file mode 100644
index 0000000..198387c
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Basic.cpp
@@ -0,0 +1,111 @@
+#include "Basic.hpp"
+
+#include <charconv>
+#include <cmath>
+#include <limits>
+
+bool NumericValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_Numeric;
+}
+
+NumericValue::NumericValue()
+ : BaseValue(BaseValue::KD_Numeric)
+{
+}
+
+template <class T, int kMaxSize>
+static std::string NumberToString(T value)
+{
+ char buf[kMaxSize];
+ auto res = std::to_chars(buf, buf + kMaxSize, value);
+ if (res.ec == std::errc()) {
+ return std::string(buf, res.ptr);
+ } else {
+ return "<err>";
+ }
+}
+
+std::string NumericValue::GetTruncatedString() const
+{
+ constexpr auto kMaxSize = std::numeric_limits<int64_t>::digits10;
+ return ::NumberToString<int64_t, kMaxSize>((int64_t)mValue);
+}
+
+std::string NumericValue::GetRoundedString() const
+{
+ constexpr auto kMaxSize = std::numeric_limits<int64_t>::digits10;
+ return ::NumberToString<int64_t, kMaxSize>((int64_t)std::round(mValue));
+}
+
+std::string NumericValue::GetString() const
+{
+ constexpr auto kMaxSize = std::numeric_limits<double>::max_digits10;
+ return ::NumberToString<double, kMaxSize>(mValue);
+}
+
+int64_t NumericValue::GetInt() const
+{
+ return static_cast<int64_t>(mValue);
+}
+
+double NumericValue::GetValue() const
+{
+ return mValue;
+}
+
+void NumericValue::SetValue(double value)
+{
+ mValue = value;
+}
+
+bool TextValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_Text;
+}
+
+TextValue::TextValue()
+ : BaseValue(BaseValue::KD_Text)
+{
+}
+
+const std::string& TextValue::GetValue() const
+{
+ return mValue;
+}
+
+void TextValue::SetValue(const std::string& value)
+{
+ mValue = value;
+}
+
+bool DateTimeValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_DateTime;
+}
+
+DateTimeValue::DateTimeValue()
+ : BaseValue(BaseValue::KD_DateTime)
+{
+}
+
+std::string DateTimeValue::GetString() const
+{
+ namespace chrono = std::chrono;
+ auto t = chrono::system_clock::to_time_t(mValue);
+
+ char data[32];
+ std::strftime(data, sizeof(data), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
+
+ return std::string(data);
+}
+
+const std::chrono::time_point<std::chrono::system_clock>& DateTimeValue::GetValue() const
+{
+ return mValue;
+}
+
+void DateTimeValue::SetValue(const std::chrono::time_point<std::chrono::system_clock>& value)
+{
+ mValue = value;
+}
diff --git a/app/source/Cplt/Model/Workflow/Values/Basic.hpp b/app/source/Cplt/Model/Workflow/Values/Basic.hpp
new file mode 100644
index 0000000..820fb13
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Basic.hpp
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Value.hpp>
+
+#include <chrono>
+#include <cstdint>
+#include <string>
+
+class NumericValue : public BaseValue
+{
+private:
+ double mValue;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ NumericValue();
+
+ NumericValue(const NumericValue&) = delete;
+ NumericValue& operator=(const NumericValue&) = delete;
+ NumericValue(NumericValue&&) = default;
+ NumericValue& operator=(NumericValue&&) = default;
+
+ std::string GetTruncatedString() const;
+ std::string GetRoundedString() const;
+ std::string GetString() const;
+
+ int64_t GetInt() const;
+ double GetValue() const;
+ void SetValue(double value);
+};
+
+class TextValue : public BaseValue
+{
+private:
+ std::string mValue;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ TextValue();
+
+ TextValue(const TextValue&) = delete;
+ TextValue& operator=(const TextValue&) = delete;
+ TextValue(TextValue&&) = default;
+ TextValue& operator=(TextValue&&) = default;
+
+ const std::string& GetValue() const;
+ void SetValue(const std::string& value);
+};
+
+class DateTimeValue : public BaseValue
+{
+private:
+ std::chrono::time_point<std::chrono::system_clock> mValue;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ DateTimeValue();
+
+ DateTimeValue(const DateTimeValue&) = delete;
+ DateTimeValue& operator=(const DateTimeValue&) = delete;
+ DateTimeValue(DateTimeValue&&) = default;
+ DateTimeValue& operator=(DateTimeValue&&) = default;
+
+ std::string GetString() const;
+ const std::chrono::time_point<std::chrono::system_clock>& GetValue() const;
+ void SetValue(const std::chrono::time_point<std::chrono::system_clock>& value);
+};
diff --git a/app/source/Cplt/Model/Workflow/Values/Database.cpp b/app/source/Cplt/Model/Workflow/Values/Database.cpp
new file mode 100644
index 0000000..25b77e9
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Database.cpp
@@ -0,0 +1,88 @@
+#include "Database.hpp"
+
+#include <Cplt/Model/Database.hpp>
+#include <Cplt/Model/Workflow/ValueInternals.hpp>
+
+#include <limits>
+
+TableKind DatabaseRowIdValue::GetTable() const
+{
+ return mTable;
+}
+
+int64_t DatabaseRowIdValue::GetRowId() const
+{
+ return mRowId;
+}
+
+bool DatabaseRowIdValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_DatabaseRowId;
+}
+
+DatabaseRowIdValue::DatabaseRowIdValue()
+ : BaseValue(KD_DatabaseRowId)
+ , mTable{ TableKind::Sales }
+ , mRowId{ std::numeric_limits<int64_t>::max() }
+{
+}
+
+bool SaleDatabaseRowValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_SaleDatabaseRow;
+}
+
+SaleDatabaseRowValue::SaleDatabaseRowValue()
+ : BaseObjectValue(KD_SaleDatabaseRow)
+{
+}
+
+const BaseValue* SaleDatabaseRowValue::GetProperty(int idx) const
+{
+ switch (idx) {
+ case 0: return &mCustomerName;
+ case 1: return &mDeadline;
+ case 2: return &mDeliveryTime;
+ default: return nullptr;
+ }
+}
+
+bool SaleDatabaseRowValue::SetProperty(int idx, std::unique_ptr<BaseValue> value)
+{
+ switch (idx) {
+ case 0: return false;
+ case 1: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeadline, value.get()); break;
+ case 2: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeliveryTime, value.get()); break;
+ }
+ return true;
+}
+
+bool PurchaseDatabaseRowValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_PurchaseDatabaseRow;
+}
+
+PurchaseDatabaseRowValue::PurchaseDatabaseRowValue()
+ : BaseObjectValue(KD_PurchaseDatabaseRow)
+{
+}
+
+const BaseValue* PurchaseDatabaseRowValue::GetProperty(int idx) const
+{
+ switch (idx) {
+ case 0: return &mFactoryName;
+ case 1: return &mOrderTime;
+ case 2: return &mDeliveryTime;
+ default: return nullptr;
+ }
+}
+
+bool PurchaseDatabaseRowValue::SetProperty(int idx, std::unique_ptr<BaseValue> value)
+{
+ switch (idx) {
+ case 0: return false;
+ case 1: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mOrderTime, value.get()); break;
+ case 2: CHECK_VALUE_TYPE_AND_MOVE(DateTimeValue, mDeliveryTime, value.get()); break;
+ }
+ return true;
+}
diff --git a/app/source/Cplt/Model/Workflow/Values/Database.hpp b/app/source/Cplt/Model/Workflow/Values/Database.hpp
new file mode 100644
index 0000000..f1c1571
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Database.hpp
@@ -0,0 +1,51 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Value.hpp>
+#include <Cplt/Model/Workflow/Values/Basic.hpp>
+#include <Cplt/fwd.hpp>
+
+class DatabaseRowIdValue : public BaseValue
+{
+private:
+ TableKind mTable;
+ int64_t mRowId;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ DatabaseRowIdValue();
+
+ TableKind GetTable() const;
+ int64_t GetRowId() const;
+};
+
+class SaleDatabaseRowValue : public BaseObjectValue
+{
+private:
+ int mCustomerId;
+ TextValue mCustomerName;
+ DateTimeValue mDeadline;
+ DateTimeValue mDeliveryTime;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ SaleDatabaseRowValue();
+
+ virtual const BaseValue* GetProperty(int idx) const;
+ virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value);
+};
+
+class PurchaseDatabaseRowValue : public BaseObjectValue
+{
+private:
+ int mFactoryId;
+ TextValue mFactoryName;
+ DateTimeValue mOrderTime;
+ DateTimeValue mDeliveryTime;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ PurchaseDatabaseRowValue();
+
+ virtual const BaseValue* GetProperty(int idx) const;
+ virtual bool SetProperty(int idx, std::unique_ptr<BaseValue> value);
+};
diff --git a/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp b/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp
new file mode 100644
index 0000000..97bf509
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Dictionary.cpp
@@ -0,0 +1,49 @@
+#include "Dictionary.hpp"
+
+#include <Cplt/Utils/Macros.hpp>
+
+bool DictionaryValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_Dictionary;
+}
+
+DictionaryValue::DictionaryValue()
+ : BaseValue(KD_Dictionary)
+{
+}
+
+int DictionaryValue::GetCount() const
+{
+ return mElements.size();
+}
+
+BaseValue* DictionaryValue::Find(std::string_view key)
+{
+ auto iter = mElements.find(key);
+ if (iter != mElements.end()) {
+ return iter.value().get();
+ } else {
+ return nullptr;
+ }
+}
+
+BaseValue* DictionaryValue::Insert(std::string_view key, std::unique_ptr<BaseValue>& value)
+{
+ auto [iter, success] = mElements.insert(key, std::move(value));
+ if (success) {
+ return iter.value().get();
+ } else {
+ return nullptr;
+ }
+}
+
+BaseValue& DictionaryValue::InsertOrReplace(std::string_view key, std::unique_ptr<BaseValue> value)
+{
+ auto [iter, DISCARD] = mElements.emplace(key, std::move(value));
+ return *iter.value();
+}
+
+void DictionaryValue::Remove(std::string_view key)
+{
+ mElements.erase(mElements.find(key));
+}
diff --git a/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp b/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp
new file mode 100644
index 0000000..6eff308
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/Dictionary.hpp
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Value.hpp>
+
+#include <tsl/array_map.h>
+#include <memory>
+#include <string>
+#include <string_view>
+
+class DictionaryValue : public BaseValue
+{
+private:
+ tsl::array_map<char, std::unique_ptr<BaseValue>> mElements;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ DictionaryValue();
+
+ int GetCount() const;
+ BaseValue* Find(std::string_view key);
+
+ BaseValue* Insert(std::string_view key, std::unique_ptr<BaseValue>& value);
+ BaseValue& InsertOrReplace(std::string_view key, std::unique_ptr<BaseValue> value);
+ void Remove(std::string_view key);
+};
diff --git a/app/source/Cplt/Model/Workflow/Values/List.cpp b/app/source/Cplt/Model/Workflow/Values/List.cpp
new file mode 100644
index 0000000..9fd6bfd
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/List.cpp
@@ -0,0 +1,100 @@
+#include "List.hpp"
+
+#include <utility>
+
+BaseValue* ListValue::Iterator::operator*() const
+{
+ return mIter->get();
+}
+
+BaseValue* ListValue::Iterator::operator->() const
+{
+ return mIter->get();
+}
+
+ListValue::Iterator& ListValue::Iterator::operator++()
+{
+ ++mIter;
+ return *this;
+}
+
+ListValue::Iterator ListValue::Iterator::operator++(int) const
+{
+ return Iterator(mIter + 1);
+}
+
+ListValue::Iterator& ListValue::Iterator::operator--()
+{
+ --mIter;
+ return *this;
+}
+
+ListValue::Iterator ListValue::Iterator::operator--(int) const
+{
+ return Iterator(mIter - 1);
+}
+
+bool operator==(const ListValue::Iterator& a, const ListValue::Iterator& b)
+{
+ return a.mIter == b.mIter;
+}
+
+ListValue::Iterator::Iterator(decltype(mIter) iter)
+ : mIter{ iter }
+{
+}
+
+bool ListValue::IsInstance(const BaseValue* value)
+{
+ return value->GetKind() == KD_List;
+}
+
+ListValue::ListValue()
+ : BaseValue(KD_List)
+{
+}
+
+int ListValue::GetCount() const
+{
+ return mElements.size();
+}
+
+BaseValue* ListValue::GetElement(int i) const
+{
+ return mElements[i].get();
+}
+
+void ListValue::Append(std::unique_ptr<BaseValue> element)
+{
+ mElements.push_back(std::move(element));
+}
+
+void ListValue::Insert(int i, std::unique_ptr<BaseValue> element)
+{
+ mElements.insert(mElements.begin() + i, std::move(element));
+}
+
+void ListValue::Insert(Iterator iter, std::unique_ptr<BaseValue> element)
+{
+ mElements.insert(iter.mIter, std::move(element));
+}
+
+void ListValue::Remove(int i)
+{
+ mElements.erase(mElements.begin() + i);
+}
+
+void ListValue::Remove(Iterator iter)
+{
+ mElements.erase(iter.mIter);
+}
+
+ListValue::Iterator ListValue::begin()
+{
+ return Iterator(mElements.begin());
+}
+
+ListValue::Iterator ListValue::end()
+{
+ return Iterator(mElements.end());
+}
diff --git a/app/source/Cplt/Model/Workflow/Values/List.hpp b/app/source/Cplt/Model/Workflow/Values/List.hpp
new file mode 100644
index 0000000..cc8e061
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/List.hpp
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Value.hpp>
+
+#include <memory>
+#include <vector>
+
+class ListValue : public BaseValue
+{
+public:
+ class Iterator
+ {
+ private:
+ std::vector<std::unique_ptr<BaseValue>>::iterator mIter;
+
+ public:
+ BaseValue* operator*() const;
+ BaseValue* operator->() const;
+
+ Iterator& operator++();
+ Iterator operator++(int) const;
+ Iterator& operator--();
+ Iterator operator--(int) const;
+
+ friend bool operator==(const Iterator& a, const Iterator& b);
+
+ private:
+ friend class ListValue;
+ Iterator(decltype(mIter) iter);
+ };
+
+private:
+ std::vector<std::unique_ptr<BaseValue>> mElements;
+
+public:
+ static bool IsInstance(const BaseValue* value);
+ ListValue();
+
+ int GetCount() const;
+ BaseValue* GetElement(int i) const;
+
+ void Append(std::unique_ptr<BaseValue> element);
+ void Insert(int i, std::unique_ptr<BaseValue> element);
+ void Insert(Iterator iter, std::unique_ptr<BaseValue> element);
+ void Remove(int i);
+ void Remove(Iterator iter);
+
+ Iterator begin();
+ Iterator end();
+};
diff --git a/app/source/Cplt/Model/Workflow/Values/fwd.hpp b/app/source/Cplt/Model/Workflow/Values/fwd.hpp
new file mode 100644
index 0000000..51a04e9
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Values/fwd.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+// Basic.hpp
+class NumericValue;
+class TextValue;
+class DateTimeValue;
+
+// Database.hpp
+class DatabaseRowIdValue;
+class SaleDatabaseRowValue;
+class PurchaseDatabaseRowValue;
+
+// Dictionary.hpp
+class DictionaryValue;
+
+// List.hpp
+class ListValue;
diff --git a/app/source/Cplt/Model/Workflow/Workflow.hpp b/app/source/Cplt/Model/Workflow/Workflow.hpp
new file mode 100644
index 0000000..e075e3c
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Workflow.hpp
@@ -0,0 +1,316 @@
+#pragma once
+
+#include <Cplt/Model/Assets.hpp>
+#include <Cplt/Model/Workflow/Value.hpp>
+#include <Cplt/Utils/Vector.hpp>
+#include <Cplt/fwd.hpp>
+
+#include <imgui_node_editor.h>
+#include <cstddef>
+#include <cstdint>
+#include <filesystem>
+#include <functional>
+#include <iosfwd>
+#include <limits>
+#include <memory>
+#include <span>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace ImNodes = ax::NodeEditor;
+
+class WorkflowConnection
+{
+public:
+ static constexpr auto kInvalidId = std::numeric_limits<uint32_t>::max();
+
+ uint32_t Id;
+ uint32_t SourceNode;
+ uint32_t SourcePin;
+ uint32_t DestinationNode;
+ uint32_t DestinationPin;
+
+public:
+ WorkflowConnection();
+
+ bool IsValid() const;
+
+ /// Used for `LinkId` when interfacing with imgui node editor. Runtime only (not saved to disk and generated when loading).
+ ImNodes::LinkId GetLinkId() const;
+
+ void DrawDebugInfo() const;
+ void ReadFrom(std::istream& stream);
+ void WriteTo(std::ostream& stream) const;
+};
+
+class WorkflowNode
+{
+public:
+ static constexpr auto kInvalidId = std::numeric_limits<uint32_t>::max();
+ static constexpr auto kInvalidPinId = std::numeric_limits<uint32_t>::max();
+
+ enum Type
+ {
+ InputType,
+ TransformType,
+ OutputType,
+ };
+
+ enum Kind
+ {
+ KD_NumericAddition,
+ KD_NumericSubtraction,
+ KD_NumericMultiplication,
+ KD_NumericDivision,
+ KD_NumericExpression,
+ KD_TextFormatting,
+ KD_DocumentTemplateExpansion,
+ KD_FormInput,
+ KD_DatabaseRowsInput,
+
+ InvalidKind,
+ KindCount = InvalidKind,
+ };
+
+ enum Category
+ {
+ CG_Numeric,
+ CG_Text,
+ CG_Document,
+ CG_UserInput,
+ CG_SystemInput,
+ CG_Output,
+
+ InvalidCategory,
+ CategoryCount = InvalidCategory,
+ };
+
+ struct InputPin
+ {
+ uint32_t Connection = WorkflowConnection::kInvalidId;
+ BaseValue::Kind MatchingType = BaseValue::InvalidKind;
+ bool ConnectionToConst = false;
+
+ /// A constant connection connects from a user-specified constant value, feeding to a valid \c DestinationNode and \c DestinationPin (i.e. input pins).
+ bool IsConstantConnection() const;
+ bool IsConnected() const;
+ BaseValue::Kind GetMatchingType() const;
+ };
+
+ struct OutputPin
+ {
+ uint32_t Connection = WorkflowConnection::kInvalidId;
+ BaseValue::Kind MatchingType = BaseValue::InvalidKind;
+
+ bool IsConnected() const;
+ BaseValue::Kind GetMatchingType() const;
+ };
+
+protected:
+ friend class Workflow;
+ friend class WorkflowEvaluationContext;
+
+ Workflow* mWorkflow;
+ std::vector<InputPin> mInputs;
+ std::vector<OutputPin> mOutputs;
+ Vec2i mPosition;
+ uint32_t mId;
+ Kind mKind;
+ int mDepth;
+ bool mLocked;
+
+public:
+ static const char* FormatKind(Kind kind);
+ static const char* FormatCategory(Category category);
+ static const char* FormatType(Type type);
+ static Category QueryCategory(Kind kind);
+ static std::span<const Kind> QueryCategoryMembers(Category category);
+ static std::unique_ptr<WorkflowNode> CreateByKind(Kind kind);
+
+ static bool IsInstance(const WorkflowNode* node);
+
+ WorkflowNode(Kind kind, bool locked);
+ virtual ~WorkflowNode() = default;
+
+ WorkflowNode(const WorkflowNode&) = delete;
+ WorkflowNode& operator=(const WorkflowNode&) = delete;
+ WorkflowNode(WorkflowNode&&) = default;
+ WorkflowNode& operator=(WorkflowNode&&) = default;
+
+ void SetPosition(const Vec2i& position);
+ Vec2i GetPosition() const;
+
+ uint32_t GetId() const;
+ /// Used for `NodeId` when interfacing with imgui node editor. Runtime only (not saved to disk and generated when loading).
+ ImNodes::NodeId GetNodeId() const;
+ Kind GetKind() const;
+ int GetDepth() const;
+ bool IsLocked() const;
+
+ Type GetType() const;
+ bool IsInputNode() const;
+ bool IsOutputNode() const;
+
+ void ConnectInput(uint32_t pinId, WorkflowNode& srcNode, uint32_t srcPinId);
+ void DisconnectInput(uint32_t pinId);
+
+ void DrawInputPinDebugInfo(uint32_t pinId) const;
+ const InputPin& GetInputPin(uint32_t pinId) const;
+ ImNodes::PinId GetInputPinUniqueId(uint32_t pinId) const;
+
+ void ConnectOutput(uint32_t pinId, WorkflowNode& dstNode, uint32_t dstPinId);
+ void DisconnectOutput(uint32_t pinId);
+
+ void DrawOutputPinDebugInfo(uint32_t pinId) const;
+ const OutputPin& GetOutputPin(uint32_t pinId) const;
+ ImNodes::PinId GetOutputPinUniqueId(uint32_t pinId) const;
+
+ virtual void Evaluate(WorkflowEvaluationContext& ctx) = 0;
+
+ void Draw();
+ virtual void DrawExtra() {}
+
+ void DrawDebugInfo() const;
+ virtual void DrawExtraDebugInfo() const {}
+
+ virtual void ReadFrom(std::istream& istream);
+ virtual void WriteTo(std::ostream& ostream);
+
+protected:
+ InputPin& InsertInputPin(int atIdx);
+ void RemoveInputPin(int pin);
+ void SwapInputPin(int a, int b);
+ OutputPin& InsertOutputPin(int atIdx);
+ void RemoveOutputPin(int pin);
+ void SwapOutputPin(int a, int b);
+
+ /* For \c Workflow to invoke, override by implementations */
+
+ void OnAttach(Workflow& workflow, uint32_t newId);
+ void OnDetach();
+};
+
+class Workflow : public Asset
+{
+ friend class WorkflowNode;
+ friend class WorkflowEvaluationContext;
+ class Private;
+
+public:
+ using CategoryType = WorkflowAssetList;
+ static constinit const WorkflowAssetList Category;
+
+private:
+ std::vector<WorkflowConnection> mConnections;
+ std::vector<std::unique_ptr<WorkflowNode>> mNodes;
+ std::vector<std::unique_ptr<BaseValue>> mConstants;
+ std::vector<std::vector<uint32_t>> mDepthGroups;
+ int mConnectionCount;
+ int mNodeCount;
+ int mConstantCount;
+ bool mDepthsDirty = true;
+
+public:
+ /* Graph access */
+
+ const std::vector<WorkflowConnection>& GetConnections() const;
+ std::vector<WorkflowConnection>& GetConnections();
+ const std::vector<std::unique_ptr<WorkflowNode>>& GetNodes() const;
+ std::vector<std::unique_ptr<WorkflowNode>>& GetNodes();
+ const std::vector<std::unique_ptr<BaseValue>>& GetConstants() const;
+ std::vector<std::unique_ptr<BaseValue>>& GetConstants();
+
+ WorkflowConnection* GetConnectionById(uint32_t id);
+ WorkflowConnection* GetConnectionByLinkId(ImNodes::LinkId linkId);
+ WorkflowNode* GetNodeById(uint32_t id);
+ WorkflowNode* GetNodeByNodeId(ImNodes::NodeId nodeId);
+ BaseValue* GetConstantById(uint32_t id);
+
+ struct GlobalPinId
+ {
+ WorkflowNode* Node;
+ uint32_t PinId;
+ /// true => input pin
+ /// false => output pin
+ bool IsOutput;
+ };
+
+ /// `pinId` should be the `UniqueId` of a pin from a node that's within this workflow.
+ GlobalPinId DisassembleGlobalPinId(ImNodes::PinId id);
+ ImNodes::PinId FabricateGlobalPinId(const WorkflowNode& node, uint32_t pinId, bool isOutput) const;
+
+ const std::vector<std::vector<uint32_t>>& GetDepthGroups() const;
+ bool DoesDepthNeedsUpdate() const;
+
+ /* Graph mutation */
+
+ void AddNode(std::unique_ptr<WorkflowNode> step);
+ void RemoveNode(uint32_t id);
+
+ void RemoveConnection(uint32_t id);
+
+ bool Connect(WorkflowNode& sourceNode, uint32_t sourcePin, WorkflowNode& destinationNode, uint32_t destinationPin);
+ bool DisconnectBySource(WorkflowNode& sourceNode, uint32_t sourcePin);
+ bool DisconnectByDestination(WorkflowNode& destinationNode, uint32_t destinationPin);
+
+ /* Graph rebuild */
+
+ enum GraphUpdateResult
+ {
+ /// Successfully rebuilt graph dependent data.
+ /// Details: nothing is written.
+ GUR_Success,
+ /// Nothing has changed since last time UpdateGraph() was called.
+ /// Details: nothing is written.
+ GUR_NoWorkToDo,
+ /// Details: list of nodes is written.
+ GUR_UnsatisfiedDependencies,
+ /// Details: list of nodes is written.
+ GUR_UnreachableNodes,
+ };
+
+ using GraphUpdateDetails = std::variant<
+ // Case: nothing
+ std::monostate,
+ // Case: list of nodes (ids)
+ std::vector<uint32_t>>;
+
+ GraphUpdateResult UpdateGraph(GraphUpdateDetails* details = nullptr);
+
+ /* Serialization */
+
+ void ReadFromDataStream(InputDataStream& stream);
+ void WriteToDataStream(OutputDataStream& stream) const;
+
+private:
+ std::pair<WorkflowConnection&, uint32_t> AllocWorkflowConnection();
+ std::pair<std::unique_ptr<WorkflowNode>&, uint32_t> AllocWorkflowStep();
+};
+
+class WorkflowAssetList final : public AssetListTyped<Workflow>
+{
+private:
+ // AC = Asset Creator
+ std::string mACNewName;
+ NameSelectionError mACNewNameError = NameSelectionError::Empty;
+
+public:
+ // Inherit constructors
+ using AssetListTyped::AssetListTyped;
+
+protected:
+ void DiscoverFiles(const std::function<void(SavedAsset)>& callback) const override;
+
+ std::string RetrieveNameFromFile(const std::filesystem::path& file) const override;
+ uuids::uuid RetrieveUuidFromFile(const std::filesystem::path& file) const override;
+ std::filesystem::path RetrievePathFromAsset(const SavedAsset& asset) const override;
+
+ bool SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const override;
+ Workflow* LoadInstance(const SavedAsset& assetInfo) const override;
+ Workflow* CreateInstance(const SavedAsset& assetInfo) const override;
+ bool RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const override;
+
+ void DisplayAssetCreator(ListState& state) override;
+ void DisplayDetailsTable(ListState& state) const override;
+};
diff --git a/app/source/Cplt/Model/Workflow/Workflow_Main.cpp b/app/source/Cplt/Model/Workflow/Workflow_Main.cpp
new file mode 100644
index 0000000..0f35b32
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Workflow_Main.cpp
@@ -0,0 +1,846 @@
+#include "Workflow.hpp"
+
+#include <Cplt/Model/GlobalStates.hpp>
+#include <Cplt/Model/Project.hpp>
+#include <Cplt/UI/UI.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/IO/Archive.hpp>
+#include <Cplt/Utils/UUID.hpp>
+
+#include <imgui.h>
+#include <imgui_node_editor.h>
+#include <imgui_stdlib.h>
+#include <tsl/robin_set.h>
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <fstream>
+#include <iostream>
+#include <queue>
+#include <utility>
+
+using namespace std::literals::string_view_literals;
+namespace fs = std::filesystem;
+namespace ImNodes = ax::NodeEditor;
+
+WorkflowConnection::WorkflowConnection()
+ : Id{ 0 }
+ , SourceNode{ WorkflowNode::kInvalidId }
+ , SourcePin{ WorkflowNode::kInvalidPinId }
+ , DestinationNode{ WorkflowNode::kInvalidId }
+ , DestinationPin{ WorkflowNode::kInvalidPinId }
+{
+}
+
+bool WorkflowConnection::IsValid() const
+{
+ return Id != 0;
+}
+
+ImNodes::LinkId WorkflowConnection::GetLinkId() const
+{
+ // Our id is 0-based (represents an index directly)
+ // but imgui-node-editor uses the value 0 to represent a null id, so we need to offset by 1
+ return Id + 1;
+}
+
+void WorkflowConnection::DrawDebugInfo() const
+{
+ ImGui::Text("Source (node with output pin):");
+ ImGui::Text("{ Node = %u, Pin = %u }", SourceNode, SourcePin);
+ ImGui::Text("Destination (node with input pin):");
+ ImGui::Text("{ Node = %u, Pin = %u }", DestinationNode, DestinationPin);
+}
+
+void WorkflowConnection::ReadFrom(std::istream& stream)
+{
+ stream >> SourceNode >> SourcePin;
+ stream >> DestinationNode >> DestinationPin;
+}
+
+void WorkflowConnection::WriteTo(std::ostream& stream) const
+{
+ stream << SourceNode << SourcePin;
+ stream << DestinationNode << DestinationPin;
+}
+
+bool WorkflowNode::InputPin::IsConstantConnection() const
+{
+ return ConnectionToConst && IsConnected();
+}
+
+bool WorkflowNode::InputPin::IsConnected() const
+{
+ return Connection != WorkflowConnection::kInvalidId;
+}
+
+BaseValue::Kind WorkflowNode::InputPin::GetMatchingType() const
+{
+ return MatchingType;
+}
+
+bool WorkflowNode::OutputPin::IsConnected() const
+{
+ return Connection != WorkflowConnection::kInvalidId;
+}
+
+BaseValue::Kind WorkflowNode::OutputPin::GetMatchingType() const
+{
+ return MatchingType;
+}
+
+WorkflowNode::WorkflowNode(Kind kind, bool locked)
+ : mKind{ kind }
+ , mDepth{ -1 }
+ , mLocked(locked)
+{
+}
+
+Vec2i WorkflowNode::GetPosition() const
+{
+ return mPosition;
+}
+
+void WorkflowNode::SetPosition(const Vec2i& position)
+{
+ mPosition = position;
+}
+
+uint32_t WorkflowNode::GetId() const
+{
+ return mId;
+}
+
+ImNodes::NodeId WorkflowNode::GetNodeId() const
+{
+ // See WorkflowConnection::GetLinkId for the rationale
+ return mId + 1;
+}
+
+WorkflowNode::Kind WorkflowNode::GetKind() const
+{
+ return mKind;
+}
+
+int WorkflowNode::GetDepth() const
+{
+ return mDepth;
+}
+
+bool WorkflowNode::IsLocked() const
+{
+ return mLocked;
+}
+
+WorkflowNode::Type WorkflowNode::GetType() const
+{
+ if (IsInputNode()) {
+ return InputType;
+ } else if (IsOutputNode()) {
+ return OutputType;
+ } else {
+ return TransformType;
+ }
+}
+
+bool WorkflowNode::IsInputNode() const
+{
+ return mInputs.size() == 0;
+}
+
+bool WorkflowNode::IsOutputNode() const
+{
+ return mOutputs.size() == 0;
+}
+
+void WorkflowNode::ConnectInput(uint32_t pinId, WorkflowNode& srcNode, uint32_t srcPinId)
+{
+ mWorkflow->Connect(*this, pinId, srcNode, srcPinId);
+}
+
+void WorkflowNode::DisconnectInput(uint32_t pinId)
+{
+ mWorkflow->DisconnectByDestination(*this, pinId);
+}
+
+void WorkflowNode::DrawInputPinDebugInfo(uint32_t pinId) const
+{
+ ImGui::Text("Node ID: %d", mId);
+ ImGui::Text("Pin ID: (input) %d", pinId);
+}
+
+const WorkflowNode::InputPin& WorkflowNode::GetInputPin(uint32_t pinId) const
+{
+ return mInputs[pinId];
+}
+
+ImNodes::PinId WorkflowNode::GetInputPinUniqueId(uint32_t pinId) const
+{
+ return mWorkflow->FabricateGlobalPinId(*this, pinId, false);
+}
+
+void WorkflowNode::ConnectOutput(uint32_t pinId, WorkflowNode& dstNode, uint32_t dstPinId)
+{
+ mWorkflow->Connect(dstNode, dstPinId, *this, pinId);
+}
+
+void WorkflowNode::DisconnectOutput(uint32_t pinId)
+{
+ mWorkflow->DisconnectBySource(*this, pinId);
+}
+
+void WorkflowNode::DrawOutputPinDebugInfo(uint32_t pinId) const
+{
+ ImGui::Text("Node ID: %d", mId);
+ ImGui::Text("Pin ID: (output) %d", pinId);
+}
+
+const WorkflowNode::OutputPin& WorkflowNode::GetOutputPin(uint32_t pinId) const
+{
+ return mOutputs[pinId];
+}
+
+ImNodes::PinId WorkflowNode::GetOutputPinUniqueId(uint32_t pinId) const
+{
+ return mWorkflow->FabricateGlobalPinId(*this, pinId, true);
+}
+
+void WorkflowNode::Draw()
+{
+ for (uint32_t i = 0; i < mInputs.size(); ++i) {
+ auto& pin = mInputs[i];
+ auto& typeInfo = BaseValue::QueryInfo(pin.MatchingType);
+ ImNodes::BeginPin(GetInputPinUniqueId(i), ImNodes::PinKind::Input);
+ // TODO
+ ImNodes::EndPin();
+ }
+ for (uint32_t i = 0; i < mOutputs.size(); ++i) {
+ auto& pin = mOutputs[i];
+ auto& typeInfo = BaseValue::QueryInfo(pin.MatchingType);
+ ImNodes::BeginPin(GetOutputPinUniqueId(i), ImNodes::PinKind::Output);
+ // TODO
+ ImNodes::EndPin();
+ }
+}
+
+void WorkflowNode::DrawDebugInfo() const
+{
+ ImGui::Text("Node kind: %s", FormatKind(mKind));
+ ImGui::Text("Node type: %s", FormatType(GetType()));
+ ImGui::Text("Node ID: %u", mId);
+ ImGui::Text("Depth: %d", mDepth);
+ DrawExtraDebugInfo();
+}
+
+void WorkflowNode::ReadFrom(std::istream& stream)
+{
+ stream >> mId;
+ stream >> mPosition.x >> mPosition.y;
+}
+
+void WorkflowNode::WriteTo(std::ostream& stream)
+{
+ stream << mId;
+ stream << mPosition.x << mPosition.y;
+}
+
+WorkflowNode::InputPin& WorkflowNode::InsertInputPin(int atIdx)
+{
+ assert(atIdx >= 0 && atIdx < mInputs.size());
+
+ mInputs.push_back(InputPin{});
+ for (int i = (int)mInputs.size() - 1, end = atIdx + 1; i >= end; --i) {
+ SwapInputPin(i, i + 1);
+ }
+
+ return mInputs[atIdx];
+}
+
+void WorkflowNode::RemoveInputPin(int pin)
+{
+ DisconnectInput(pin);
+ for (int i = 0, end = (int)mInputs.size() - 1; i < end; ++i) {
+ SwapInputPin(i, i + 1);
+ }
+ mInputs.resize(mInputs.size() - 1);
+}
+
+void WorkflowNode::SwapInputPin(int a, int b)
+{
+ auto& pinA = mInputs[a];
+ auto& pinB = mInputs[b];
+
+ if (mWorkflow) {
+ if (pinA.IsConnected() && !pinA.IsConstantConnection()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinA.Connection);
+ conn.DestinationPin = b;
+ }
+ if (pinB.IsConnected() && !pinB.IsConstantConnection()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinB.Connection);
+ conn.DestinationPin = a;
+ }
+ }
+
+ std::swap(pinA, pinB);
+}
+
+WorkflowNode::OutputPin& WorkflowNode::InsertOutputPin(int atIdx)
+{
+ assert(atIdx >= 0 && atIdx < mOutputs.size());
+
+ mOutputs.push_back(OutputPin{});
+ for (int i = (int)mOutputs.size() - 1, end = atIdx + 1; i >= end; --i) {
+ SwapOutputPin(i, i + 1);
+ }
+
+ return mOutputs[atIdx];
+}
+
+void WorkflowNode::RemoveOutputPin(int pin)
+{
+ DisconnectOutput(pin);
+ for (int i = 0, end = (int)mOutputs.size() - 1; i < end; ++i) {
+ SwapInputPin(i, i + 1);
+ }
+ mOutputs.resize(mOutputs.size() - 1);
+}
+
+void WorkflowNode::SwapOutputPin(int a, int b)
+{
+ auto& pinA = mOutputs[a];
+ auto& pinB = mOutputs[b];
+
+ if (mWorkflow) {
+ if (pinA.IsConnected()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinA.Connection);
+ conn.SourcePin = b;
+ }
+ if (pinB.IsConnected()) {
+ auto& conn = *mWorkflow->GetConnectionById(pinB.Connection);
+ conn.SourcePin = a;
+ }
+ }
+
+ std::swap(pinA, pinB);
+}
+
+void WorkflowNode::OnAttach(Workflow& workflow, uint32_t newId)
+{
+}
+
+void WorkflowNode::OnDetach()
+{
+}
+
+const std::vector<WorkflowConnection>& Workflow::GetConnections() const
+{
+ return mConnections;
+}
+
+std::vector<WorkflowConnection>& Workflow::GetConnections()
+{
+ return mConnections;
+}
+
+const std::vector<std::unique_ptr<WorkflowNode>>& Workflow::GetNodes() const
+{
+ return mNodes;
+}
+
+std::vector<std::unique_ptr<WorkflowNode>>& Workflow::GetNodes()
+{
+ return mNodes;
+}
+
+const std::vector<std::unique_ptr<BaseValue>>& Workflow::GetConstants() const
+{
+ return mConstants;
+}
+
+std::vector<std::unique_ptr<BaseValue>>& Workflow::GetConstants()
+{
+ return mConstants;
+}
+
+WorkflowConnection* Workflow::GetConnectionById(uint32_t id)
+{
+ return &mConnections[id];
+}
+
+WorkflowConnection* Workflow::GetConnectionByLinkId(ImNodes::LinkId id)
+{
+ return &mConnections[(uint32_t)(size_t)id - 1];
+}
+
+WorkflowNode* Workflow::GetNodeById(uint32_t id)
+{
+ return mNodes[id].get();
+}
+
+WorkflowNode* Workflow::GetNodeByNodeId(ImNodes::NodeId id)
+{
+ return mNodes[(uint32_t)(size_t)id - 1].get();
+}
+
+BaseValue* Workflow::GetConstantById(uint32_t id)
+{
+ return mConstants[id].get();
+}
+
+Workflow::GlobalPinId Workflow::DisassembleGlobalPinId(ImNodes::PinId pinId)
+{
+ // imgui-node-editor requires all pins to have a global, unique id
+ // but in our model the pin are typed (input vs output) and associated with a node: there is no built-in global id
+ // Therefore we encode one ourselves
+
+ // Global pin id format
+ // nnnnnnnn nnnnnnnn nnnnnnnn nnnnnnnn Tppppppp ppppppppp pppppppp pppppppp
+ // <------- (32 bits) node id -------> ^<------ (31 bits) pin id -------->
+ // | (1 bit) input (false) vs output (true)
+
+ // 1 is added to pin id to prevent the 0th node's 0th input pin resulting in a 0 global pin id
+ // (this is problematic because imgui-node-editor use 0 to represent null)
+
+ auto id = static_cast<uint64_t>(pinId);
+ GlobalPinId result;
+
+ result.Node = mNodes[id >> 32].get();
+ result.PinId = (uint32_t)(id & 0x000000001FFFFFFF) - 1;
+ result.IsOutput = id >> 31;
+
+ return result;
+}
+
+ImNodes::PinId Workflow::FabricateGlobalPinId(const WorkflowNode& node, uint32_t pinId, bool isOutput) const
+{
+ // See this->DisassembleGlobalPinId for format details and rationale
+
+ uint64_t id = 0;
+ id |= ((uint64_t)node.GetId() << 32);
+ id |= (isOutput << 31);
+ id |= ((pinId + 1) & 0x1FFFFFFF);
+
+ return id;
+}
+
+const std::vector<std::vector<uint32_t>>& Workflow::GetDepthGroups() const
+{
+ return mDepthGroups;
+}
+
+bool Workflow::DoesDepthNeedsUpdate() const
+{
+ return mDepthsDirty;
+}
+
+void Workflow::AddNode(std::unique_ptr<WorkflowNode> step)
+{
+ auto [storage, id] = AllocWorkflowStep();
+ storage = std::move(step);
+ storage->OnAttach(*this, id);
+ storage->mWorkflow = this;
+ storage->mId = id;
+}
+
+void Workflow::RemoveNode(uint32_t id)
+{
+ auto& step = mNodes[id];
+ if (step == nullptr) return;
+
+ step->OnDetach();
+ step->mWorkflow = nullptr;
+ step->mId = WorkflowNode::kInvalidId;
+}
+
+void Workflow::RemoveConnection(uint32_t id)
+{
+ auto& conn = mConnections[id];
+ if (!conn.IsValid()) return;
+
+ mNodes[conn.SourceNode]->mInputs[conn.SourcePin].Connection = WorkflowNode::kInvalidId;
+ mNodes[conn.DestinationNode]->mInputs[conn.DestinationPin].Connection = WorkflowNode::kInvalidId;
+
+ conn = {};
+ mDepthsDirty = true;
+}
+
+bool Workflow::Connect(WorkflowNode& sourceNode, uint32_t sourcePin, WorkflowNode& destinationNode, uint32_t destinationPin)
+{
+ auto& src = sourceNode.mOutputs[sourcePin];
+ auto& dst = destinationNode.mInputs[destinationPin];
+
+ // TODO report error to user?
+ if (src.GetMatchingType() != dst.GetMatchingType()) {
+ return false;
+ }
+
+ if (src.IsConnected()) {
+ DisconnectBySource(sourceNode, sourcePin);
+ }
+
+ auto [conn, id] = AllocWorkflowConnection();
+ conn.SourceNode = sourceNode.GetId();
+ conn.SourcePin = sourcePin;
+ conn.DestinationNode = destinationNode.GetId();
+ conn.DestinationPin = destinationPin;
+
+ src.Connection = id;
+ dst.Connection = id;
+
+ mDepthsDirty = true;
+ return true;
+}
+
+bool Workflow::DisconnectBySource(WorkflowNode& sourceNode, uint32_t sourcePin)
+{
+ auto& sn = sourceNode.mOutputs[sourcePin];
+ if (!sn.IsConnected()) return false;
+
+ auto& conn = mConnections[sn.Connection];
+ auto& dn = mNodes[conn.DestinationNode]->mInputs[conn.DestinationPin];
+
+ sn.Connection = WorkflowConnection::kInvalidId;
+ dn.Connection = WorkflowConnection::kInvalidId;
+ conn = {};
+
+ mDepthsDirty = true;
+ return true;
+}
+
+bool Workflow::DisconnectByDestination(WorkflowNode& destinationNode, uint32_t destinationPin)
+{
+ auto& dn = destinationNode.mOutputs[destinationPin];
+ if (!dn.IsConnected()) return false;
+
+ auto& conn = mConnections[dn.Connection];
+ auto& sn = mNodes[conn.SourceNode]->mInputs[conn.SourcePin];
+
+ sn.Connection = WorkflowConnection::kInvalidId;
+ dn.Connection = WorkflowConnection::kInvalidId;
+ conn = {};
+
+ mDepthsDirty = true;
+ return true;
+}
+
+Workflow::GraphUpdateResult Workflow::UpdateGraph(GraphUpdateDetails* details)
+{
+ if (!mDepthsDirty) {
+ return GUR_NoWorkToDo;
+ }
+
+ // Terminology:
+ // - Dependency = nodes its input pins are connected to
+ // - Dependents = nodes its output pins are connected to
+
+ struct WorkingNode
+ {
+ // The max depth out of all dependency nodes, maintained during the traversal and committed as the actual depth
+ // when all dependencies of this node has been resolved. Add 1 to get the depth that will be assigned to the node.
+ int MaximumDepth = 0;
+ int FulfilledInputCount = 0;
+ };
+
+ std::vector<WorkingNode> workingNodes;
+ std::queue<uint32_t> q;
+
+ // Check if all dependencies of this node is satisfied
+ auto CheckNodeDependencies = [&](WorkflowNode& node) -> bool {
+ for (auto& pin : node.mInputs) {
+ if (!pin.IsConnected()) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ workingNodes.reserve(mNodes.size());
+ {
+ std::vector<uint32_t> unsatisfiedNodes;
+ for (uint32_t i = 0; i < mNodes.size(); ++i) {
+ auto& node = mNodes[i];
+ workingNodes.push_back(WorkingNode{});
+
+ if (!node) continue;
+
+ if (!CheckNodeDependencies(*node)) {
+ unsatisfiedNodes.push_back(i);
+ }
+
+ node->mDepth = -1;
+
+ // Start traversing with the input nodes
+ if (node->GetType() == WorkflowNode::InputType) {
+ q.push(i);
+ }
+ }
+
+ if (!unsatisfiedNodes.empty()) {
+ if (details) {
+ details->emplace<decltype(unsatisfiedNodes)>(std::move(unsatisfiedNodes));
+ }
+ return GUR_UnsatisfiedDependencies;
+ }
+ }
+
+ auto ProcessNode = [&](WorkflowNode& node) -> void {
+ for (auto& pin : node.mOutputs) {
+ if (!pin.IsConnected()) continue;
+ auto& conn = mConnections[pin.Connection];
+
+ auto& wn = workingNodes[conn.DestinationNode];
+ auto& n = *mNodes[conn.DestinationPin].get();
+
+ wn.FulfilledInputCount++;
+ wn.MaximumDepth = std::max(node.mDepth, wn.MaximumDepth);
+
+ // Node's dependency is fulfilled, we can process its dependents next
+ // We use >= here because for a many-to-one pin, the dependency is an "or" relation ship, i.e. any of the nodes firing before this will fulfill the requirement
+ if (n.mInputs.size() >= wn.FulfilledInputCount) {
+ n.mDepth = wn.MaximumDepth + 1;
+ }
+ }
+ };
+
+ int processedNodes = 0;
+ while (!q.empty()) {
+ auto& wn = workingNodes[q.front()];
+ auto& n = *mNodes[q.front()];
+ q.pop();
+ processedNodes++;
+
+ ProcessNode(n);
+ }
+
+ if (processedNodes < mNodes.size()) {
+ // There is unreachable nodes, collect them and report to the caller
+
+ std::vector<uint32_t> unreachableNodes;
+ for (uint32_t i = 0; i < mNodes.size(); ++i) {
+ auto& wn = workingNodes[i];
+ auto& n = *mNodes[i];
+
+ // This is a reachable node
+ if (n.mDepth != -1) continue;
+
+ unreachableNodes.push_back(i);
+ }
+
+ if (details) {
+ details->emplace<decltype(unreachableNodes)>(std::move(unreachableNodes));
+ }
+ return GUR_UnreachableNodes;
+ }
+
+ return GUR_Success;
+}
+
+class Workflow::Private
+{
+public:
+ template <class TSelf, class TProxy>
+ static void OperateStream(TSelf& self, TProxy& proxy)
+ {
+ // TODO
+ }
+};
+
+void Workflow::ReadFromDataStream(InputDataStream& stream)
+{
+ Private::OperateStream(*this, stream);
+}
+
+void Workflow::WriteToDataStream(OutputDataStream& stream) const
+{
+ Private::OperateStream(*this, stream);
+}
+
+std::pair<WorkflowConnection&, uint32_t> Workflow::AllocWorkflowConnection()
+{
+ for (size_t idx = 0; idx < mConnections.size(); ++idx) {
+ auto& elm = mConnections[idx];
+ if (!elm.IsValid()) {
+ return { elm, (uint32_t)idx };
+ }
+ }
+
+ auto id = (uint32_t)mConnections.size();
+ auto& conn = mConnections.emplace_back(WorkflowConnection{});
+ conn.Id = id;
+
+ return { conn, id };
+}
+
+std::pair<std::unique_ptr<WorkflowNode>&, uint32_t> Workflow::AllocWorkflowStep()
+{
+ for (size_t idx = 0; idx < mNodes.size(); ++idx) {
+ auto& elm = mNodes[idx];
+ if (elm == nullptr) {
+ return { elm, (uint32_t)idx };
+ }
+ }
+
+ auto id = (uint32_t)mNodes.size();
+ auto& node = mNodes.emplace_back(std::unique_ptr<WorkflowNode>());
+
+ return { node, id };
+}
+
+void WorkflowAssetList::DiscoverFiles(const std::function<void(SavedAsset)>& callback) const
+{
+ auto dir = GetConnectedProject().GetWorkflowsDirectory();
+ DiscoverFilesByExtension(callback, dir, ".cplt-workflow"sv);
+}
+
+std::string WorkflowAssetList::RetrieveNameFromFile(const fs::path& file) const
+{
+ auto res = DataArchive::LoadFile(file);
+ if (!res) return "";
+ auto& stream = res.value();
+
+ SavedAsset assetInfo;
+ stream.ReadObject(assetInfo);
+
+ return assetInfo.Name;
+}
+
+uuids::uuid WorkflowAssetList::RetrieveUuidFromFile(const fs::path& file) const
+{
+ return uuids::uuid::from_string(file.stem().string());
+}
+
+fs::path WorkflowAssetList::RetrievePathFromAsset(const SavedAsset& asset) const
+{
+ auto fileName = uuids::to_string(asset.Uuid);
+ return GetConnectedProject().GetWorkflowPath(fileName);
+}
+
+bool WorkflowAssetList::SaveInstance(const SavedAsset& assetInfo, const Asset* asset) const
+{
+ auto path = RetrievePathFromAsset(assetInfo);
+ auto res = DataArchive::SaveFile(path);
+ if (!res) return false;
+ auto& stream = res.value();
+
+ stream.WriteObject(assetInfo);
+ // This cast is fine: calls to this class will always be wrapped in TypedAssetList<T>, which will ensure `asset` points to some Workflow
+ if (auto workflow = static_cast<const Workflow*>(asset)) { // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
+ stream.WriteObject(*workflow);
+ }
+
+ return true;
+}
+
+static std::unique_ptr<Workflow> LoadWorkflowFromFile(const fs::path& path)
+{
+ auto res = DataArchive::LoadFile(path);
+ if (!res) return nullptr;
+ auto& stream = res.value();
+
+ // TODO this is currently unused
+ SavedAsset assetInfo;
+ stream.ReadObject(assetInfo);
+
+ auto workflow = std::make_unique<Workflow>();
+ stream.ReadObject(*workflow);
+
+ return workflow;
+}
+
+Workflow* WorkflowAssetList::LoadInstance(const SavedAsset& assetInfo) const
+{
+ return ::LoadWorkflowFromFile(RetrievePathFromAsset(assetInfo)).release();
+}
+
+Workflow* WorkflowAssetList::CreateInstance(const SavedAsset& assetInfo) const
+{
+ return new Workflow();
+}
+
+bool WorkflowAssetList::RenameInstanceOnDisk(const SavedAsset& assetInfo, std::string_view oldName) const
+{
+ auto path = RetrievePathFromAsset(assetInfo);
+
+ auto workflow = ::LoadWorkflowFromFile(path);
+ if (!workflow) return false;
+
+ SaveInstance(assetInfo, workflow.get());
+
+ return true;
+}
+
+void WorkflowAssetList::DisplayAssetCreator(ListState& state)
+{
+ auto ValidateNewName = [&]() -> void {
+ if (mACNewName.empty()) {
+ mACNewNameError = NameSelectionError::Empty;
+ return;
+ }
+
+ if (FindByName(mACNewName)) {
+ mACNewNameError = NameSelectionError::Duplicated;
+ return;
+ }
+
+ mACNewNameError = NameSelectionError::None;
+ };
+
+ auto ShowNewNameErrors = [&]() -> void {
+ switch (mACNewNameError) {
+ case NameSelectionError::None: break;
+ case NameSelectionError::Duplicated:
+ ImGui::ErrorMessage(I18N_TEXT("Duplicate name", L10N_DUPLICATE_NAME_ERROR));
+ break;
+ case NameSelectionError::Empty:
+ ImGui::ErrorMessage(I18N_TEXT("Name cannot be empty", L10N_EMPTY_NAME_ERROR));
+ break;
+ }
+ };
+
+ auto IsInputValid = [&]() -> bool {
+ return mACNewNameError == NameSelectionError::None;
+ };
+
+ auto ResetState = [&]() -> void {
+ mACNewName.clear();
+ ValidateNewName();
+ };
+
+ if (ImGui::InputText(I18N_TEXT("Name", L10N_NAME), &mACNewName)) {
+ ValidateNewName();
+ }
+
+ ShowNewNameErrors();
+
+ if (ImGui::Button(I18N_TEXT("OK", L10N_CONFIRM), !IsInputValid())) {
+ ImGui::CloseCurrentPopup();
+
+ Create(SavedAsset{
+ .Name = mACNewName,
+ });
+ ResetState();
+ }
+ ImGui::SameLine();
+ if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) {
+ ImGui::CloseCurrentPopup();
+ }
+}
+
+void WorkflowAssetList::DisplayDetailsTable(ListState& state) const
+{
+ ImGui::BeginTable("AssetDetailsTable", 1, ImGuiTableFlags_Borders);
+
+ ImGui::TableSetupColumn(I18N_TEXT("Name", L10N_NAME));
+ ImGui::TableHeadersRow();
+
+ for (auto& asset : this->GetAssets()) {
+ ImGui::TableNextRow();
+
+ ImGui::TableNextColumn();
+ if (ImGui::Selectable(asset.Name.c_str(), state.SelectedAsset == &asset, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_DontClosePopups)) {
+ state.SelectedAsset = &asset;
+ }
+ }
+
+ ImGui::EndTable();
+}
diff --git a/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp b/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp
new file mode 100644
index 0000000..ee3da28
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/Workflow_RTTI.cpp
@@ -0,0 +1,143 @@
+#include "Workflow.hpp"
+
+#include <Cplt/Model/Workflow/Nodes/DocumentNodes.hpp>
+#include <Cplt/Model/Workflow/Nodes/NumericNodes.hpp>
+#include <Cplt/Model/Workflow/Nodes/TextNodes.hpp>
+#include <Cplt/Model/Workflow/Nodes/UserInputNodes.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/Macros.hpp>
+
+#include <memory>
+
+const char* WorkflowNode::FormatKind(Kind kind)
+{
+ switch (kind) {
+ case KD_NumericAddition: return I18N_TEXT("Add", L10N_WORKFLOW_ADD);
+ case KD_NumericSubtraction: return I18N_TEXT("Subtract", L10N_WORKFLOW_SUB);
+ case KD_NumericMultiplication: return I18N_TEXT("Multiply", L10N_WORKFLOW_MUL);
+ case KD_NumericDivision: return I18N_TEXT("Divide", L10N_WORKFLOW_DIV);
+ case KD_NumericExpression: return I18N_TEXT("Evaluate expression", L10N_WORKFLOW_EVAL);
+ case KD_TextFormatting: return I18N_TEXT("Format text", L10N_WORKFLOW_FMT);
+ case KD_DocumentTemplateExpansion: return I18N_TEXT("Expand template", L10N_WORKFLOW_INSTANTIATE_TEMPLATE);
+ case KD_FormInput: return I18N_TEXT("Form input", L10N_WORKFLOW_FORM_INPUT);
+ case KD_DatabaseRowsInput: return I18N_TEXT("Database input", L10N_WORKFLOW_DB_INPUT);
+
+ case InvalidKind: break;
+ }
+ return "";
+}
+
+const char* WorkflowNode::FormatCategory(WorkflowNode::Category category)
+{
+ switch (category) {
+ case CG_Numeric: return I18N_TEXT("Numeric", L10N_WORKFLOW_CATEGORY_NUMERIC);
+ case CG_Text: return I18N_TEXT("Text", L10N_WORKFLOW_CATEGORY_TEXT);
+ case CG_Document: return I18N_TEXT("Document", L10N_WORKFLOW_CATEGORY_DOCUMENT);
+ case CG_UserInput: return I18N_TEXT("User input", L10N_WORKFLOW_CATEGORY_USER_INPUT);
+ case CG_SystemInput: return I18N_TEXT("System input", L10N_WORKFLOW_CATEGORY_SYS_INPUT);
+ case CG_Output: return I18N_TEXT("Output", L10N_WORKFLOW_CATEGORY_OUTPUT);
+
+ case InvalidCategory: break;
+ }
+ return "";
+}
+
+const char* WorkflowNode::FormatType(Type type)
+{
+ switch (type) {
+ case InputType: return I18N_TEXT("Input", L10N_WORKFLOW_KIND_INPUT);
+ case TransformType: return I18N_TEXT("Transform", L10N_WORKFLOW_KIND_TRANSFORM);
+ case OutputType: return I18N_TEXT("Output", L10N_WORKFLOW_KIND_OUTPUT);
+ }
+ return "";
+}
+
+WorkflowNode::Category WorkflowNode::QueryCategory(Kind kind)
+{
+ switch (kind) {
+ case KD_NumericAddition:
+ case KD_NumericSubtraction:
+ case KD_NumericMultiplication:
+ case KD_NumericDivision:
+ case KD_NumericExpression:
+ return CG_Numeric;
+ case KD_TextFormatting:
+ return CG_Text;
+ case KD_DocumentTemplateExpansion:
+ return CG_Document;
+ case KD_FormInput:
+ case KD_DatabaseRowsInput:
+ return CG_UserInput;
+
+ case InvalidKind: break;
+ }
+ return InvalidCategory;
+}
+
+std::span<const WorkflowNode::Kind> WorkflowNode::QueryCategoryMembers(Category category)
+{
+ constexpr WorkflowNode::Kind kNumeric[] = {
+ KD_NumericAddition,
+ KD_NumericSubtraction,
+ KD_NumericMultiplication,
+ KD_NumericDivision,
+ KD_NumericExpression,
+ };
+
+ constexpr WorkflowNode::Kind kText[] = {
+ KD_TextFormatting,
+ };
+
+ constexpr WorkflowNode::Kind kDocument[] = {
+ KD_DocumentTemplateExpansion,
+ };
+
+ constexpr WorkflowNode::Kind kUserInput[] = {
+ KD_FormInput,
+ KD_DatabaseRowsInput,
+ };
+
+ // TODO remove invalid kinds after we have nodes of these categories
+ constexpr WorkflowNode::Kind kSystemInput[] = {
+ InvalidKind,
+ };
+
+ constexpr WorkflowNode::Kind kOutput[] = {
+ InvalidKind,
+ };
+
+ switch (category) {
+ case CG_Numeric: return kNumeric;
+ case CG_Text: return kText;
+ case CG_Document: return kDocument;
+ case CG_UserInput: return kUserInput;
+ case CG_SystemInput: return kSystemInput;
+ case CG_Output: return kOutput;
+
+ case InvalidCategory: break;
+ }
+ return {};
+}
+
+std::unique_ptr<WorkflowNode> WorkflowNode::CreateByKind(WorkflowNode::Kind kind)
+{
+ switch (kind) {
+ case KD_NumericAddition: return std::make_unique<NumericOperationNode>(NumericOperationNode::Addition);
+ case KD_NumericSubtraction: return std::make_unique<NumericOperationNode>(NumericOperationNode::Subtraction);
+ case KD_NumericMultiplication: return std::make_unique<NumericOperationNode>(NumericOperationNode::Multiplication);
+ case KD_NumericDivision: return std::make_unique<NumericOperationNode>(NumericOperationNode::Division);
+ case KD_NumericExpression: return std::make_unique<NumericExpressionNode>();
+ case KD_TextFormatting: return std::make_unique<TextFormatterNode>();
+ case KD_DocumentTemplateExpansion: return std::make_unique<DocumentTemplateExpansionNode>();
+ case KD_FormInput: return std::make_unique<FormInputNode>();
+ case KD_DatabaseRowsInput: return std::make_unique<DatabaseRowsInputNode>();
+
+ case InvalidKind: break;
+ }
+ return nullptr;
+}
+
+bool WorkflowNode::IsInstance(const WorkflowNode* node)
+{
+ return true;
+}
diff --git a/app/source/Cplt/Model/Workflow/fwd.hpp b/app/source/Cplt/Model/Workflow/fwd.hpp
new file mode 100644
index 0000000..ce5b6db
--- /dev/null
+++ b/app/source/Cplt/Model/Workflow/fwd.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <Cplt/Model/Workflow/Nodes/fwd.hpp>
+#include <Cplt/Model/Workflow/Values/fwd.hpp>
+
+// Evaluation.hpp
+class WorkflowEvaluationError;
+class WorkflowEvaluationContext;
+
+// SavedWorkflow.hpp
+class SavedWorkflowCache;
+class SavedWorkflow;
+
+// Value.hpp
+class BaseValue;
+class BaseObjectValue;
+
+// Workflow.hpp
+class WorkflowConnection;
+class WorkflowNode;
+class Workflow;
+class WorkflowAssetList;
diff --git a/app/source/Cplt/Model/fwd.hpp b/app/source/Cplt/Model/fwd.hpp
new file mode 100644
index 0000000..c7e44e6
--- /dev/null
+++ b/app/source/Cplt/Model/fwd.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <Cplt/Model/Template/fwd.hpp>
+#include <Cplt/Model/Workflow/fwd.hpp>
+
+// Database.hpp
+enum class TableKind;
+class SalesTable;
+class PurchasesTable;
+class DeliveryTable;
+class MainDatabase;
+
+// Assets.hpp
+struct SavedAsset;
+class Asset;
+enum class NameSelectionError;
+class AssetList;
+
+// Filter.hpp
+class TableRowsFilter;
+
+// GlobalStates.hpp
+class GlobalStates;
+
+// Items.hpp
+template <class T>
+class ItemList;
+template <class TSelf>
+class ItemBase;
+class ProductItem;
+class FactoryItem;
+class CustomerItem;
+
+// Project.hpp
+class Project;