Files
pangolin/src/hooks/useOptimisticLabels.ts
2026-06-03 19:15:44 +02:00

104 lines
3.0 KiB
TypeScript

import type { SelectedLabel } from "@app/components/labels-selector";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useState, useMemo } from "react";
import { toast } from "./useToast";
import { useEnvContext } from "./useEnvContext";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
export type LabelToggleAction = {
label: SelectedLabel;
action: "attach" | "detach";
};
function computeLabelToggleActions(
values: SelectedLabel[],
actions: LabelToggleAction[]
) {
let newValues = [...values];
for (const { action, label } of actions) {
if (action === "attach") {
newValues = [...newValues, label];
} else {
newValues = newValues.filter((lb) => lb.labelId !== label.labelId);
}
}
return newValues;
}
type UseOptimisticLabelsArgs = {
serverLabels: SelectedLabel[] | undefined;
orgId: string;
entityId: number;
entityIdField: string;
};
export function useOptimisticLabels({
serverLabels,
orgId,
entityId,
entityIdField
}: UseOptimisticLabelsArgs) {
const router = useRouter();
const labels = serverLabels ?? [];
const api = createApiClient(useEnvContext());
const t = useTranslations();
const [pendingActions, setPendingActions] = useState<LabelToggleAction[]>(
[]
);
const localLabels = useMemo(
() => computeLabelToggleActions(labels ?? [], pendingActions),
[labels, pendingActions]
);
async function toggleLabel(
label: SelectedLabel,
action: "attach" | "detach"
) {
const oppositeAction = action === "attach" ? "detach" : "attach";
const existingActionIndex = pendingActions.findIndex(
(pending) =>
pending.action === oppositeAction &&
pending.label.labelId === label.labelId
);
// if there are two actions that cancel each-other
// they should just be removed
if (existingActionIndex !== -1) {
setPendingActions((prevActions) =>
prevActions.toSpliced(existingActionIndex, 1)
);
} else {
setPendingActions((actions) => [...actions, { label, action }]);
}
try {
if (action === "attach") {
await api.put(`/org/${orgId}/label/${label.labelId}/attach`, {
[entityIdField]: entityId
});
} else {
await api.put(`/org/${orgId}/label/${label.labelId}/detach`, {
[entityIdField]: entityId
});
}
} catch (e) {
toast({
title: t("error"),
description: formatAxiosError(e, t("errorOccurred")),
variant: "destructive"
});
}
}
async function refresh() {
router.refresh();
setPendingActions([]);
}
return { localLabels, toggleLabel, refresh };
}