mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-07 16:18:47 +00:00
♻️ Label table cell optimization
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useSearchParams, usePathname, useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
import { useCallback, useMemo, useTransition } from "react";
|
||||
|
||||
export function useNavigationContext() {
|
||||
const router = useRouter();
|
||||
@@ -7,29 +7,38 @@ export function useNavigationContext() {
|
||||
const path = usePathname();
|
||||
const [isNavigating, startTransition] = useTransition();
|
||||
|
||||
function navigate({
|
||||
searchParams: params,
|
||||
pathname = path,
|
||||
replace = false
|
||||
}: {
|
||||
pathname?: string;
|
||||
searchParams?: URLSearchParams;
|
||||
replace?: boolean;
|
||||
}) {
|
||||
startTransition(() => {
|
||||
const fullPath = pathname + (params ? `?${params.toString()}` : "");
|
||||
const navigate = useCallback(
|
||||
function ({
|
||||
searchParams: params,
|
||||
pathname = path,
|
||||
replace = false
|
||||
}: {
|
||||
pathname?: string;
|
||||
searchParams?: URLSearchParams;
|
||||
replace?: boolean;
|
||||
}) {
|
||||
startTransition(() => {
|
||||
const fullPath =
|
||||
pathname + (params ? `?${params.toString()}` : "");
|
||||
|
||||
if (replace) {
|
||||
router.replace(fullPath);
|
||||
} else {
|
||||
router.push(fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (replace) {
|
||||
router.replace(fullPath);
|
||||
} else {
|
||||
router.push(fullPath);
|
||||
}
|
||||
});
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const writableSearchParams = useMemo(
|
||||
() => new URLSearchParams(searchParams),
|
||||
[searchParams]
|
||||
);
|
||||
|
||||
return {
|
||||
pathname: path,
|
||||
searchParams: new URLSearchParams(searchParams), // we want the search params to be writeable
|
||||
searchParams: writableSearchParams,
|
||||
navigate,
|
||||
isNavigating
|
||||
};
|
||||
|
||||
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