Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 163 additions & 71 deletions LoopEditor/editorhost.cpp

Large diffs are not rendered by default.

26 changes: 23 additions & 3 deletions LoopEditor/editorhost.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ class EditorHost final : public QObject
Q_PROPERTY(bool pageFidelityIsExact READ pageFidelityIsExact NOTIFY presentationChanged)
Q_PROPERTY(QString pageFidelityReason READ pageFidelityReason NOTIFY presentationChanged)
Q_PROPERTY(bool pageFidelityIsAuthoritative READ pageFidelityIsAuthoritative NOTIFY presentationChanged)
Q_PROPERTY(QString governedArtifactPath READ governedArtifactPath NOTIFY presentationChanged)
Q_PROPERTY(QVariantList revisionHistory READ revisionHistory NOTIFY presentationChanged)
Q_PROPERTY(QString revisionHistorySummary READ revisionHistorySummary NOTIFY presentationChanged)
Q_PROPERTY(bool historyUndoAvailable READ historyUndoAvailable NOTIFY presentationChanged)
Q_PROPERTY(bool historyRedoAvailable READ historyRedoAvailable NOTIFY presentationChanged)
Q_PROPERTY(bool searchPanelVisible READ searchPanelVisible NOTIFY presentationChanged)
Q_PROPERTY(bool fullscreenRequested READ fullscreenRequested NOTIFY presentationChanged)
Q_PROPERTY(int workspaceRequest READ workspaceRequest NOTIFY presentationChanged)
Expand Down Expand Up @@ -195,7 +200,7 @@ class EditorHost final : public QObject
};
Q_ENUM(LoopWorkspace)

explicit EditorHost(QObject* parent = nullptr);
explicit EditorHost(QObject* parent = nullptr, QString recipesDirectory = QString());
~EditorHost() override;

EditorHost(const EditorHost&) = delete;
Expand Down Expand Up @@ -307,6 +312,13 @@ class EditorHost final : public QObject

/// Recorded rollback points for the open document (read-only).
QVariantList fixRollbackPoints() const;
QString governedArtifactPath() const { return historyDocumentPath(); }
Q_INVOKABLE void openGovernedArtifact() { openFileUrl(QUrl::fromLocalFile(historyDocumentPath())); }
QVariantList revisionHistory() const { return m_revisionHistory; }
QString revisionHistorySummary() const;
bool historyUndoAvailable() const;
bool historyRedoAvailable() const;


/// Why the rollback affordance is enabled or unavailable, in the operator's words.
QString fixRollbackSummary() const;
Expand Down Expand Up @@ -503,6 +515,7 @@ class EditorHost final : public QObject
const DocumentViewSession* sessionForTest() const noexcept { return m_session.get(); }

signals:
void historyNavigationRequested(const QString& commandId, const QString& rollbackId, const QString& revision);
void passwordRequestChanged();
/// A completed drag was refused rather than committed (#206). Both values are the
/// target's stable names so a surface can explain the refusal.
Expand Down Expand Up @@ -580,8 +593,9 @@ class EditorHost final : public QObject
void clearFixReview();
pdf::PDFActionListExecutionResult fixRunResult() const;
QVariantMap fixPreviewFromResult() const;
QList<pdf::PDFRollbackPoint> documentRollbackPoints() const;
void refreshFixRollbackPointsFromHistory();
QString historyDocumentPath() const;
QString historyArtifactDigest() const;

/// Signature invalidation disclosure (#125). `applies` means the plan's
/// merged save policy invalidates signatures and the open document has some.
Expand Down Expand Up @@ -623,6 +637,13 @@ class EditorHost final : public QObject
QString retentionError;
};
QHash<QString, std::shared_ptr<RollbackJobOutcome>> m_rollbackOutcomes;
QVariantList m_revisionHistory;
QString m_historyError;
QString m_historyUndoId;
QString m_historyRedoId;
QString m_publishedHistoryPath;
QString m_publishedHistoryRevision;

struct PreflightProfileChoice
{
QString id;
Expand Down Expand Up @@ -676,7 +697,6 @@ class EditorHost final : public QObject
QString m_fixPlannedDocumentRevision;
QString m_fixPlannedPlanDigest;
QVariantList m_fixRollbackPoints;
QString m_fixRollbackSummary;
QString m_fixSignatureInvalidationAcknowledgedDigest;

/// One signature check at a time, fenced by the facade token it began with.
Expand Down
59 changes: 57 additions & 2 deletions LoopEditor/qml/ActionListPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ Pane {
GroupBox {
objectName: "fixRollbackGroup"
Layout.fillWidth: true
title: qsTr("Recorded revisions")
title: qsTr("Revision history")
Accessible.name: qsTr("Rollback to a recorded revision")

ColumnLayout {
Expand All @@ -326,6 +326,61 @@ Pane {
Accessible.name: qsTr("Rollback availability")
}

Label {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: root.host ? root.host.revisionHistorySummary : ""
Accessible.name: qsTr("Governed revision history summary")
}
RowLayout {
Button {
objectName: "historyUndoButton"
text: qsTr("Undo")
enabled: root.host && root.host.historyUndoAvailable
onClicked: root.host.invokeCommand("actionUndo")
Accessible.name: qsTr("Undo by publishing a forward rollback")
}
Button {
objectName: "historyRedoButton"
text: qsTr("Redo")
enabled: root.host && root.host.historyRedoAvailable
onClicked: root.host.invokeCommand("actionRedo")
Accessible.name: qsTr("Redo by publishing a forward rollback")
}
Button {
text: qsTr("Open published artifact")
enabled: root.host && root.host.hasDocument
onClicked: root.host.openGovernedArtifact()
Accessible.description: root.host ? root.host.governedArtifactPath : ""
}
}
Repeater {
model: root.host ? root.host.revisionHistory : []
delegate: ColumnLayout {
required property var modelData
Layout.fillWidth: true
Label {
Layout.fillWidth: true
wrapMode: Text.Wrap
text: qsTr("Event %1 · %2 · revision %3%4 · approval %5 by %6")
.arg(modelData.sequence).arg(modelData.status)
.arg(modelData.output ? modelData.output.sha256 : modelData.documentRevisionDigest)
.arg(modelData.current ? qsTr(" (current)") : "")
.arg(modelData.approval.decision || qsTr("none"))
.arg(modelData.approval.actorId || qsTr("none"))
Accessible.name: text
}
TextArea {
Layout.fillWidth: true
readOnly: true
selectByMouse: true
wrapMode: TextEdit.Wrap
visible: modelData.receiptText !== "{}"
text: modelData.receiptText
Accessible.name: qsTr("Revision approval and publication receipt")
}
}
}
Repeater {
model: root.host ? root.host.fixRollbackPoints : []

Expand All @@ -346,7 +401,7 @@ Pane {

Button {
text: qsTr("Return to this revision")
enabled: root.host !== null && root.host.fixRollbackAvailable
enabled: root.host !== null && root.host.fixRollbackAvailable && modelData.canReturn
onClicked: {
root.pendingRollbackId = modelData.rollbackId
rollbackConfirm.open()
Expand Down
30 changes: 30 additions & 0 deletions LoopEditor/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,36 @@ ApplicationWindow {
saveAsDialog: saveAsDialog
}

Dialog {
id: historyConfirm
objectName: "historyNavigationConfirmDialog"
parent: Overlay.overlay
anchors.centerIn: parent
modal: true
title: commandId === "actionUndo" ? qsTr("Undo published correction") : qsTr("Redo published correction")
standardButtons: Dialog.Ok | Dialog.Cancel
property string commandId: ""
property string rollbackId: ""
property string revision: ""
Label {
width: 360
wrapMode: Text.Wrap
text: qsTr("Publish a new rollback artifact after revalidation? Existing revisions, approvals and receipts are retained.")
Accessible.name: text
}
onAccepted: if (window.host) window.host.invokeCommand(commandId, {rollbackId: rollbackId, revision: revision})
}

Connections {
target: window.host
function onHistoryNavigationRequested(commandId, rollbackId, revision) {
historyConfirm.commandId = commandId
historyConfirm.rollbackId = rollbackId
historyConfirm.revision = revision
historyConfirm.open()
}
}

FileDialog {
id: openDialog
title: qsTr("Open PDF")
Expand Down
13 changes: 11 additions & 2 deletions LoopLibCore/sources/pdfoperationhistorystore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1138,8 +1138,17 @@ PDFOperationResult PDFOperationHistoryStore::resolveRollbackTarget(const PDFRoll
query.prepare(QStringLiteral("SELECT a.sha256, a.size_bytes, a.media_type, a.logical_name, a.storage_token FROM history_events h JOIN artifacts a ON a.sha256 = h.output_sha256 JOIN rollback_points p ON p.audit_event_id = h.entry_id WHERE h.execution_id = ? AND h.status = 'accepted' AND h.output_sha256 = ? AND p.artifact_evicted = 0 ORDER BY h.sequence DESC LIMIT 1"));
query.addBindValue(request.targetExecutionId.toString(QUuid::WithoutBraces));
query.addBindValue(request.targetArtifactSha256.toLower());
if (!query.exec() || !query.next())
return PDFOperationResult(QStringLiteral("Rollback target is not an accepted immutable artifact."));
if (!query.exec())
return PDFOperationResult(queryError(query));
if (!query.next())
{
query.prepare(QStringLiteral("SELECT a.sha256, a.size_bytes, a.media_type, a.logical_name, a.storage_token FROM artifacts a JOIN rollback_points p ON p.document_revision_digest = a.sha256 JOIN executions e ON e.source_sha256 = a.sha256 JOIN history_events h ON h.execution_id = e.execution_id WHERE e.execution_id = ? AND h.status = 'accepted' AND h.output_sha256 = ? AND a.sha256 = ? AND p.is_original_input = 1 AND p.artifact_evicted = 0 AND a.artifact_evicted = 0 LIMIT 1"));
query.addBindValue(request.targetExecutionId.toString(QUuid::WithoutBraces));
query.addBindValue(request.currentArtifactSha256.toLower());
query.addBindValue(request.targetArtifactSha256.toLower());
if (!query.exec() || !query.next())
return PDFOperationResult(QStringLiteral("Rollback target is neither an accepted immutable artifact nor the retained original input of the accepted revision being undone."));
}
targetArtifact->sha256 = query.value(0).toString();
targetArtifact->size = query.value(1).toLongLong();
targetArtifact->mediaType = query.value(2).toString();
Expand Down
3 changes: 2 additions & 1 deletion LoopLibCore/sources/pdfoperationhistorystore.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ class LOOPLIBCORESHARED_EXPORT PDFOperationHistoryStore
const QString& destinationPath,
qint64* sequence = nullptr);

/// Resolves a rollback target only when it is the output of an accepted event.
/// Resolves an accepted output, or its retained original input when the
/// request binds that input to the accepted execution being undone.
PDFOperationResult resolveRollbackTarget(const PDFRollbackRequest& request,
PDFArtifactIdentity* targetArtifact) const;

Expand Down
11 changes: 7 additions & 4 deletions LoopLibInteraction/sources/actionlistcatalog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "actionlistcatalog.h"

#include <algorithm>
#include <utility>

#include <QDir>
#include <QFile>
Expand Down Expand Up @@ -61,15 +62,17 @@ const QList<BuiltInRecipe>& builtInRecipes()

} // namespace

ActionListCatalog::ActionListCatalog(QObject* parent) :
QObject(parent)
ActionListCatalog::ActionListCatalog(QObject* parent, QString recipesDirectory) :
QObject(parent),
m_recipesDirectory(std::move(recipesDirectory))
{
}

QString ActionListCatalog::recipesDirectory() const
{
return QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation))
.filePath(QStringLiteral("recipes"));
return m_recipesDirectory.isEmpty()
? QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)).filePath(QStringLiteral("recipes"))
: m_recipesDirectory;
}

bool ActionListCatalog::ensureRecipesDirectory(QString* error) const
Expand Down
3 changes: 2 additions & 1 deletion LoopLibInteraction/sources/actionlistcatalog.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class ActionListCatalog final : public QObject
Q_OBJECT

public:
explicit ActionListCatalog(QObject* parent = nullptr);
explicit ActionListCatalog(QObject* parent = nullptr, QString recipesDirectory = QString());

const QList<ActionListRecipeEntry>& recipes() const noexcept { return m_recipes; }
QString recipesDirectory() const;
Expand All @@ -72,6 +72,7 @@ class ActionListCatalog final : public QObject
bool loadRecipeFile(const QString& sourcePath, const QJsonObject& bindings, ActionListRecipeEntry* entry);

QList<ActionListRecipeEntry> m_recipes;
QString m_recipesDirectory;
};

} // namespace pdfinteraction
Expand Down
Loading
Loading