(frontend) enhance API and driver functionality

Updated the fetchAPI to accept boolean parameters, added new enums for
item filters, and expanded the Driver class with methods for managing
favorite items and link configurations. Introduced utility functions for
managing paginated lists and improved item handling in the explorer
component. Removed unused constants and optimized query handling for
better performance.
This commit is contained in:
Nathan Panchout
2026-02-06 15:39:32 +01:00
committed by Manuel Raynaud
parent a705b1529f
commit a4954afba1
17 changed files with 915 additions and 414 deletions
@@ -33,7 +33,7 @@ export interface fetchAPIOptions {
export const fetchAPI = async (
input: string,
init?: RequestInit & { params?: Record<string, string | number> },
init?: RequestInit & { params?: Record<string, string | number | boolean> },
options?: fetchAPIOptions
) => {
const apiUrl = new URL(`${baseApiUrl("1.0")}${input}`);
@@ -1,20 +1,25 @@
import { Role } from "../types";
import { LinkReach, LinkRole, Role } from "../types";
export type DTOCreateAccess = {
itemId: string;
userId: string;
role: Role;
itemId: string;
userId: string;
role: Role;
};
export type DTOUpdateAccess = {
itemId: string;
accessId: string;
user_id: string;
role: Role;
itemId: string;
accessId: string;
user_id: string;
role: Role;
};
export type DTODeleteAccess = {
itemId: string;
accessId: string;
}
itemId: string;
accessId: string;
};
export type DTOUpdateLinkConfiguration = {
itemId: string;
link_reach: LinkReach;
link_role?: LinkRole | null;
};
@@ -2,6 +2,7 @@ import {
DTOCreateAccess,
DTODeleteAccess,
DTOUpdateAccess,
DTOUpdateLinkConfiguration,
} from "./DTOs/AccessesDTO";
import {
DTOCreateInvitation,
@@ -27,6 +28,17 @@ export enum ItemFiltersScope {
NOT_DELETED = "not_deleted",
}
export enum ItemFiltersOrdering {
CREATED_AT_ASC = "created_at",
CREATED_AT_DESC = "-created_at",
UPDATED_AT_ASC = "updated_at",
UPDATED_AT_DESC = "-updated_at",
TITLE_ASC = "title",
TITLE_DESC = "-title",
TYPE_ASC = "type",
TYPE_DESC = "-type",
}
export type ItemFilters = {
type?: ItemType;
title?: string;
@@ -35,6 +47,9 @@ export type ItemFilters = {
page?: number;
page_size?: number;
workspaces?: WorkspaceType;
is_creator_me?: boolean;
ordering?: string;
is_favorite?: boolean;
};
export type PaginatedChildrenResult = {
@@ -68,8 +83,8 @@ export abstract class Driver {
abstract getItemBreadcrumb(id: string): Promise<ItemBreadcrumb[]>;
abstract updateItem(item: Partial<Item>): Promise<Item>;
abstract restoreItems(ids: string[]): Promise<void>;
abstract moveItem(id: string, parentId: string): Promise<void>;
abstract moveItems(ids: string[], parentId: string): Promise<void>;
abstract moveItem(id: string, parentId?: string): Promise<void>;
abstract moveItems(ids: string[], parentId?: string): Promise<void>;
abstract getChildren(
id: string,
filters?: ItemFilters
@@ -77,9 +92,21 @@ export abstract class Driver {
abstract searchItems(filters?: ItemFilters): Promise<Item[]>;
// Accesses
abstract getItemAccesses(itemId: string): Promise<APIList<Access>>;
abstract getRecentItems(
filters?: ItemFilters
): Promise<PaginatedChildrenResult>;
abstract getFavoriteItems(
filters?: ItemFilters
): Promise<PaginatedChildrenResult>;
abstract createFavoriteItem(itemId: string): Promise<void>;
abstract deleteFavoriteItem(itemId: string): Promise<void>;
abstract getItemAccesses(itemId: string): Promise<Access[]>;
abstract createAccess(data: DTOCreateAccess): Promise<void>;
abstract updateAccess(payload: DTOUpdateAccess): Promise<Access>;
abstract updateAccess(payload: DTOUpdateAccess): Promise<Access | void>;
abstract updateLinkConfiguration(
payload: DTOUpdateLinkConfiguration
): Promise<void>;
abstract deleteAccess(payload: DTODeleteAccess): Promise<void>;
// Invitations
abstract getItemInvitations(itemId: string): Promise<APIList<Invitation>>;
@@ -100,7 +127,7 @@ export abstract class Driver {
abstract updateWorkspace(item: Partial<Item>): Promise<Item>;
abstract deleteWorkspace(id: string): Promise<void>;
abstract createFile(data: {
parentId: string;
parentId?: string;
filename: string;
}): Promise<Item>;
abstract deleteItems(ids: string[]): Promise<void>;
@@ -1,11 +1,20 @@
import { fetchAPI } from "@/features/api/fetchApi";
import { Driver, Entitlements, ItemFilters, UserFilters, PaginatedChildrenResult } from "../Driver";
import {
Driver,
Entitlements,
ItemFilters,
UserFilters,
PaginatedChildrenResult,
} from "../Driver";
import {
DTODeleteInvitation,
DTOCreateInvitation,
DTOUpdateInvitation,
} from "../DTOs/InvitationDTO";
import { DTOCreateAccess } from "../DTOs/AccessesDTO";
import {
DTOCreateAccess,
DTOUpdateLinkConfiguration,
} from "../DTOs/AccessesDTO";
import { DTOUpdateAccess } from "../DTOs/AccessesDTO";
import {
Access,
@@ -112,7 +121,7 @@ export class StandardDriver extends Driver {
async getChildren(
id: string,
filters?: ItemFilters
filters?: ItemFilters,
): Promise<PaginatedChildrenResult> {
const params = {
page: 1,
@@ -142,14 +151,17 @@ export class StandardDriver extends Driver {
return jsonToItem(data);
}
async moveItem(id: string, parentId: string): Promise<void> {
async moveItem(id: string, parentId?: string): Promise<void> {
const payload = {
...(parentId ? { target_item_id: parentId } : {}),
};
await fetchAPI(`items/${id}/move/`, {
method: "POST",
body: JSON.stringify({ target_item_id: parentId }),
body: JSON.stringify(payload),
});
}
async getItemAccesses(itemId: string): Promise<APIList<Access>> {
async getItemAccesses(itemId: string): Promise<Access[]> {
const response = await fetchAPI(`items/${itemId}/accesses/`);
const data = await response.json();
return data;
@@ -171,15 +183,30 @@ export class StandardDriver extends Driver {
});
}
async updateLinkConfiguration(
payload: DTOUpdateLinkConfiguration,
): Promise<void> {
const { itemId, ...rest } = payload;
await fetchAPI(`items/${itemId}/link-configuration/`, {
method: "PUT",
body: JSON.stringify(rest),
});
}
async updateAccess({
itemId,
accessId,
...payload
}: DTOUpdateAccess): Promise<Access> {
}: DTOUpdateAccess): Promise<Access | void> {
const response = await fetchAPI(`items/${itemId}/accesses/${accessId}/`, {
method: "PATCH",
body: JSON.stringify(payload),
});
if (response.status === 204) {
return;
}
const data = await response.json();
return data;
}
@@ -201,7 +228,7 @@ export class StandardDriver extends Driver {
`items/${payload.itemId}/invitations/${payload.invitationId}/`,
{
method: "DELETE",
}
},
);
}
@@ -211,7 +238,7 @@ export class StandardDriver extends Driver {
{
method: "PATCH",
body: JSON.stringify(payload),
}
},
);
const data = await response.json();
return data;
@@ -223,7 +250,7 @@ export class StandardDriver extends Driver {
return data;
}
async moveItems(ids: string[], parentId: string): Promise<void> {
async moveItems(ids: string[], parentId?: string): Promise<void> {
for (const id of ids) {
await this.moveItem(id, parentId);
}
@@ -234,7 +261,8 @@ export class StandardDriver extends Driver {
parentId?: string;
}): Promise<Item> {
const { parentId, ...rest } = data;
const response = await fetchAPI(`items/${parentId}/children/`, {
const url = parentId ? `items/${parentId}/children/` : `items/`;
const response = await fetchAPI(url, {
method: "POST",
body: JSON.stringify({
...rest,
@@ -268,15 +296,63 @@ export class StandardDriver extends Driver {
return this.deleteItems([id]);
}
async getRecentItems(
filters?: ItemFilters,
): Promise<PaginatedChildrenResult> {
const response = await fetchAPI(`items/recents/`, {
params: { ...filters, page_size: 200 },
});
const data = await response.json();
return {
children: jsonToItems(data.results),
pagination: {
currentPage: filters?.page ?? 1,
totalCount: data.count,
hasMore: data.next !== null,
},
};
}
async getFavoriteItems(
filters?: ItemFilters,
): Promise<PaginatedChildrenResult> {
const response = await fetchAPI(`items/favorite_list/`, {
params: { ...filters, page_size: 200 },
});
const data = await response.json();
return {
children: jsonToItems(data.results),
pagination: {
currentPage: filters?.page ?? 1,
totalCount: data.count,
hasMore: data.next !== null,
},
};
}
async createFavoriteItem(itemId: string): Promise<void> {
await fetchAPI(`items/${itemId}/favorite/`, {
method: "POST",
});
}
async deleteFavoriteItem(itemId: string): Promise<void> {
await fetchAPI(`items/${itemId}/favorite/`, {
method: "DELETE",
});
}
async createFile(data: {
parentId: string;
parentId?: string;
file: File;
filename: string;
progressHandler?: (progress: number) => void;
}): Promise<Item> {
const { parentId, file, progressHandler, ...rest } = data;
const url = parentId ? `items/${parentId}/children/` : `items/`;
const response = await fetchAPI(
`items/${parentId}/children/`,
url,
{
method: "POST",
body: JSON.stringify({
@@ -289,7 +365,7 @@ export class StandardDriver extends Driver {
// We don't want to redirect to the login page in this case, instead
// we want to show an error.
redirectOn40x: false,
}
},
);
const item = jsonToItem(await response.json());
if (!item.policy) {
@@ -374,7 +450,7 @@ const jsonToItem = (data: any): Item => {
export const uploadFile = (
url: string,
file: File,
progressHandler: (progress: number) => void
progressHandler: (progress: number) => void,
) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
@@ -400,7 +476,7 @@ export const uploadFile = (
xhr.upload.addEventListener("progress", (progressEvent) => {
if (progressEvent.lengthComputable) {
progressHandler(
Math.floor((progressEvent.loaded / progressEvent.total) * 100)
Math.floor((progressEvent.loaded / progressEvent.total) * 100),
);
}
});
@@ -26,6 +26,7 @@ export enum ItemUploadState {
export type ItemBreadcrumb = {
id: string;
originalId?: string; // Used to identify all occurrences of the same item in the tree
title: string;
path: string;
depth: number;
@@ -34,6 +35,7 @@ export type ItemBreadcrumb = {
export type Item = {
id: string;
originalId?: string; // Used to identify all occurrences of the same item in the tree
title: string;
filename: string;
creator: {
@@ -42,12 +44,17 @@ export type Item = {
short_name: string;
};
type: ItemType;
ancestors_link_reach: LinkReach | null;
ancestors_link_role: LinkRole | null;
computed_link_reach: LinkReach | null;
computed_link_role: LinkRole | null;
deleted_at?: Date;
upload_state: string;
updated_at: Date;
description: string;
is_wopi_supported?: boolean;
created_at: Date;
is_favorite?: boolean;
children?: Item[];
parents?: Item[];
breadcrumb?: ItemBreadcrumb[];
@@ -61,6 +68,7 @@ export type Item = {
size?: number;
mimetype?: string;
user_roles?: Role[];
user_role?: Role;
link_reach?: LinkReach;
link_role?: LinkRole;
abilities: {
@@ -74,6 +82,7 @@ export type Item = {
link_configuration: boolean;
media_auth: boolean;
move: boolean;
link_select_options: Record<LinkReach, LinkRole[] | null>;
partial_update: boolean;
restore: boolean;
retrieve: boolean;
@@ -86,6 +95,11 @@ export type Item = {
export type TreeItemData = Omit<Item, "children"> & {
parentId?: string;
/**
* The original item ID (without tree path prefix).
* Used to identify all occurrences of the same item in the tree.
*/
originalId: string;
};
export type TreeItem = TreeViewDataType<TreeItemData>;
@@ -101,6 +115,16 @@ export type Access = {
role: string;
team: string;
user: User;
is_explicit: boolean;
max_role: Role;
max_ancestors_role: Role;
max_ancestors_role_item_id: string;
parent_id_max_role?: string; // Just for UI purposes
item: {
id: string;
path: string;
depth: number;
};
abilities: {
destroy: boolean;
partial_update: boolean;
@@ -1,17 +1,24 @@
import { getDriver } from "@/features/config/Config";
import { Item } from "@/features/drivers/types";
import { PaginatedChildrenResult } from "@gouvfr-lasuite/ui-kit";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useRemoveItemsFromPaginatedList } from "../hooks/useOptimisticPagination";
import {
getMyFilesQueryKey,
getRecentItemsQueryKey,
getSharedWithMeQueryKey,
} from "@/utils/defaultRoutes";
export const useMoveItems = () => {
type MoveItemPayload = {
ids: string[];
parentId: string;
oldParentId: string;
parentId?: string;
oldParentId?: string;
};
const queryClient = useQueryClient();
const driver = getDriver();
const removeItems = useRemoveItemsFromPaginatedList();
return useMutation({
mutationFn: async (payload: MoveItemPayload) => {
await driver.moveItems(payload.ids, payload.parentId);
@@ -27,25 +34,12 @@ export const useMoveItems = () => {
});
},
onSuccess: (data, payload: MoveItemPayload) => {
const queriesData = queryClient.getQueriesData({
queryKey: ["items", payload.oldParentId, "children", "infinite"],
});
queriesData.forEach((query) => {
const key = query[0];
const data: { pages: PaginatedChildrenResult<Item>[] } = JSON.parse(
JSON.stringify(query[1])
) as {
pages: PaginatedChildrenResult<Item>[];
};
data.pages.forEach((page) => {
page.children = page.children?.filter(
(child) => !payload.ids.includes(child.id)
);
});
queryClient.setQueryData(key, data);
removeItems(["items", payload.oldParentId], payload.ids);
removeItems(getMyFilesQueryKey(), payload.ids);
removeItems(getSharedWithMeQueryKey(), payload.ids);
removeItems(getRecentItemsQueryKey(), payload.ids);
queryClient.invalidateQueries({
queryKey: ["items", payload.parentId],
});
},
onError: (err, variables) => {
@@ -12,7 +12,11 @@ import {
} from "@dnd-kit/core";
import { getEventCoordinates } from "@dnd-kit/utilities";
import { useMoveItems } from "../api/useMoveItem";
import { useGlobalExplorer } from "./GlobalExplorerContext";
import {
itemToTreeItem,
useGlobalExplorer,
getOriginalIdFromTreeId,
} from "./GlobalExplorerContext";
import { Item, TreeItem } from "@/features/drivers/types";
import { ExplorerDragOverlay } from "./tree/ExploreDragOverlay";
import { TreeViewNodeTypeEnum, useTreeContext } from "@gouvfr-lasuite/ui-kit";
@@ -23,6 +27,8 @@ import {
ConfirmationMoveState,
ExplorerTreeMoveConfirmationModal,
} from "./tree/ExplorerTreeMoveConfirmationModal";
import { DefaultRoute } from "@/utils/defaultRoutes";
import { useMutationCreateFavoriteItem } from "../hooks/useMutations";
const activationConstraint = {
distance: 20,
@@ -52,12 +58,13 @@ export const useDragItemContext = () => {
export const ExplorerDndProvider = ({ children }: ExplorerDndProviderProps) => {
const moveConfirmationModal = useModal();
const [overedItemIds, setOveredItemIds] = useState<Record<string, boolean>>(
{}
{},
);
const [moveState, setMoveState] = useState<ConfirmationMoveState | undefined>(
undefined
undefined,
);
const { itemId, selectedItems, setSelectedItems } = useGlobalExplorer();
const { mutateAsync: createFavoriteItem } = useMutationCreateFavoriteItem();
const treeContext = useTreeContext<TreeItem>();
@@ -72,6 +79,12 @@ export const ExplorerDndProvider = ({ children }: ExplorerDndProviderProps) => {
const keyboardSensor = useSensor(KeyboardSensor, {});
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
const handleCreateFavoriteItem = async (item: Item) => {
await createFavoriteItem(item.id);
// Generate a unique tree ID for the favorite item
const itemTree = itemToTreeItem(item, DefaultRoute.FAVORITES, true);
treeContext?.treeData.addChild(DefaultRoute.FAVORITES, itemTree);
};
const handleDragStart = (ev: DragStartEvent) => {
document.body.style.cursor = "grabbing";
@@ -108,19 +121,36 @@ export const ExplorerDndProvider = ({ children }: ExplorerDndProviderProps) => {
// Reset the selected items after the move
setSelectedItems([]);
},
}
},
);
};
const handleDragEnd = async ({ active, over }: DragEndEvent) => {
document.body.style.cursor = "default";
const activeItem = active.data.current?.item as Item;
const overItem = over?.data.current?.item as Item;
const activeItemRaw = active.data.current?.item as Item;
const overItemRaw = over?.data.current?.item as Item;
if (!activeItem || !overItem) {
// Extract the original item ID from the tree ID (handles favorites path format)
const activeItem = {
...activeItemRaw,
id: getOriginalIdFromTreeId(activeItemRaw.id),
};
const overItemId = overItemRaw?.id
? getOriginalIdFromTreeId(overItemRaw.id)
: undefined;
if (overItemId === DefaultRoute.FAVORITES && activeItem) {
await handleCreateFavoriteItem(activeItem);
return;
}
if (!activeItem || !overItemRaw || !overItemId) {
return;
}
const overItem = { ...overItemRaw, id: overItemId };
if (activeItem.id === overItem.id) {
return;
}
@@ -139,6 +169,7 @@ export const ExplorerDndProvider = ({ children }: ExplorerDndProviderProps) => {
sourceItem: activeItem,
targetItem: overItem,
});
setOveredItemIds({});
moveConfirmationModal.open();
return;
}
@@ -212,7 +243,16 @@ export const snapToTopLeft: Modifier = ({
};
export const canDrop = (activeItem: Item, overItem: Item | TreeItem) => {
if (activeItem.id === overItem.id) {
// Extract the original item ID from the tree ID (handles favorites path format)
const overItemId = overItem?.id
? getOriginalIdFromTreeId(overItem.id)
: undefined;
const activeItemId = getOriginalIdFromTreeId(activeItem.id);
if (overItemId === DefaultRoute.FAVORITES) {
return true;
}
if (activeItemId === overItemId) {
return false;
}
@@ -244,14 +284,6 @@ export const canDrop = (activeItem: Item, overItem: Item | TreeItem) => {
return false;
}
if (activePathSegments.length === 1 && overPathSegments.length === 1) {
return activePathSegments[0] === overPathSegments[0];
}
if (activePathSegments.length < 2) {
return false;
}
if (overPathSegments.length < 1) {
return false;
}
@@ -1,44 +0,0 @@
import { Item, TreeItem } from "@/features/drivers/types";
import {
TreeViewDataType,
TreeViewNodeTypeEnum,
useTreeContext,
} from "@gouvfr-lasuite/ui-kit";
import { useTranslation } from "react-i18next";
import { itemToTreeItem, useGlobalExplorer } from "../../GlobalExplorerContext";
import { WorkspaceCategory } from "../../../constants";
export const useAddWorkspaceNode = () => {
const treeContext = useTreeContext<TreeItem>();
const { t } = useTranslation();
const { refreshMobileNodes } = useGlobalExplorer();
const addWorkspaceNode = (data: Item) => {
const sharedNode = treeContext?.treeData.getNode(
WorkspaceCategory.SHARED_SPACE
);
if (!sharedNode) {
const publicWorkspaceNode: TreeViewDataType<TreeItem> = {
id: WorkspaceCategory.SHARED_SPACE,
nodeType: TreeViewNodeTypeEnum.SIMPLE_NODE,
childrenCount: 1,
label: t("explorer.tree.shared_space"),
children: [itemToTreeItem(data)],
pagination: {
currentPage: 1,
hasMore: false,
},
};
treeContext?.treeData.addRootNode(publicWorkspaceNode, 1);
} else {
treeContext?.treeData.addChild(
WorkspaceCategory.SHARED_SPACE,
itemToTreeItem(data),
0
);
}
// Refresh mobile nodes to ensure the workspace category is displayed if needed
refreshMobileNodes();
};
return { addWorkspaceNode };
};
@@ -1,64 +0,0 @@
import { TreeItem } from "@/features/drivers/types";
import { useTreeContext } from "@gouvfr-lasuite/ui-kit";
import { WorkspaceCategory } from "../../../constants";
export const useDeleteTreeNode = () => {
const treeContext = useTreeContext<TreeItem>();
const deleteTreeNode = (
nodeId: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_redirectToParent: boolean = false
) => {
// Get the node to delete
const node = treeContext?.treeData.getNode(nodeId);
if (!node) return; // If the node is not found, return
const parentId = treeContext?.treeData.getParentId(nodeId);
if (
parentId !== WorkspaceCategory.SHARED_SPACE &&
parentId !== WorkspaceCategory.PUBLIC_SPACE
) {
treeContext?.treeData.deleteNode(nodeId);
return;
}
// This function checks if the node to be deleted is a direct child of the root "shared" or "public" nodes.
// If it is, it deletes the node. If that root node only had this single child,
// it also deletes the root node itself (removing the shared/public section from the tree when empty).
const checkRootNode = (parentKey: string): boolean => {
const rootNode = treeContext?.treeData.getNode(parentKey);
if (!rootNode) return false;
// Check if nodeId is a direct child of the given root node (shared/public section)
const isChildOfRoot = rootNode?.children?.some(
(child) => child.id === nodeId
);
if (!isChildOfRoot) return false;
// If this is the last child, we will remove the root as well
const isLastChild = rootNode.children?.length === 1;
// Delete the target node from the tree
treeContext?.treeData.deleteNode(nodeId);
// If it was the last child, remove the section/root node as well
if (isLastChild) {
treeContext?.treeData.deleteNode(parentKey);
}
return true;
};
const isSharedLastChild = checkRootNode(WorkspaceCategory.SHARED_SPACE);
if (!isSharedLastChild) {
checkRootNode(WorkspaceCategory.PUBLIC_SPACE);
}
};
return { deleteTreeNode };
};
@@ -1,4 +0,0 @@
export enum WorkspaceCategory {
SHARED_SPACE = "SHARED_SPACE",
PUBLIC_SPACE = "PUBLIC_SPACE",
}
@@ -4,20 +4,26 @@ import {
} from "@/features/ui/components/toaster/Toaster";
import { useMutationDeleteItems } from "./useMutations";
import { useTranslation } from "react-i18next";
import { useTreeUtils } from "./useTreeUtils";
export const useDeleteItem = () => {
const { t } = useTranslation();
const treeUtils = useTreeUtils();
const deleteItemsMutation = useMutationDeleteItems();
const deleteItems = async (itemIds: string[]) => {
try {
await deleteItemsMutation.mutateAsync(itemIds);
for (const itemId of itemIds) {
treeUtils.deleteAllByOriginalId(itemId);
}
addToast(
<ToasterItem>
<span className="material-icons">delete</span>
<span>
{t("explorer.actions.delete.toast", { count: itemIds.length })}
</span>
</ToasterItem>
</ToasterItem>,
);
} catch {
addToast(
@@ -28,7 +34,7 @@ export const useDeleteItem = () => {
count: itemIds.length,
})}
</span>
</ToasterItem>
</ToasterItem>,
);
}
};
@@ -1,29 +1,57 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { getDriver } from "@/features/config/Config";
import { ItemFilters } from "@/features/drivers/Driver";
import { ItemFilters, PaginatedChildrenResult } from "@/features/drivers/Driver";
type Fetcher = (filters: ItemFilters) => Promise<PaginatedChildrenResult>;
const createInfiniteItemsHook = (
defaultQueryKey: string[],
fetcher: Fetcher
) => {
return (
filters: ItemFilters = {},
queryKey: string[] = defaultQueryKey,
enabled: boolean = true
) => {
const effectiveQueryKey = [
...queryKey,
...(Object.keys(filters).length ? [JSON.stringify(filters)] : []),
];
return useInfiniteQuery({
queryKey: effectiveQueryKey,
queryFn: ({ pageParam = 1 }) => {
return fetcher({
page: pageParam,
...filters,
});
},
getNextPageParam: (lastPage) => {
return lastPage.pagination.hasMore
? lastPage.pagination.currentPage + 1
: undefined;
},
initialPageParam: 1,
enabled: enabled,
});
};
};
export const useInfiniteItems = (
filters: ItemFilters = {},
queryKey: string[] = ["items", "infinite"],
enabled: boolean = true
) => {
return useInfiniteQuery({
queryKey: [
"items",
"infinite",
...(Object.keys(filters).length ? [JSON.stringify(filters)] : []),
],
queryFn: ({ pageParam = 1 }) => {
return getDriver().getItems({
page: pageParam,
...filters,
});
},
getNextPageParam: (lastPage) => {
return lastPage.pagination.hasMore
? lastPage.pagination.currentPage + 1
: undefined;
},
initialPageParam: 1,
enabled: enabled,
});
const fetcher: Fetcher = filters.is_favorite
? (f) => getDriver().getFavoriteItems(f)
: (f) => getDriver().getItems(f);
return createInfiniteItemsHook(
["items", "infinite"],
fetcher
)(filters, queryKey, enabled);
};
export const useInfiniteRecentItems = createInfiniteItemsHook(
["items", "recent", "infinite"],
(filters) => getDriver().getRecentItems(filters)
);
@@ -1,21 +1,37 @@
import { getDriver } from "@/features/config/Config";
import { Item } from "@/features/drivers/types";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useGlobalExplorer } from "../components/GlobalExplorerContext";
import {
useGlobalExplorer,
generateTreeId,
} from "../components/GlobalExplorerContext";
import {
useAddItemToPaginatedList,
useRemoveItemsFromPaginatedList,
} from "./useOptimisticPagination";
import { useTreeContext } from "@gouvfr-lasuite/ui-kit";
import {
useRefreshQueryCacheAfterMutation,
useDeleteMutationCallbacks,
useRefreshItemCache,
useRefreshFavoriteCache,
} from "./useRefreshItems";
import { DefaultRoute } from "@/utils/defaultRoutes";
// ============================================================================
// MUTATIONS
// ============================================================================
export const useMutationCreateFile = () => {
const driver = getDriver();
const queryClient = useQueryClient();
const refresh = useRefreshQueryCacheAfterMutation();
return useMutation({
mutationFn: async (...payload: Parameters<typeof driver.createFile>) => {
return driver.createFile(...payload);
},
onSuccess: (data, variables) => {
if (variables.parentId) {
queryClient.invalidateQueries({
queryKey: ["items", variables.parentId],
});
}
refresh(variables.parentId);
},
meta: {
showErrorOn403: true,
@@ -25,53 +41,25 @@ export const useMutationCreateFile = () => {
export const useMutationDeleteItems = () => {
const driver = getDriver();
const queryClient = useQueryClient();
const { item } = useGlobalExplorer();
const mutationCallbacks = useDeleteMutationCallbacks(
item?.originalId ?? item?.id,
);
return useMutation({
mutationFn: async (...payload: Parameters<typeof driver.deleteItems>) => {
await driver.deleteItems(...payload);
},
onMutate: async (itemIds) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: ["items", item!.id, "children"],
});
// Snapshot the previous value
const previousItems = queryClient.getQueryData([
"items",
item!.id,
"children",
]);
// Optimistically update to the new value
queryClient.setQueryData(
["items", item!.id, "children"],
(old: Item[]) =>
old ? old.filter((i: Item) => !itemIds.includes(i.id)) : old
);
// Return a context object with the snapshotted value
return { previousItems };
},
onError: (err, variables, context) => {
// If the mutation fails, use the context returned from onMutate to roll back
queryClient.setQueryData(
["items", item!.id, "children"],
context?.previousItems
);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["items", item!.id],
});
},
...mutationCallbacks,
});
};
export const useMutationHardDeleteItems = () => {
const driver = getDriver();
const queryClient = useQueryClient();
const mutationCallbacks = useDeleteMutationCallbacks(undefined, [
["items", "trash"],
]);
return useMutation({
mutationFn: async (
@@ -79,107 +67,76 @@ export const useMutationHardDeleteItems = () => {
) => {
await driver.hardDeleteItems(...payload);
},
onMutate: async (itemIds) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: ["items", "trash"],
});
// Snapshot the previous value
const previousItems = queryClient.getQueryData(["items", "trash"]);
// Optimistically update to the new value
queryClient.setQueryData(["items", "trash"], (old: Item[]) =>
old ? old.filter((i: Item) => !itemIds.includes(i.id)) : old
);
// Return a context object with the snapshotted value
return { previousItems };
},
onError: (err, variables, context) => {
// If the mutation fails, use the context returned from onMutate to roll back
queryClient.setQueryData(["items", "trash"], context?.previousItems);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["items", "trash"],
});
},
...mutationCallbacks,
});
};
export const useMutationRenameItem = () => {
const driver = getDriver();
const queryClient = useQueryClient();
const { item } = useGlobalExplorer();
const refreshItemCache = useRefreshItemCache();
return useMutation({
mutationFn: async (...payload: Parameters<typeof driver.updateItem>) => {
await driver.updateItem(...payload);
},
onMutate: async (itemUpdated) => {
await queryClient.cancelQueries({
queryKey: ["items", item!.id, "children"],
});
const previousItems = queryClient.getQueryData([
"items",
item!.id,
"children",
]);
queryClient.setQueryData(
["items", item!.id, "children"],
(old: Item[]) =>
old
? old.map((i: Item) =>
i.id === itemUpdated.id ? { ...i, ...itemUpdated } : i
)
: old
);
return { previousItems };
onMutate: async (...payload: Parameters<typeof driver.updateItem>) => {
if (!payload[0].id) {
return;
}
await refreshItemCache(payload[0].id!, { title: payload[0].title });
},
onError: (err, variables, context) => {
queryClient.setQueryData(
["items", item!.id, "children"],
context?.previousItems
);
onError: (_error, variables) => {
if (!variables.id) {
return;
}
refreshItemCache(variables.id);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["items", item!.id, "children"],
});
onSuccess: (_, itemUpdated) => {
if (!itemUpdated?.id) {
return;
}
refreshItemCache(itemUpdated.id, itemUpdated);
},
});
};
export const useMutationCreateFolder = () => {
const queryClient = useQueryClient();
const driver = getDriver();
const addItemToTopOfPaginatedList = useAddItemToPaginatedList();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.createFolder>) => {
return driver.createFolder(...payload);
},
onSuccess: (data, variables) => {
if (variables.parentId) {
queryClient.invalidateQueries({
queryKey: ["items", variables.parentId],
});
}
const queryKey = variables.parentId
? ["items", variables.parentId, "children"]
: ["items", "infinite", JSON.stringify({ is_creator_me: true })];
addItemToTopOfPaginatedList(queryKey, data);
},
});
};
export const useMutationUpdateItem = () => {
export const useMutationUpdateLinkConfiguration = () => {
const driver = getDriver();
const refreshItemCache = useRefreshItemCache();
const queryClient = useQueryClient();
const { item } = useGlobalExplorer();
const refreshQueryCacheAfterMutation = useRefreshQueryCacheAfterMutation();
return useMutation({
mutationFn: async (...payload: Parameters<typeof driver.updateItem>) => {
await driver.updateItem(...payload);
mutationFn: async (
...payload: Parameters<typeof driver.updateLinkConfiguration>
) => {
await driver.updateLinkConfiguration(...payload);
},
onSuccess: () => {
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["items", item!.id],
exact: true,
queryKey: ["items", variables.itemId],
});
queryClient.invalidateQueries({
queryKey: ["itemAccesses"],
});
},
});
@@ -248,98 +205,48 @@ export const useMutationUpdateWorkspace = () => {
});
};
// TODO: Make optimistic once the tree is implemented
export const useMutationDeleteWorskpace = () => {
const queryClient = useQueryClient();
export const useMutationCreateFavoriteItem = () => {
const driver = getDriver();
const refreshFavoriteCache = useRefreshFavoriteCache();
const refreshItemCache = useRefreshItemCache();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.deleteWorkspace>) => {
return driver.deleteWorkspace(...payload);
mutationFn: (...payload: Parameters<typeof driver.createFavoriteItem>) => {
return driver.createFavoriteItem(...payload);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["items"],
});
onSuccess: (_, itemId: string) => {
refreshFavoriteCache(itemId, true);
refreshItemCache(itemId, { is_favorite: true });
},
});
};
export const useMutationCreateAccess = () => {
export const useMutationDeleteFavoriteItem = () => {
const driver = getDriver();
const treeContext = useTreeContext();
const removeItems = useRemoveItemsFromPaginatedList();
const refreshFavoriteCache = useRefreshFavoriteCache();
const refreshItemCache = useRefreshItemCache();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.createAccess>) => {
return driver.createAccess(...payload);
},
});
};
export const useMutationCreateInvitation = () => {
const driver = getDriver();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.createInvitation>) => {
return driver.createInvitation(...payload);
},
});
};
export const useMutationUpdateInvitation = () => {
const driver = getDriver();
const queryClient = useQueryClient();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.updateInvitation>) => {
return driver.updateInvitation(...payload);
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["itemInvitations", variables.itemId],
});
},
});
};
export const useMutationUpdateAccess = () => {
const driver = getDriver();
const queryClient = useQueryClient();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.updateAccess>) => {
return driver.updateAccess(...payload);
},
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: ["itemAccesses", variables.itemId],
});
},
});
};
export const useMutationDeleteAccess = () => {
const driver = getDriver();
const queryClient = useQueryClient();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.deleteAccess>) => {
return driver.deleteAccess(...payload);
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["itemAccesses", variables.itemId],
});
},
});
};
export const useMutationDeleteInvitation = () => {
const driver = getDriver();
const queryClient = useQueryClient();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.deleteInvitation>) => {
return driver.deleteInvitation(...payload);
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["itemInvitations", variables.itemId],
});
mutationFn: (...payload: Parameters<typeof driver.deleteFavoriteItem>) => {
return driver.deleteFavoriteItem(...payload);
},
onSuccess: (_data, itemId: string) => {
// Only delete the root favorite node (directly under favorites)
// Children of opened favorite folders should remain visible
const rootFavoriteTreeId = generateTreeId(
itemId,
DefaultRoute.FAVORITES,
true,
);
treeContext?.treeData.deleteNode(rootFavoriteTreeId);
removeItems(
["items", "infinite", JSON.stringify({ is_favorite: true })],
[itemId],
);
refreshItemCache(itemId, { is_favorite: false });
refreshFavoriteCache(itemId, false);
},
});
};
@@ -0,0 +1,87 @@
import { getDriver } from "@/features/config/Config";
import { useMutation } from "@tanstack/react-query";
import { useOnSuccessAccessOrInvitationMutation } from "./useRefreshItems";
// ============================================================================
// ACCESS & INVITATION MUTATIONS
// ============================================================================
export const useMutationCreateAccess = () => {
const driver = getDriver();
const onSuccessAccessOrInvitation = useOnSuccessAccessOrInvitationMutation();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.createAccess>) => {
return driver.createAccess(...payload);
},
onSuccess: (_, variables) => {
onSuccessAccessOrInvitation(variables.itemId, false);
},
});
};
export const useMutationCreateInvitation = () => {
const driver = getDriver();
const onSuccessAccessOrInvitation = useOnSuccessAccessOrInvitationMutation();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.createInvitation>) => {
return driver.createInvitation(...payload);
},
onSuccess: (_, variables) => {
onSuccessAccessOrInvitation(variables.itemId, true);
},
});
};
export const useMutationUpdateInvitation = () => {
const driver = getDriver();
const onSuccessAccessOrInvitation = useOnSuccessAccessOrInvitationMutation();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.updateInvitation>) => {
return driver.updateInvitation(...payload);
},
onSuccess: (_, variables) => {
onSuccessAccessOrInvitation(variables.itemId, true);
},
});
};
export const useMutationUpdateAccess = () => {
const driver = getDriver();
const onSuccessAccessOrInvitation = useOnSuccessAccessOrInvitationMutation();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.updateAccess>) => {
return driver.updateAccess(...payload);
},
onSuccess: (_data, variables) => {
onSuccessAccessOrInvitation(variables.itemId, false);
},
});
};
export const useMutationDeleteAccess = () => {
const driver = getDriver();
const onSuccessAccessOrInvitation = useOnSuccessAccessOrInvitationMutation();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.deleteAccess>) => {
return driver.deleteAccess(...payload);
},
onSuccess: (_, variables) => {
onSuccessAccessOrInvitation(variables.itemId, false);
},
});
};
export const useMutationDeleteInvitation = () => {
const driver = getDriver();
const onSuccessAccessOrInvitation = useOnSuccessAccessOrInvitationMutation();
return useMutation({
mutationFn: (...payload: Parameters<typeof driver.deleteInvitation>) => {
return driver.deleteInvitation(...payload);
},
onSuccess: (_, variables) => {
onSuccessAccessOrInvitation(variables.itemId, true);
},
});
};
@@ -0,0 +1,235 @@
import { Item } from "@/features/drivers/types";
import { PaginatedChildrenResult } from "@gouvfr-lasuite/ui-kit";
import { QueryClient, QueryKey, useQueryClient } from "@tanstack/react-query";
/**
* Adds an item to the top of the first page of a paginated infinite query list.
* This function finds all queries matching the queryKey pattern and updates them
* by prepending the new item to the first page.
*
* @param queryClient - The react-query QueryClient instance
* @param queryKey - The query key pattern to match (can be partial)
* @param newItem - The item to add at the top of the list
*/
export const addItemToTopOfPaginatedList = (
queryClient: QueryClient,
queryKey: QueryKey,
newItem: Item
): void => {
// Get all queries matching the queryKey pattern
const queriesData = queryClient.getQueriesData({
queryKey,
});
queriesData.forEach((query) => {
const key = query[0];
const data = query[1] as
| { pages: PaginatedChildrenResult<Item>[] }
| undefined;
if (!data || !data.pages || data.pages.length === 0) {
return;
}
// Deep clone to avoid mutating the original data
const updatedData: { pages: PaginatedChildrenResult<Item>[] } = JSON.parse(
JSON.stringify(data)
);
// Add the new item to the top of the first page
if (updatedData.pages[0]) {
// Check if item already exists to avoid duplicates
const itemExists = updatedData.pages.some((page) =>
page.children?.some((child) => child.id === newItem.id)
);
if (!itemExists) {
updatedData.pages[0].children = [
newItem,
...(updatedData.pages[0].children || []),
];
}
}
// Update the query data
queryClient.setQueryData(key, updatedData);
});
};
/**
* Removes items from a paginated infinite query list.
* This function finds all queries matching the queryKey pattern and updates them
* by filtering out the items with the specified IDs from all pages.
*
* @param queryClient - The react-query QueryClient instance
* @param queryKey - The query key pattern to match (can be partial)
* @param itemIds - The IDs of items to remove from the list
*/
export const removeItemsFromPaginatedList = (
queryClient: QueryClient,
queryKey: QueryKey,
itemIds: string[]
): void => {
// Get all queries matching the queryKey pattern
const queriesData = queryClient.getQueriesData({
queryKey,
});
queriesData.forEach((query) => {
const key = query[0];
const data = query[1] as
| { pages: PaginatedChildrenResult<Item>[] }
| undefined;
if (!data || !data.pages || data.pages.length === 0) {
return;
}
// Deep clone to avoid mutating the original data
const updatedData: { pages: PaginatedChildrenResult<Item>[] } = JSON.parse(
JSON.stringify(data)
);
// Remove items from all pages
updatedData.pages.forEach((page) => {
page.children = page.children?.filter(
(child) => !itemIds.includes(child.id)
);
});
// Update the query data
queryClient.setQueryData(key, updatedData);
});
};
/**
* Hook that returns a function to add an item to the top of a paginated list.
* This is a convenience hook that provides access to the queryClient.
*
* @example
* ```tsx
* const addItemToTop = useAddItemToPaginatedList();
*
* // In a mutation's onSuccess callback:
* onSuccess: (newItem) => {
* addItemToTop(
* ["items", parentId, "children", "infinite"],
* newItem
* );
* }
* ```
*
* @returns A function that takes a queryKey and an item, and adds the item to the top of the list
*/
export const useAddItemToPaginatedList = () => {
const queryClient = useQueryClient();
return (queryKey: QueryKey, newItem: Item) => {
addItemToTopOfPaginatedList(queryClient, queryKey, newItem);
};
};
/**
* Hook that returns a function to remove items from a paginated list.
* This is a convenience hook that provides access to the queryClient.
*
* @example
* ```tsx
* const removeItems = useRemoveItemsFromPaginatedList();
*
* // In a mutation's onSuccess callback:
* onSuccess: () => {
* removeItems(
* ["items", parentId, "children", "infinite"],
* ["item-id-1", "item-id-2"]
* );
* }
* ```
*
* @returns A function that takes a queryKey and item IDs, and removes those items from the list
*/
export const useRemoveItemsFromPaginatedList = () => {
const queryClient = useQueryClient();
return (queryKey: QueryKey, itemIds: string[]) => {
removeItemsFromPaginatedList(queryClient, queryKey, itemIds);
};
};
/**
* Updates a partial item in a paginated infinite query list.
* This function finds all queries matching the queryKey pattern and updates them
* by merging the partial update with the existing item in all pages.
*
* @param queryClient - The react-query QueryClient instance
* @param queryKey - The query key pattern to match (can be partial)
* @param itemId - The ID of the item to update
* @param partialUpdate - The partial item data to merge with the existing item
*/
export const updateItemInPaginatedList = (
queryClient: QueryClient,
queryKey: QueryKey,
itemId: string,
partialUpdate: Partial<Item>
): void => {
// Get all queries matching the queryKey pattern
const queriesData = queryClient.getQueriesData({
queryKey,
});
queriesData.forEach((query) => {
const key = query[0];
const data = query[1] as
| { pages: PaginatedChildrenResult<Item>[] }
| undefined;
if (!data || !data.pages || data.pages.length === 0) {
return;
}
// Deep clone to avoid mutating the original data
const updatedData: { pages: PaginatedChildrenResult<Item>[] } = JSON.parse(
JSON.stringify(data)
);
// Update item in all pages
updatedData.pages.forEach((page) => {
if (page.children) {
page.children = page.children.map((child) =>
child.id === itemId ? { ...child, ...partialUpdate } : child
);
}
});
// Update the query data
queryClient.setQueryData(key, updatedData);
});
};
/**
* Hook that returns a function to update a partial item in a paginated list.
* This is a convenience hook that provides access to the queryClient.
*
* @example
* ```tsx
* const updateItem = useUpdateItemInPaginatedList();
*
* // In a mutation's onSuccess callback:
* onSuccess: (updatedItem) => {
* updateItem(
* ["items", parentId, "children", "infinite"],
* updatedItem.id,
* { title: updatedItem.title, is_favorite: updatedItem.is_favorite }
* );
* }
* ```
*
* @returns A function that takes a queryKey, itemId, and partial update, and updates the item in the list
*/
export const useUpdateItemInPaginatedList = () => {
const queryClient = useQueryClient();
return (queryKey: QueryKey, itemId: string, partialUpdate: Partial<Item>) => {
updateItemInPaginatedList(queryClient, queryKey, itemId, partialUpdate);
};
};
@@ -1,15 +1,28 @@
import { APIError } from "@/features/api/APIError";
import { getDriver } from "@/features/config/Config";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { ItemFilters } from "@/features/drivers/Driver";
import { Item } from "@/features/drivers/types";
import { HookUseQueryOptions } from "@/utils/useQueries";
import {
useInfiniteQuery,
useQuery,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
export const useInfiniteItemAccesses = (itemId: string) => {
const driver = getDriver();
return useInfiniteQuery({
export const useFavoriteItems = () => {
return useQuery({
queryKey: ["items", "favorites"],
queryFn: () => getDriver().getFavoriteItems(),
});
};
export const useItemAccesses = (itemId: string) => {
return useQuery({
queryKey: ["itemAccesses", itemId],
queryFn: () => driver.getItemAccesses(itemId),
initialPageParam: 1,
getNextPageParam(lastPage, allPages) {
return lastPage.next ? allPages.length + 1 : undefined;
},
queryFn: () => getDriver().getItemAccesses(itemId),
staleTime: 0,
gcTime: 0,
});
};
@@ -41,7 +54,21 @@ export const useItems = () => {
});
};
export const getRootItems = async () => {
const result = await getDriver().getItems();
export const getRootItems = async (filters?: ItemFilters) => {
const result = await getDriver().getItems(filters);
return result.children;
};
export const useItem = (
itemId: string,
options?: HookUseQueryOptions<Item>,
): UseQueryResult<Item | undefined, APIError> => {
return useQuery<Item, APIError>({
queryKey: ["items", itemId],
queryFn: () => getDriver().getItem(itemId),
...(options as Omit<
UseQueryOptions<Item, APIError>,
"queryKey" | "queryFn"
>),
});
};
@@ -0,0 +1,165 @@
import { useQueryClient, QueryKey } from "@tanstack/react-query";
import { Item } from "@/features/drivers/types";
import {
useRemoveItemsFromPaginatedList,
useUpdateItemInPaginatedList,
} from "./useOptimisticPagination";
import { useTreeContext } from "@gouvfr-lasuite/ui-kit";
import { DefaultRoute } from "@/utils/defaultRoutes";
import { generateTreeId } from "../components/GlobalExplorerContext";
export const useGetQueryKeyToRefresh = () => {
return (parentId?: string) => {
const queryKeys = [["items", "infinite"]];
if (parentId) {
queryKeys.push(["items", parentId, "children"]);
}
// let queryKey = parentId ? ["items", parentId, "children"] : [];
// if (queryKeyForRoute.length > 0) {
// queryKey = queryKeyForRoute;
// }
return queryKeys;
};
};
export const useRefreshQueryCacheAfterMutation = () => {
const queryClient = useQueryClient();
const getQueryKey = useGetQueryKeyToRefresh();
return (parentId?: string) => {
const queryKey = getQueryKey(parentId);
for (const key of queryKey) {
queryClient.invalidateQueries({
queryKey: key,
});
}
};
};
export const useDeleteMutationCallbacks = (
parentId?: string,
defaultQueryKey?: string[][],
) => {
const queryClient = useQueryClient();
const getQueryKey = useGetQueryKeyToRefresh();
const removeItems = useRemoveItemsFromPaginatedList();
const queryKeys = defaultQueryKey ?? getQueryKey(parentId);
const onMutate = async (itemIds: string[]) => {
const returnPreviousItems: Map<string[], Item[]> = new Map();
queryKeys.forEach(async (key) => {
await queryClient.cancelQueries({
queryKey: key,
});
const previousItems = queryClient.getQueryData<Item[]>(key);
returnPreviousItems.set(key, previousItems ?? []);
removeItems(key, itemIds);
});
return { previousItems: returnPreviousItems };
};
const onError = (_err: unknown, _variables: unknown, context: unknown) => {
const returnPreviousItems = context as {
previousItems: Map<string[], Item[]>;
};
returnPreviousItems.previousItems.forEach((previousItems, key) => {
queryClient.setQueryData(key, previousItems);
});
};
const onSuccess = () => {
if (queryKeys.length === 0) {
return;
}
queryClient.invalidateQueries({
queryKey: queryKeys,
});
};
return { onMutate, onError, onSuccess };
};
// Explanation:
// The function below is used to refresh the cache for certain queries after a mutation (creation, deletion, update)
// on items/files in the explorer. It takes as an argument the id of the parent whose list of children needs to be refreshed.
// It uses the QueryClient from react-query to force reloading/invalidating queries associated with the parent key:
// - This prevents the UI from becoming out of sync with the backend state after a mutation.
export const useRefreshItemCache = () => {
const queryClient = useQueryClient();
const updateItemInPaginatedList = useUpdateItemInPaginatedList();
return async (
itemId: string,
partialUpdate?: Partial<Item>,
moreQueriesToInvalidate?: QueryKey[],
) => {
if (partialUpdate) {
updateItemInPaginatedList(["items"], itemId, partialUpdate);
queryClient.setQueryData(["item", itemId], (old: Item) => {
return {
...old,
...partialUpdate,
};
});
} else {
queryClient.invalidateQueries({
queryKey: ["items"],
});
queryClient.invalidateQueries({
queryKey: ["item", itemId],
});
moreQueriesToInvalidate?.forEach((queryKey) => {
queryClient.invalidateQueries({
queryKey,
});
});
}
};
};
export const useOnSuccessAccessOrInvitationMutation = () => {
const queryClient = useQueryClient();
const refreshItemCache = useRefreshItemCache();
return (itemId: string, isInvitation: boolean = false) => {
refreshItemCache(itemId);
queryClient.invalidateQueries({
queryKey: ["items", itemId, "children"],
});
if (isInvitation) {
queryClient.invalidateQueries({
queryKey: ["itemInvitations", itemId],
});
} else {
queryClient.invalidateQueries({
queryKey: ["itemAccesses", itemId],
});
}
};
};
export const useRefreshFavoriteCache = () => {
const queryClient = useQueryClient();
const treeContext = useTreeContext();
return (itemId: string, isFavorite: boolean) => {
const moreQueriesToInvalidate: QueryKey[] = [
["items", "infinite", JSON.stringify({ is_favorite: isFavorite })],
["item", itemId],
];
const rootFavoriteTreeId = generateTreeId(
itemId,
DefaultRoute.FAVORITES,
true,
);
treeContext?.treeData.deleteNode(rootFavoriteTreeId);
queryClient.invalidateQueries({
queryKey: moreQueriesToInvalidate,
});
};
};