mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-06 23:59:02 +00:00
♻️ Label table cell optimization
This commit is contained in:
103
src/hooks/useOptimisticLabels.ts
Normal file
103
src/hooks/useOptimisticLabels.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user