initial commit after split

This commit is contained in:
2026-06-24 01:43:27 +02:00
parent 2406e70597
commit 7f25a3ccdf
125 changed files with 25551 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
import { useState } from "react";
import type { ApiSettings } from "../../../types";
import Button from "../../../components/Button";
import ConfirmDialog from "../../../components/ConfirmDialog";
import DismissibleAlert from "../../../components/DismissibleAlert";
import {
forkCampaignVersion,
lockCampaignVersionPermanently,
unlockCampaignVersionUserLock,
unlockCampaignVersionValidation,
type CampaignVersionDetail,
type CampaignVersionListItem,
} from "../../../api/campaigns";
import {
canUnlockValidationVersion,
formatDateTime,
isFinalLockedVersion,
isHistoricalCampaignVersion,
isPermanentUserLockedVersion,
isTemporaryUserLockedVersion,
} from "../utils/campaignView";
type LockedVersionNoticeProps = {
settings: ApiSettings;
campaignId: string;
version: CampaignVersionDetail | CampaignVersionListItem | null;
currentVersionId?: string | null;
reload: () => Promise<void>;
message?: string;
};
type ConfirmAction = "unlock-validation" | "unlock-user" | "permanent" | null;
export default function LockedVersionNotice({ settings, campaignId, version, currentVersionId, reload, message }: LockedVersionNoticeProps) {
const [busy, setBusy] = useState(false);
const [localError, setLocalError] = useState("");
const [localMessage, setLocalMessage] = useState("");
const [confirmAction, setConfirmAction] = useState<ConfirmAction>(null);
const historicalVersion = isHistoricalCampaignVersion(version, currentVersionId);
const validationLock = !historicalVersion && canUnlockValidationVersion(version);
const temporaryUserLock = !historicalVersion && isTemporaryUserLockedVersion(version);
const permanentUserLock = isPermanentUserLockedVersion(version);
const finalLock = isFinalLockedVersion(version);
const canCreateEditableCopy = !historicalVersion && (permanentUserLock || finalLock);
const presentation = lockPresentation(version, {
historicalVersion,
validationLock,
temporaryUserLock,
permanentUserLock,
finalLock,
});
async function runAction(action: Exclude<ConfirmAction, null>) {
if (!version || busy) return;
setBusy(true);
setLocalError("");
setLocalMessage("");
try {
if (action === "unlock-validation") {
await unlockCampaignVersionValidation(settings, campaignId, version.id);
setLocalMessage("Validation lock removed. Validation, build and generated queue state were invalidated.");
} else if (action === "unlock-user") {
await unlockCampaignVersionUserLock(settings, campaignId, version.id);
setLocalMessage("Temporary user lock removed. The version is editable again.");
} else {
await lockCampaignVersionPermanently(settings, campaignId, version.id);
setLocalMessage("Version permanently locked. Future changes require an editable copy.");
}
await reload();
} catch (err) {
setLocalError(err instanceof Error ? err.message : String(err));
} finally {
setBusy(false);
}
}
async function createEditableCopy() {
if (!version || busy) return;
setBusy(true);
setLocalError("");
setLocalMessage("");
try {
const result = await forkCampaignVersion(settings, campaignId, version.id, {
current_flow: "manual",
current_step: version.current_step ?? null,
});
setLocalMessage(`Created editable version #${result.version.version_number}.`);
await reload();
} catch (err) {
setLocalError(err instanceof Error ? err.message : String(err));
} finally {
setBusy(false);
}
}
return (
<DismissibleAlert tone="info" dismissible={false} className={`locked-version-notice lock-kind-${presentation.kind}`}>
<div className="locked-version-copy">
<strong>{presentation.title}</strong>{" "}
<span>{presentation.description}</span>
{message && <span className="locked-version-context"> {message}</span>}
{presentation.info && <span className="locked-version-reason"> {presentation.info}</span>}
{localMessage && <span className="locked-version-feedback"> {localMessage}</span>}
{localError && <span className="locked-version-error"> {localError}</span>}
</div>
<div className="button-row compact-actions locked-version-actions">
{validationLock && (
<Button onClick={() => setConfirmAction("unlock-validation")} disabled={!version || busy}>
{busy ? "Working…" : "Unlock validation"}
</Button>
)}
{temporaryUserLock && (
<>
<Button onClick={() => setConfirmAction("unlock-user")} disabled={!version || busy}>
{busy ? "Working…" : "Unlock"}
</Button>
<Button variant="danger" onClick={() => setConfirmAction("permanent")} disabled={!version || busy}>
Lock permanently
</Button>
</>
)}
{canCreateEditableCopy && (
<Button variant="primary" onClick={() => void createEditableCopy()} disabled={!version || busy}>
{busy ? "Creating copy…" : "Create editable copy"}
</Button>
)}
</div>
<ConfirmDialog
open={confirmAction !== null}
title={confirmDialogTitle(confirmAction)}
message={confirmDialogMessage(confirmAction)}
confirmLabel={confirmDialogLabel(confirmAction)}
tone={confirmAction === "unlock-user" ? "default" : "danger"}
busy={busy}
onCancel={() => setConfirmAction(null)}
onConfirm={() => {
const action = confirmAction;
setConfirmAction(null);
if (action) void runAction(action);
}}
/>
</DismissibleAlert>
);
}
type LockFlags = {
historicalVersion: boolean;
validationLock: boolean;
temporaryUserLock: boolean;
permanentUserLock: boolean;
finalLock: boolean;
};
function lockPresentation(
version: CampaignVersionDetail | CampaignVersionListItem | null,
flags: LockFlags,
): { kind: string; title: string; description: string; info: string } {
if (flags.historicalVersion) {
return {
kind: "historical",
title: "Historical version.",
description: "This version is review-only. Only the campaign's current working version may be edited in place.",
info: `Version #${version?.version_number ?? "—"}.`,
};
}
if (flags.temporaryUserLock) {
return {
kind: "temporary-user",
title: "Temporarily locked version.",
description: "Campaign data is read-only, but an authorized user may unlock it or make the lock permanent.",
info: version?.user_locked_at ? `Locked ${formatDateTime(version.user_locked_at)}.` : "",
};
}
if (flags.permanentUserLock) {
return {
kind: "permanent-user",
title: "Permanently locked version.",
description: "This user-requested snapshot cannot be unlocked. Create an editable copy for future changes.",
info: version?.user_locked_at || version?.published_at
? `Locked ${formatDateTime(version.user_locked_at ?? version.published_at)}.`
: "",
};
}
if (flags.validationLock) {
return {
kind: "validation",
title: "Temporarily locked after validation.",
description: "This protects the validated build input. Unlocking invalidates validation, build and generated queue state.",
info: version?.locked_at ? `Validated ${formatDateTime(version.locked_at)}.` : "",
};
}
if (flags.finalLock) {
return {
kind: "delivery",
title: "Permanently locked delivery snapshot.",
description: "Delivery has started or the version is in a final state, so it cannot be unlocked in place.",
info: version?.locked_at ? `Locked ${formatDateTime(version.locked_at)}.` : "",
};
}
return {
kind: "other",
title: "Locked version.",
description: "This version is read-only. Create an editable copy to continue working.",
info: version?.locked_at ? `Locked ${formatDateTime(version.locked_at)}.` : "",
};
}
function confirmDialogTitle(action: ConfirmAction): string {
if (action === "unlock-validation") return "Unlock validation?";
if (action === "unlock-user") return "Unlock temporary lock?";
if (action === "permanent") return "Lock permanently?";
return "Confirm action";
}
function confirmDialogMessage(action: ConfirmAction): string {
if (action === "unlock-validation") {
return "This makes the version editable and clears validation, build results, review acknowledgement and generated jobs for this version.";
}
if (action === "unlock-user") {
return "This removes the reversible user lock. Campaign data and existing workflow results are not otherwise changed.";
}
if (action === "permanent") {
return "This lock cannot be removed by any role. The version remains available for review and audit, but future changes require an editable copy.";
}
return "Continue?";
}
function confirmDialogLabel(action: ConfirmAction): string {
if (action === "unlock-validation") return "Unlock validation";
if (action === "unlock-user") return "Unlock";
if (action === "permanent") return "Lock permanently";
return "Confirm";
}