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
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ registerPack("ar", {
"automations.filter.completed": "مكتمل",
"automations.refresh": "تحديث عمليات الأتمتة",
"automations.error.loadFailed": "غير قادر على تحميل عمليات الأتمتة",
"automations.error.actionFailed": "تعذّر تحديث الأتمتة. يُرجى المحاولة مرة أخرى.",
"automations.rename.action": "إعادة تسمية الأتمتة",
"automations.rename.nameLabel": "اسم الأتمتة",
"automations.rename.nameRequired": "اسم الأتمتة مطلوب.",
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ registerPack("de", {
"automations.filter.completed": "Abgeschlossen",
"automations.refresh": "Automatisierungen aktualisieren",
"automations.error.loadFailed": "Automatisierungen können nicht geladen werden",
"automations.error.actionFailed": "Die Automatisierung konnte nicht aktualisiert werden. Versuche es erneut.",
"automations.rename.action": "Automatisierung umbenennen",
"automations.rename.nameLabel": "Automatisierungsname",
"automations.rename.nameRequired": "Automatisierungsname ist erforderlich.",
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,7 @@ registerPack("en", {
"automations.filter.completed": "Completed",
"automations.refresh": "Refresh automations",
"automations.error.loadFailed": "Unable to load automations",
"automations.error.actionFailed": "Unable to update the automation. Please try again.",
"automations.rename.action": "Rename automation",
"automations.rename.nameLabel": "Automation name",
"automations.rename.nameRequired": "Automation name is required.",
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ registerPack("es", {
"automations.filter.completed": "Completadas",
"automations.refresh": "Actualizar automatizaciones",
"automations.error.loadFailed": "No se pueden cargar automatizaciones",
"automations.error.actionFailed": "No se pudo actualizar la automatización. Inténtalo de nuevo.",
"automations.rename.action": "Cambiar nombre de automatización",
"automations.rename.nameLabel": "Nombre de automatización",
"automations.rename.nameRequired": "El nombre de la automatización es obligatorio.",
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ registerPack("fr", {
"automations.filter.completed": "Terminées",
"automations.refresh": "Actualiser les automatisations",
"automations.error.loadFailed": "Impossible de charger les automatisations",
"automations.error.actionFailed": "Impossible de mettre à jour l’automatisation. Réessayez.",
"automations.rename.action": "Renommer l’automatisation",
"automations.rename.nameLabel": "Nom de l’automatisation",
"automations.rename.nameRequired": "Le nom de l’automatisation est requis.",
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ registerPack("hi", {
"automations.filter.completed": "पूर्ण",
"automations.refresh": "ऑटोमेशन ताज़ा करें",
"automations.error.loadFailed": "ऑटोमेशन लोड करने में असमर्थ",
"automations.error.actionFailed": "ऑटोमेशन अपडेट नहीं किया जा सका। फिर से कोशिश करें।",
"automations.rename.action": "ऑटोमेशन का नाम बदलें",
"automations.rename.nameLabel": "ऑटोमेशन नाम",
"automations.rename.nameRequired": "ऑटोमेशन नाम आवश्यक है.",
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ registerPack("ja", {
"automations.filter.completed": "完了",
"automations.refresh": "自動化の更新",
"automations.error.loadFailed": "自動化をロードできません",
"automations.error.actionFailed": "自動化を更新できませんでした。もう一度お試しください。",
"automations.rename.action": "自動化名を変更",
"automations.rename.nameLabel": "自動化名",
"automations.rename.nameRequired": "自動化名は必須です。",
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ registerPack("ko", {
"automations.filter.completed": "완료됨",
"automations.refresh": "자동화 새로 고침",
"automations.error.loadFailed": "자동화를 로드할 수 없음",
"automations.error.actionFailed": "자동화를 업데이트할 수 없습니다. 다시 시도하세요.",
"automations.rename.action": "자동화 이름 변경",
"automations.rename.nameLabel": "자동화 이름",
"automations.rename.nameRequired": "자동화 이름은 필수입니다.",
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ registerPack("pt-BR", {
"automations.filter.completed": "Concluídas",
"automations.refresh": "Atualizar automações",
"automations.error.loadFailed": "Não é possível carregar automações",
"automations.error.actionFailed": "Não foi possível atualizar a automação. Tente novamente.",
"automations.rename.action": "Renomear automação",
"automations.rename.nameLabel": "Nome da automação",
"automations.rename.nameRequired": "O nome da automação é obrigatório.",
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/uk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ registerPack("uk", {
"automations.filter.completed": "Завершені",
"automations.refresh": "Оновити автоматизації",
"automations.error.loadFailed": "Неможливо завантажити автоматизації",
"automations.error.actionFailed": "Не вдалося оновити автоматизацію. Спробуйте ще раз.",
"automations.rename.action": "Перейменувати автоматизацію",
"automations.rename.nameLabel": "Назва автоматизації",
"automations.rename.nameRequired": "Назва автоматизації обов'язкова.",
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ registerPack("zh-CN", {
"automations.filter.completed": "已完成",
"automations.refresh": "更新最新自动化",
"automations.error.loadFailed": "提取系统自动化计划列表不成功!",
"automations.error.actionFailed": "无法更新自动化。请重试。",
"automations.rename.action": "重命名自动化",
"automations.rename.nameLabel": "自动化名称",
"automations.rename.nameRequired": "自动化名称不能为空。",
Expand Down
10 changes: 10 additions & 0 deletions crates/ironclaw_webui/frontend/src/lib/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,16 @@ test("locale packs include skill auto-activation controls", () => {
}
});

test("locale packs include automation action failure copy", () => {
const key = "automations.error.actionFailed";

for (const locale of LOCALES) {
const pack = loadLocalePack(locale);
assert.equal(typeof pack[key], "string", `${locale} missing ${key}`);
assert.notEqual(pack[key].trim(), "", `${locale} has empty ${key}`);
}
});

test("locale packs include admin write-only secret management copy", () => {
const requiredKeys = [
"admin.user.secrets.title",
Expand Down
10 changes: 10 additions & 0 deletions crates/ironclaw_webui/frontend/src/lib/toast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { beforeEach, test, vi } from "vitest";

const hotToast = vi.hoisted(() => ({
blank: vi.fn((..._args: unknown[]) => "blank-id"),
dismiss: vi.fn((..._args: unknown[]) => {}),
error: vi.fn((..._args: unknown[]) => "error-id"),
success: vi.fn((..._args: unknown[]) => "success-id"),
}));

vi.mock("react-hot-toast", () => ({
default: Object.assign(hotToast.blank, {
dismiss: hotToast.dismiss,
error: hotToast.error,
success: hotToast.success,
}),
Expand All @@ -17,6 +19,7 @@ vi.mock("react-hot-toast", () => ({
import {
DEFAULT_ERROR_TOAST_DURATION,
DEFAULT_TOAST_DURATION,
dismissToast,
toast,
} from "./toast";

Expand Down Expand Up @@ -62,3 +65,10 @@ test("toast preserves an explicit duration", () => {
},
]);
});

test("dismissToast removes the matching active toast", () => {
dismissToast("error-id");
dismissToast(null);

assert.deepEqual(hotToast.dismiss.mock.calls, [["error-id"]]);
});
5 changes: 5 additions & 0 deletions crates/ironclaw_webui/frontend/src/lib/toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ export function toast(message: string, opts: ToastOptions = {}) {
if (tone === "success") return hotToast.success(message, options);
return hotToast(message, options);
}

export function dismissToast(id: string | null | undefined) {
if (id == null) return;
hotToast.dismiss(id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,6 @@ export function AutomationsPage() {
{t("automations.error.loadFailed")}
</div>
)}
{automationsState.actionError &&
(
<div
className="rounded-xl border border-red-400/30 bg-red-500/10 px-4 py-3 text-sm text-red-200"
>
{automationsState.actionError.message}
</div>
)}

{showErrorOnly
? null
: (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import assert from "node:assert/strict";
import { test } from "vitest";

import {
createAutomationMutationConfig,
createAutomationMutationLifecycle,
} from "./useAutomations";

test("automation mutation configs share an explicit latest-action lifecycle", async () => {
const latestActionSequence = { current: 0 };
const actionErrorToastId = { current: null as string | null };
const dismissedToastIds: Array<string | null | undefined> = [];
const shownToastIds: string[] = [];
let invalidationCount = 0;

const lifecycle = createAutomationMutationLifecycle({
latestActionSequence,
actionErrorToastId,
dismissErrorToast: (id) => dismissedToastIds.push(id),
showErrorToast: () => {
const id = `toast-${shownToastIds.length + 1}`;
shownToastIds.push(id);
return id;
},
invalidateAutomations: () => {
invalidationCount += 1;
},
});
const pause = async (automationId: string) => ({ automationId });
const rename = async (variables: { automationId: string; name: string }) =>
variables;
const pauseConfig = createAutomationMutationConfig(pause, lifecycle);
const renameConfig = createAutomationMutationConfig(rename, lifecycle);

assert.equal(pauseConfig.mutationFn, pause);
assert.equal(renameConfig.mutationFn, rename);
for (const callbackName of ["onMutate", "onError", "onSuccess"] as const) {
assert.equal(pauseConfig[callbackName], lifecycle[callbackName]);
assert.equal(renameConfig[callbackName], lifecycle[callbackName]);
}

const firstAction = await lifecycle.onMutate("automation-1");
const secondAction = await lifecycle.onMutate({
automationId: "automation-2",
name: "New name",
});
assert.deepEqual(dismissedToastIds, []);

lifecycle.onError(
new Error("raw backend detail"),
"automation-1",
firstAction
);
assert.deepEqual(shownToastIds, []);

lifecycle.onError(
new Error("raw backend detail"),
{ automationId: "automation-2", name: "New name" },
secondAction
);
assert.deepEqual(shownToastIds, ["toast-1"]);
assert.equal(actionErrorToastId.current, "toast-1");

// A late callback from the older action must not dismiss or overwrite the
// toast now owned by the latest action.
lifecycle.onError(new Error("late failure"), "automation-1", firstAction);
lifecycle.onSuccess({ updated: true }, "automation-1", firstAction);
assert.deepEqual(shownToastIds, ["toast-1"]);
assert.deepEqual(dismissedToastIds, []);
assert.equal(actionErrorToastId.current, "toast-1");
assert.equal(invalidationCount, 1);

const thirdAction = await lifecycle.onMutate("automation-3");
assert.deepEqual(dismissedToastIds, ["toast-1"]);
assert.equal(actionErrorToastId.current, null);

lifecycle.onSuccess({ updated: true }, "automation-3", thirdAction);
assert.deepEqual(dismissedToastIds, ["toast-1"]);
assert.equal(invalidationCount, 2);
});
Loading
Loading