Modules/Taskboard/public/TaskServerSync.hpp
Шурупов Илья Викторович f344c644e6 Kanban: move columns by dragging their headers
KanbanBoard: pressing a column header (anywhere but its options button) and
dragging it lifts the whole column the same way a card is lifted: the column
widget itself follows the pointer sideways above the board, and the remaining
columns slide apart to open the slot it will drop into. Releasing moves the
column and it settles into its slot with the usual geometry animation; Escape
puts it back. Hidden columns are skipped when choosing the slot, so they keep
their place among their neighbours. The board scrolls sideways near its edges
while a column is lifted, as it does for cards.

Card and column dragging share one mechanism: Lift captures the widget, the
grab offset, the pointer and the focus to restore, lift()/drop() reparent the
widget onto the board and back, and the board layout places whatever is
lifted. moveColumn(from, to) is the single place that reorders the column data,
views and options menus, and it reports the move through setOnColumnMoved.

Taskboard: KanbanColumnSettings now owns the column order next to the hidden
set and persists it as "order" in kanban-columns.json; statuses missing from a
saved order (e.g. added later) are appended in their default position. The
kanban model reads statuses through that order instead of the enum, so column
index and status no longer coincide by construction. Status-name parsing moves
next to statusName(): statusNamed() matches current names only (settings),
statusFromName() also accepts the retired server names (sync).
2026-10-26 21:02:54 +03:00

138 lines
5.5 KiB
C++

#pragma once
#include "BoardSnapshot.hpp"
#include "McpClient.hpp"
#include "TaskBoard.hpp"
#include "TaskStore.hpp"
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <string_view>
namespace tp::taskboard {
std::string colorToHex(const RGBA& color);
std::optional<RGBA> colorFromHex(const std::string& text);
const char* serverFieldName(TaskField field);
picojson::value fieldValue(const Task& task, TaskField field);
void setFieldValue(Task& task, TaskField field, const picojson::value& value);
std::string fieldLabel(TaskField field);
struct FieldConflict {
ui::NodeId task = 0;
TaskField field = TaskField::Title;
picojson::value mine;
picojson::value theirs;
};
struct HistorySpan {
std::optional<double> first;
std::optional<double> last;
};
// Keeps the board in step with the task server.
// `mSaved` is the board as the server last had it. The user's unsaved work is the difference between the
// live board and `mSaved`; reloads apply server changes only where the user has no unsaved change.
class TaskServerSync : public TaskStore, public SecretsStore {
public:
TaskServerSync(TaskBoard& board, McpClient& client);
bool load();
void processResponses();
void pollForChanges();
bool viewHistoryAt(std::optional<double> moment);
[[nodiscard]] std::optional<double> viewedMoment() const { return mViewedMoment; }
[[nodiscard]] bool isViewingHistory() const { return mViewedMoment.has_value(); }
[[nodiscard]] const HistorySpan& historySpan() const { return mHistorySpan; }
[[nodiscard]] const std::vector<double>& historyChanges() const { return mHistoryChanges; }
void save();
[[nodiscard]] BoardChanges unsavedChanges() const;
[[nodiscard]] bool hasUnsavedChanges() const { return !unsavedChanges().empty(); }
[[nodiscard]] bool isSaving() const { return mSaving; }
void startSession(ui::NodeId id) override;
void restartSession(ui::NodeId id, bool fresh) override;
void stopSession(ui::NodeId id) override;
void beginEditing(ui::NodeId id, TaskField field) override;
void endEditing() override;
void listSecrets(std::function<void(std::vector<SecretEntry>)> onResult) override;
void setSecret(
const std::string& key, const std::string& value, const std::string& description,
std::function<void(bool ok, std::string error)> onDone
) override;
void deleteSecret(const std::string& key, std::function<void(bool ok, std::string error)> onDone) override;
bool loadOptions();
[[nodiscard]] const AgentOptions& options() const { return mOptions; }
void resolveConflict(FieldConflict conflict, bool keepMine);
[[nodiscard]] const std::vector<FieldConflict>& conflicts() const { return mConflicts; }
[[nodiscard]] const std::string& lastError() const { return mLastError; }
void loadSubrouterInfo();
void setOnBoardReloaded(std::function<void()> callback) { mOnBoardReloaded = std::move(callback); }
void setOnHistoryChanged(std::function<void()> callback) { mOnHistoryChanged = std::move(callback); }
void setOnSaved(std::function<void()> callback) { mOnSaved = std::move(callback); }
void setOnError(std::function<void(const std::string&)> callback) { mOnError = std::move(callback); }
void setOnConflictsChanged(std::function<void()> callback) { mOnConflictsChanged = std::move(callback); }
void setOnSubrouterInfo(std::function<void(const SubrouterInfo&)> callback) {
mOnSubrouterInfo = std::move(callback);
}
private:
void applyBoard(const picojson::object& board);
void applyPalette(const picojson::object& board, const BoardSnapshot& local);
std::vector<TaskLink> applyTasks(const picojson::object& board, const BoardSnapshot& local);
void
applyLinks(const picojson::object& board, const std::vector<TaskLink>& parentLinks, const BoardSnapshot& local);
void mergeTask(ui::NodeId id, const Task& incoming, const NodeLayout& layout, const BoardSnapshot& local);
void connect(const TaskLink& link);
void disconnect(const TaskLink& link);
[[nodiscard]] picojson::object saveRequest(const BoardChanges& changes) const;
void finishSave(const McpToolResult& result, const BoardChanges& changes);
void adoptCreatedIds(const picojson::object& ids);
void addConflict(FieldConflict conflict);
bool dropConflict(ui::NodeId id, TaskField field);
void requestReload();
void requestBoard(std::optional<double> moment);
void boardArrived(const McpToolResult& result, unsigned generation);
void applyHistorySpan(const picojson::object& board);
void loadHistoryChanges();
void sessionCall(const std::string& tool, ui::NodeId id, picojson::object arguments);
void reportError(const std::string& error);
[[nodiscard]] bool isEditing(ui::NodeId id, TaskField field) const;
TaskBoard& mBoard;
McpClient& mClient;
BoardSnapshot mSaved;
std::string mRevision;
std::string mLastError;
std::vector<FieldConflict> mConflicts;
AgentOptions mOptions = offlineAgentOptions();
std::optional<std::pair<ui::NodeId, TaskField>> mEditing;
std::optional<double> mViewedMoment;
std::optional<std::optional<double>> mQueuedMoment;
HistorySpan mHistorySpan;
std::vector<double> mHistoryChanges;
unsigned mBoardGeneration = 0;
bool mHistoryInFlight = false;
bool mPollInFlight = false;
bool mSaving = false;
bool mSubrouterInFlight = false;
int mSubrouterPollTick = 0;
std::function<void()> mOnBoardReloaded;
std::function<void()> mOnSaved;
std::function<void()> mOnHistoryChanged;
std::function<void(const std::string&)> mOnError;
std::function<void()> mOnConflictsChanged;
std::function<void(const SubrouterInfo&)> mOnSubrouterInfo;
};
}