24namespace exportKeys = ui::payload::keys::exportSelection;
28inline constexpr auto kPlot =
"plot";
29inline constexpr auto kTab =
"tab";
30inline constexpr auto kCsv =
"CSV";
31inline constexpr auto kXlsx =
"XLSX";
32inline constexpr auto kPng =
"PNG";
33inline constexpr auto kJpg =
"JPG";
34inline constexpr int kCreateMode = 0;
35inline constexpr int kProgressMode = 1;
37QString qstr(
const char* value) {
38 return QString::fromLatin1(value);
41int indexOfId(
const QVariantList& rows,
const QString&
id,
42 const QString& idKey = QStringLiteral(
"id")) {
46 for (
int i = 0; i < rows.size(); ++i) {
47 if (rows.at(i).toMap().value(idKey).toString() ==
id) {
54QVariantMap rowById(
const QVariantList& rows,
const QString&
id,
55 const QString& idKey = QStringLiteral(
"id")) {
56 const int index = indexOfId(rows,
id.trimmed(), idKey);
57 return index >= 0 ? rows.at(index).toMap() : QVariantMap{};
60QString nonEmptyString(
const QVariantMap& map,
const QString& key,
61 const QString& defaultValue = {}) {
62 const QString value = map.value(key).toString();
63 return value.isEmpty() ? defaultValue : value;
72 if (store_ == store && commands_ == commands && selectors_ == selectors) {
75 bindWorkspaceRoles(store, commands, selectors);
76 configureExportLogSink();
81 if (exportWorkflow_ == value) {
85 configureExportLogSink();
90 if (actions_ == value) {
98 if (fileSystemBrowser_ == value) {
101 fileSystemBrowser_ = value;
102 if (targetDirectory_.isEmpty()) {
103 targetDirectory_ = defaultTargetDirectory();
104 appliedDefaultTargetDirectory_ = targetDirectory_;
110 if (settings_ == value) {
114 if (targetDirectory_.isEmpty() ||
115 targetDirectory_ == appliedDefaultTargetDirectory_) {
116 targetDirectory_ = defaultTargetDirectory();
117 appliedDefaultTargetDirectory_ = targetDirectory_;
119 packageFormatIndex_ = settings_ ? settings_->exportArchiveFormat() : 0;
127 disconnect(store_,
nullptr,
this,
nullptr);
130 commands_ = commands;
131 selectors_ = selectors;
138void ExportViewModel::bindActions(
Actions* value) {
140 disconnect(actions_,
nullptr,
this,
nullptr);
149void ExportViewModel::bindSettings(
Settings* value) {
151 disconnect(settings_,
nullptr,
this,
nullptr);
158 if (targetDirectory_.isEmpty() ||
159 targetDirectory_ == appliedDefaultTargetDirectory_) {
160 targetDirectory_ = defaultTargetDirectory();
161 appliedDefaultTargetDirectory_ = targetDirectory_;
166 packageFormatIndex_ = settings_ ? settings_->exportArchiveFormat() : 0;
170 &ExportViewModel::emitChanged);
174 if (exportWorkflow_) {
175 disconnect(exportWorkflow_,
nullptr,
this,
nullptr);
177 exportWorkflow_ = value;
178 if (exportWorkflow_) {
180 &ExportViewModel::emitChanged);
184void ExportViewModel::configureExportLogSink() {
185 if (!exportWorkflow_) {
189 exportWorkflow_->setExportLogSink({});
192 exportWorkflow_->setExportLogSink([
this](
const auto& log) {
194 commands_->saveExportLog(log);
199bool ExportViewModel::isAnnualMode()
const {
200 return addMode_ == exportKeys::kAnnual;
204 return selectors_ ? selectors_->annualRows() : QVariantList();
208 return selectors_ ? selectors_->analysisRows() : QVariantList();
211QVariantMap ExportViewModel::annualRowById(
const QString&
id)
const {
215QVariantMap ExportViewModel::analysisRowById(
const QString&
id)
const {
219QString ExportViewModel::selectedAddObjectId()
const {
220 return isAnnualMode() ? selectedAddAnnualId_ : selectedAddAnalysisId_;
223void ExportViewModel::ensureAddSelection() {
224 const QVariantList rows =
addRows();
225 if (rows.isEmpty()) {
226 if (isAnnualMode()) {
227 selectedAddAnnualId_.clear();
229 selectedAddAnalysisId_.clear();
233 if (indexOfId(rows, selectedAddObjectId()) >= 0) {
236 const QString firstId =
237 rows.first().toMap().value(QStringLiteral(
"id")).toString();
238 if (isAnnualMode()) {
239 selectedAddAnnualId_ = firstId;
241 selectedAddAnalysisId_ = firstId;
246 if (targetDirectory_ == value) {
249 targetDirectory_ = value;
254 const int next = std::clamp(value, 0, 1);
255 if (packageFormatIndex_ == next) {
258 packageFormatIndex_ = next;
263 const QString normalized = value.trimmed().toLower() == exportKeys::kAnalysis
264 ? exportKeys::kAnalysis
265 : exportKeys::kAnnual;
266 if (addMode_ == normalized) {
269 addMode_ = normalized;
270 ensureAddSelection();
279 return isAnnualMode() ? QStringLiteral(
"display") : QStringLiteral(
"name");
283 return indexOfId(
addRows(), selectedAddObjectId());
287 return !selectedAddObjectId().isEmpty();
291 if (targetDirectory_.isEmpty()) {
292 targetDirectory_ = defaultTargetDirectory();
293 appliedDefaultTargetDirectory_ = targetDirectory_;
296 packageFormatIndex_ = settings_->exportArchiveFormat();
298 ensureAddSelection();
299 refreshEntriesFromWorkspace();
304 targetDirectory_ = defaultTargetDirectory();
305 appliedDefaultTargetDirectory_ = targetDirectory_;
306 packageFormatIndex_ = settings_ ? settings_->exportArchiveFormat() : 0;
307 exportEntries_.clear();
308 ensureAddSelection();
309 if (exportWorkflow_) {
310 exportWorkflow_->clearActiveExportLog();
315QString ExportViewModel::defaultTargetDirectory()
const {
319 if (fileSystemBrowser_) {
320 return fileSystemBrowser_->
appDir();
325void ExportViewModel::refreshEntriesFromWorkspace() {
326 QVariantList refreshed;
327 refreshed.reserve(exportEntries_.size());
328 for (
const QVariant& value : exportEntries_) {
329 QVariantMap entry = value.toMap();
330 if (entry.value(QStringLiteral(
"kind")).toString() == exportKeys::kAnnual) {
331 const QString
id = entry.value(QStringLiteral(
"objectId")).toString();
332 const QVariantMap annual = annualRowById(
id);
334 QStringLiteral(
"objectName"),
335 nonEmptyString(annual, QStringLiteral(
"name"),
336 entry.value(QStringLiteral(
"objectName")).toString()));
337 entry.insert(QStringLiteral(
"analyses"),
339 id, entry.value(QStringLiteral(
"analyses")).toList()));
340 refreshed.push_back(entry);
343 const QString
id = entry.value(QStringLiteral(
"objectId")).toString();
344 const QVariantMap analysis = analysisRowById(
id);
345 refreshed.push_back(createAnalysisEntry(
347 nonEmptyString(analysis, QStringLiteral(
"name"),
348 entry.value(QStringLiteral(
"objectName")).toString()),
349 nonEmptyString(analysis, QStringLiteral(
"type"),
350 entry.value(QStringLiteral(
"analysisType")).toString()),
351 entry.value(QStringLiteral(
"exportType")).toString()));
353 exportEntries_ = refreshed;
357ExportViewModel::createAnnualEntry(
const QString&
id,
const QString& name,
358 const QVariantList& analyses)
const {
359 return {{QStringLiteral(
"kind"), exportKeys::kAnnual},
360 {QStringLiteral(
"isAnnual"),
true},
361 {QStringLiteral(
"objectId"),
id},
362 {QStringLiteral(
"objectName"), name},
363 {QStringLiteral(
"annualIndex"), indexOfId(
annualRows(),
id)},
364 {QStringLiteral(
"collapsed"),
false},
365 {QStringLiteral(
"analyses"), analyses}};
369ExportViewModel::createAnalysisEntry(
const QString&
id,
const QString& name,
371 const QString& exportType)
const {
372 const QString normalizedType = type.trimmed().isEmpty()
373 ? analysisTypeById(
id)
374 : type.trimmed().toLower();
375 const QVariantList
options = exportOptionsForAnalysisType(normalizedType);
376 const QString selectedExportType =
377 normalizedExportType(exportType, normalizedType);
379 {QStringLiteral(
"kind"), exportKeys::kAnalysis},
380 {QStringLiteral(
"isAnnual"),
false},
381 {QStringLiteral(
"objectId"),
id},
382 {QStringLiteral(
"objectName"), name},
383 {QStringLiteral(
"analysisIndex"), indexOfId(
analysisRows(),
id)},
384 {QStringLiteral(
"analysisType"), normalizedType},
385 {QStringLiteral(
"exportType"), selectedExportType},
386 {QStringLiteral(
"exportTypeOptions"),
options},
387 {QStringLiteral(
"exportTypeIndex"),
options.indexOf(selectedExportType)}};
391ExportViewModel::analysesForAnnual(
const QString& annualId,
392 const QVariantList& currentAnalyses)
const {
393 const QVariantMap annual = annualRowById(annualId);
394 const QVariantList ids =
395 annual.value(QStringLiteral(
"analysisIds")).toList().isEmpty()
396 ? annual.value(QStringLiteral(
"assignedAnalysisIds")).toList()
397 : annual.value(QStringLiteral(
"analysisIds")).toList();
398 QVariantMap currentTypeById;
399 for (
const QVariant& value : currentAnalyses) {
400 const QVariantMap row = value.toMap();
401 const QString
id = row.value(QStringLiteral(
"objectId")).toString();
403 currentTypeById.insert(
id, row.value(QStringLiteral(
"exportType")));
407 for (
const QVariant& idValue : ids) {
408 const QString analysisId = idValue.toString();
409 const QVariantMap row = analysisRowById(analysisId);
410 out.push_back(createAnalysisEntry(
411 analysisId, nonEmptyString(row, QStringLiteral(
"name")),
412 nonEmptyString(row, QStringLiteral(
"type"),
413 analysisTypeById(analysisId)),
414 currentTypeById.value(analysisId).toString()));
420ExportViewModel::exportOptionsForAnalysisType(
const QString& type)
const {
421 return type.trimmed().toLower() == qstr(kPlot)
422 ? QVariantList{qstr(kPng), qstr(kJpg)}
423 : QVariantList{qstr(kCsv), qstr(kXlsx)};
426QString ExportViewModel::normalizedExportType(
const QString& exportType,
427 const QString& type)
const {
428 const QString upper = exportType.trimmed().toUpper();
429 const QVariantList
options = exportOptionsForAnalysisType(type);
430 for (
const QVariant& option : options) {
431 if (option.toString() == upper) {
435 return defaultExportType(type);
438QString ExportViewModel::defaultExportType(
const QString& type)
const {
439 return exportOptionsForAnalysisType(type).value(0).toString();
442QString ExportViewModel::analysisTypeById(
const QString&
id)
const {
443 const QVariantMap row = analysisRowById(
id);
444 return nonEmptyString(row, QStringLiteral(
"type"), qstr(kTab)).toLower();
448 const QVariantList rows =
addRows();
449 const QVariantMap row = index >= 0 && index < rows.size()
450 ? rows.at(index).toMap()
452 const QString
id = row.value(QStringLiteral(
"id")).toString();
453 if (isAnnualMode()) {
454 selectedAddAnnualId_ = id;
456 selectedAddAnalysisId_ = id;
462 const QString objectId = selectedAddObjectId();
463 if (objectId.isEmpty()) {
466 if (isAnnualMode()) {
467 const QVariantMap annual = annualRowById(objectId);
468 exportEntries_.push_back(createAnnualEntry(
469 objectId, nonEmptyString(annual, QStringLiteral(
"name")),
470 analysesForAnnual(objectId, {})));
472 const QVariantMap analysis = analysisRowById(objectId);
473 const QString type = nonEmptyString(analysis, QStringLiteral(
"type"),
474 analysisTypeById(objectId));
475 exportEntries_.push_back(createAnalysisEntry(
476 objectId, nonEmptyString(analysis, QStringLiteral(
"name")), type,
477 nonEmptyString(analysis, QStringLiteral(
"exportFormat"))));
483 if (index < 0 || index >= exportEntries_.size()) {
486 exportEntries_.removeAt(index);
492 if (entryIndex < 0 || entryIndex >= exportEntries_.size()) {
496 if (annualIndex < 0 || annualIndex >= rows.size()) {
499 QVariantMap entry = exportEntries_.at(entryIndex).toMap();
500 const QVariantList existingAnalyses =
501 entry.value(QStringLiteral(
"analyses")).toList();
502 const QVariantMap annual = rows.at(annualIndex).toMap();
503 const QString
id = annual.value(QStringLiteral(
"id")).toString();
504 entry.insert(QStringLiteral(
"objectId"),
id);
505 entry.insert(QStringLiteral(
"objectName"),
506 nonEmptyString(annual, QStringLiteral(
"name")));
507 entry.insert(QStringLiteral(
"annualIndex"), annualIndex);
508 entry.insert(QStringLiteral(
"analyses"),
509 analysesForAnnual(
id, existingAnalyses));
510 exportEntries_[entryIndex] = entry;
515 if (entryIndex < 0 || entryIndex >= exportEntries_.size()) {
518 QVariantMap entry = exportEntries_.at(entryIndex).toMap();
519 entry.insert(QStringLiteral(
"collapsed"), collapsed);
520 exportEntries_[entryIndex] = entry;
526 if (entryIndex < 0 || entryIndex >= exportEntries_.size()) {
530 if (analysisIndex < 0 || analysisIndex >= rows.size()) {
533 const QVariantMap row = rows.at(analysisIndex).toMap();
534 const QString
id = row.value(QStringLiteral(
"id")).toString();
536 nonEmptyString(row, QStringLiteral(
"type"), qstr(kTab)).toLower();
537 exportEntries_[entryIndex] = createAnalysisEntry(
538 id, nonEmptyString(row, QStringLiteral(
"name")), type, {});
543 int entryIndex,
const QString& exportType) {
544 if (entryIndex < 0 || entryIndex >= exportEntries_.size()) {
547 const QVariantMap current = exportEntries_.at(entryIndex).toMap();
548 exportEntries_[entryIndex] = createAnalysisEntry(
549 current.value(QStringLiteral(
"objectId")).toString(),
550 current.value(QStringLiteral(
"objectName")).toString(),
551 current.value(QStringLiteral(
"analysisType")).toString(), exportType);
556 int entryIndex,
int analysisIndex,
const QString& exportType) {
557 if (entryIndex < 0 || entryIndex >= exportEntries_.size()) {
560 QVariantMap entry = exportEntries_.at(entryIndex).toMap();
561 QVariantList analyses = entry.value(QStringLiteral(
"analyses")).toList();
562 if (analysisIndex < 0 || analysisIndex >= analyses.size()) {
565 const QVariantMap analysis = analyses.at(analysisIndex).toMap();
566 analyses[analysisIndex] = createAnalysisEntry(
567 analysis.value(QStringLiteral(
"objectId")).toString(),
568 analysis.value(QStringLiteral(
"objectName")).toString(),
569 analysis.value(QStringLiteral(
"analysisType")).toString(), exportType);
570 entry.insert(QStringLiteral(
"analyses"), analyses);
571 exportEntries_[entryIndex] = entry;
575QVariantList ExportViewModel::exportItems()
const {
577 for (
const QVariant& value : exportEntries_) {
578 const QVariantMap entry = value.toMap();
579 const QString kind = entry.value(QStringLiteral(
"kind")).toString();
580 if (kind == exportKeys::kAnnual) {
581 const QString annualId =
582 entry.value(QStringLiteral(
"objectId")).toString();
584 QVariantMap{{exportKeys::kObjectType, exportKeys::kAnnual},
585 {exportKeys::kObjectId, annualId},
586 {exportKeys::kObjectName,
587 entry.value(QStringLiteral(
"objectName")).toString()},
588 {exportKeys::kExportType, QString()}});
589 for (
const QVariant& analysisValue :
590 entry.value(QStringLiteral(
"analyses")).toList()) {
591 const QVariantMap analysis = analysisValue.toMap();
592 out.push_back(QVariantMap{
593 {exportKeys::kObjectType, exportKeys::kAnalysis},
594 {exportKeys::kAnnualId, annualId},
595 {exportKeys::kObjectId,
596 analysis.value(QStringLiteral(
"objectId")).toString()},
597 {exportKeys::kObjectName,
598 analysis.value(QStringLiteral(
"objectName")).toString()},
599 {exportKeys::kExportType,
600 analysis.value(QStringLiteral(
"exportType")).toString()}});
605 QVariantMap{{exportKeys::kObjectType, exportKeys::kAnalysis},
606 {exportKeys::kAnnualId, QString()},
607 {exportKeys::kObjectId,
608 entry.value(QStringLiteral(
"objectId")).toString()},
609 {exportKeys::kObjectName,
610 entry.value(QStringLiteral(
"objectName")).toString()},
611 {exportKeys::kExportType,
612 entry.value(QStringLiteral(
"exportType")).toString()}});
618 return exportWorkflow_ && workflowMode() == kCreateMode &&
619 !targetDirectory_.isEmpty() && !exportItems().isEmpty();
623 return exportWorkflow_ && workflowMode() == kCreateMode;
627 return exportWorkflow_ && workflowMode() == kProgressMode;
635 return exportWorkflow_ && exportWorkflow_->isPaused();
639 return isPaused() ? tr(
"Resume") : tr(
"Pause");
643 return exportWorkflow_ && workflowMode() != kCreateMode
644 ? exportWorkflow_->progress()
649 if (!exportWorkflow_) {
652 if (!exportWorkflow_->error().isEmpty()) {
653 return exportWorkflow_->error();
655 if (!exportWorkflow_->phase().isEmpty()) {
656 return exportWorkflow_->phase();
662 return exportWorkflow_ && !exportWorkflow_->error().isEmpty();
666 return selectors_ ? selectors_->exportLogRows() : QVariantList{};
671 actions_->browseExportDirectory();
680 "ExportViewModel::startExport",
"Export start submitted",
683 std::to_string(std::max(1,
static_cast<int>(exportItems().size())))}});
684 const bool includeFormulas =
685 settings_ ? settings_->exportIncludeFormulas() :
true;
686 const int itemCount =
static_cast<int>(exportItems().size());
687 exportWorkflow_->exportDataWithPayload(0, targetDirectory_, includeFormulas,
688 defaultLocale(), payload(),
689 std::max(1, itemCount));
693 if (exportWorkflow_) {
695 "Export cancel submitted");
696 exportWorkflow_->cancelExport();
701 if (exportWorkflow_) {
702 exportWorkflow_->pauseExport();
707 if (exportWorkflow_) {
708 exportWorkflow_->resumeExport();
713 if (!selectors_ || !fileSystemBrowser_) {
717 const QString targetPath = selectors_->exportLogTargetPath(logId);
718 fileSystemBrowser_->openLocation(targetPath);
726 "ExportViewModel::deleteExportLog",
"Export log delete submitted",
728 const QString trimmed = logId.trimmed();
729 if (!trimmed.isEmpty()) {
730 commands_->deleteExportLog(trimmed);
734QVariantMap ExportViewModel::payload()
const {
735 return {{exportKeys::kTargetDirectory, targetDirectory_},
736 {exportKeys::kPackageFormatIndex, packageFormatIndex_},
737 {exportKeys::kItems, exportItems()}};
740QString ExportViewModel::defaultLocale()
const {
741 return QLocale::system().name().replace(QLatin1Char(
'_'), QLatin1Char(
'-'));
744int ExportViewModel::workflowMode()
const {
745 return exportWorkflow_ ? exportWorkflow_->currentMode() : kCreateMode;
748void ExportViewModel::emitChanged() {
Declares QAction wrappers exposed to the UI layer and QML.
Declares the QML API for the Export view.
Declares the asynchronous export workflow used by ExportViewModel.
Declares the UI helper that exposes a minimal filesystem view to QML.
Declarations for the UI PayloadKeys component.
Declares the centralized UI settings view model exposed to QML.
Declarations for the UI Trace component.
Declares read-only workspace selectors for QML payloads.
Declares the UI workspace snapshot store.
Owns the main window actions and exposes them to QML.
void exportDirectorySelected(const QString &path)
Emitted after an export directory was chosen by the user.
Q_INVOKABLE void clearForm()
Clears the export form back to defaults.
QVariantList analysisRows
Q_INVOKABLE void browseDirectory()
Opens the export directory picker through shell actions.
Q_INVOKABLE void cancelExport()
Cancels the active export workflow.
void changed()
Emitted when export view model state changed.
void setActions(Actions *value)
Binds shell actions such as the export directory picker.
Q_INVOKABLE void updateStandaloneAnalysisAtIndex(int entryIndex, int analysisIndex)
Replaces a standalone analysis package entry.
Q_INVOKABLE void refreshFromWorkspace()
Refreshes export form rows from the current workspace state.
Q_INVOKABLE void deleteExportLog(const QString &logId)
Deletes an export log from the workspace.
void setPackageFormatIndex(int value)
Updates the selected package format index.
Q_INVOKABLE void updateAnnualAnalysisExportType(int entryIndex, int analysisIndex, const QString &exportType)
Updates export type for an analysis inside an annual entry.
Q_INVOKABLE void openExportLogLocation(const QString &logId)
Opens the target folder for an export log.
Q_INVOKABLE void pauseExport()
Pauses the active export workflow.
Q_INVOKABLE void updateStandaloneAnalysisExportType(int entryIndex, const QString &exportType)
Updates export type for a standalone analysis entry.
void setWorkspaceRoles(WorkspaceStore *store, WorkspaceCommands *commands, WorkspaceSelectors *selectors)
Binds workspace roles used for catalog rows and export logs.
void setTargetDirectory(const QString &value)
Updates the selected export target directory.
Q_INVOKABLE void removeEntry(int index)
Removes an export package entry.
Q_INVOKABLE void addSelectedEntry()
Adds the selected annual or analysis to the export package.
void setSettings(Settings *value)
Binds export settings used by defaults and package options.
Q_INVOKABLE void updateAnnualCollapsed(int entryIndex, bool collapsed)
Updates collapsed state for an annual package entry.
void setFileSystemBrowser(FileSystemBrowser *value)
Binds filesystem defaults used by the export form.
void setExportWorkflow(ExportWorkflow *value)
Binds the workflow used to execute exports.
Q_INVOKABLE void updateAnnualEntryAtIndex(int entryIndex, int annualIndex)
Replaces an annual package entry with another annual row.
Q_INVOKABLE void resumeExport()
Resumes the active export workflow.
ExportViewModel(QObject *parent=nullptr)
Creates the export view model.
Q_INVOKABLE void startExport()
Starts the export workflow with the current package configuration.
Q_INVOKABLE void selectAddRow(int index)
Selects a row in the current add combobox.
void setAddMode(const QString &value)
Switches the add combobox between annual and analysis mode.
Coordinates asynchronous exports from a read-only state snapshot.
void stateChanged()
Emitted when export workflow state changed.
Exposes directory listing and application-directory lookup to QML.
Q_INVOKABLE QString appDir() const
Return the current application directory path.
Stores and exposes globally persisted UI settings for settings-related views.
QString exportDefaultDirectory
void exportDefaultDirectoryChanged()
void exportArchiveFormatChanged()
void exportIncludeFormulasChanged()
Maps UI intent to core workspace writer commands.
Projects workspace store data into QML-facing read models.
Owns the current UI-side workspace snapshot and revision lifecycle.
void dataRevisionChanged()
void traceViewModel(const char *origin, std::string message, core::errors::ErrorContext context={})
Emits a view model trace event.
QVariantList options(const QString &neutral, const QString &unverified, const QString &verified, const QString &completed)
Builds transaction status options.
Declares UI-side workspace command submission.