feat: boards with statuses fetch

This commit is contained in:
2025-08-03 13:40:09 +04:00
parent 624c94155c
commit 5435750fb5
21 changed files with 265 additions and 106 deletions

View File

@ -1,6 +1,6 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Box } from "@mantine/core"; import { Box } from "@mantine/core";
import { BoardSchema } from "@/types/BoardSchema"; import { BoardSchema } from "@/client";
type Props = { type Props = {
board: BoardSchema; board: BoardSchema;

View File

@ -4,11 +4,11 @@ import React from "react";
import { ScrollArea } from "@mantine/core"; import { ScrollArea } from "@mantine/core";
import Board from "@/app/deals/components/Board/Board"; import Board from "@/app/deals/components/Board/Board";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import { BoardSchema } from "@/client";
import SortableDnd from "@/components/SortableDnd"; import SortableDnd from "@/components/SortableDnd";
import { BoardSchema } from "@/types/BoardSchema";
const Boards = () => { const Boards = () => {
const { boards } = useBoardsContext(); const { boards, setSelectedBoard } = useBoardsContext();
const renderBoard = (board: BoardSchema) => { const renderBoard = (board: BoardSchema) => {
return <Board board={board} />; return <Board board={board} />;
@ -18,6 +18,11 @@ const Boards = () => {
console.log("onDragEnd:", itemId, newLexorank); console.log("onDragEnd:", itemId, newLexorank);
}; };
const selectBoard = (board: BoardSchema) => {
console.log("Board selecting:", board);
setSelectedBoard(board);
};
return ( return (
<ScrollArea <ScrollArea
offsetScrollbars={"x"} offsetScrollbars={"x"}
@ -28,6 +33,7 @@ const Boards = () => {
initialItems={boards} initialItems={boards}
renderItem={renderBoard} renderItem={renderBoard}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
onItemClick={selectBoard}
rowStyle={{ flexWrap: "nowrap" }} rowStyle={{ flexWrap: "nowrap" }}
/> />
</ScrollArea> </ScrollArea>

View File

@ -4,7 +4,7 @@ import DealCard from "@/app/deals/components/DealCard/DealCard";
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn"; import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext"; import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import { DealSchema } from "@/types/DealSchema"; import { DealSchema } from "@/types/DealSchema";
import { StatusSchema } from "@/types/StatusSchema"; import { StatusSchema } from "@/client";
type Props = { type Props = {
activeDeal: DealSchema | null; activeDeal: DealSchema | null;

View File

@ -6,8 +6,8 @@ import {
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { Box, Stack, Text } from "@mantine/core"; import { Box, Stack, Text } from "@mantine/core";
import DealContainer from "@/app/deals/components/DealContainer/DealContainer"; import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
import { StatusSchema } from "@/client";
import { DealSchema } from "@/types/DealSchema"; import { DealSchema } from "@/types/DealSchema";
import { StatusSchema } from "@/types/StatusSchema";
import { sortByLexorank } from "@/utils/lexorank"; import { sortByLexorank } from "@/utils/lexorank";
type Props = { type Props = {

View File

@ -8,8 +8,8 @@ import React, {
useState, useState,
} from "react"; } from "react";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext"; import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import useBoards from "@/app/deals/hooks/useBoards"; import { BoardSchema } from "@/client";
import { BoardSchema } from "@/types/BoardSchema"; import useBoardsList from "@/hooks/useBoardsList";
type BoardsContextState = { type BoardsContextState = {
boards: BoardSchema[]; boards: BoardSchema[];
@ -21,11 +21,11 @@ type BoardsContextState = {
const BoardsContext = createContext<BoardsContextState | undefined>(undefined); const BoardsContext = createContext<BoardsContextState | undefined>(undefined);
const useBoardsContextState = () => { const useBoardsContextState = () => {
const { boards, setBoards } = useBoards(); const { selectedProject: project } = useProjectsContext();
const { boards, setBoards } = useBoardsList({ projectId: project?.id });
const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>( const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>(
null null
); );
const { selectedProject: project } = useProjectsContext();
useEffect(() => { useEffect(() => {
if (boards.length > 0 && selectedBoard === null) { if (boards.length > 0 && selectedBoard === null) {

View File

@ -1,11 +1,16 @@
"use client"; "use client";
import React, { createContext, FC, useContext } from "react"; import React, {
createContext,
FC,
useContext,
useEffect,
useState,
} from "react";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import useDeals from "@/app/deals/hooks/useDeals"; import useDeals from "@/app/deals/hooks/useDeals";
import useStatuses from "@/app/deals/hooks/useStatuses"; import { StatusSchema } from "@/client";
import { DealSchema } from "@/types/DealSchema"; import { DealSchema } from "@/types/DealSchema";
import { StatusSchema } from "@/types/StatusSchema";
type StatusesContextState = { type StatusesContextState = {
statuses: StatusSchema[]; statuses: StatusSchema[];
@ -19,10 +24,14 @@ const StatusesContext = createContext<StatusesContextState | undefined>(
); );
const useStatusesContextState = () => { const useStatusesContextState = () => {
const { statuses, setStatuses } = useStatuses(); const [statuses, setStatuses] = useState<StatusSchema[]>([]);
const { deals, setDeals } = useDeals(); const { deals, setDeals } = useDeals();
const { selectedBoard } = useBoardsContext(); const { selectedBoard } = useBoardsContext();
useEffect(() => {
setStatuses(selectedBoard?.statuses ?? []);
}, [selectedBoard]);
return { return {
statuses, statuses,
setStatuses, setStatuses,

View File

@ -1,19 +1,9 @@
import { useEffect, useState } from "react"; import { useState } from "react";
import { BoardSchema } from "@/types/BoardSchema"; import { BoardSchema } from "@/client";
const useBoards = () => { const useBoards = () => {
const [boards, setBoards] = useState<BoardSchema[]>([]); const [boards, setBoards] = useState<BoardSchema[]>([]);
useEffect(() => {
setBoards([
{ id: 1, name: "1 Item", rank: "0|aaaaaa:" },
{ id: 2, name: "2 Item", rank: "0|gggggg:" },
{ id: 3, name: "3 Item", rank: "0|mmmmmm:" },
{ id: 4, name: "4 Item", rank: "0|ssssss:" },
{ id: 5, name: "5 Item", rank: "0|zzzzzz:" },
]);
}, []);
return { return {
boards, boards,
setBoards, setBoards,

View File

@ -9,19 +9,19 @@ const useDeals = () => {
{ {
id: 1, id: 1,
name: "Deal 1", name: "Deal 1",
rank: "0|gggggg:", lexorank: "0|gggggg:",
statusId: 1, statusId: 1,
}, },
{ {
id: 2, id: 2,
name: "Deal 2", name: "Deal 2",
rank: "0|mmmmmm:", lexorank: "0|mmmmmm:",
statusId: 1, statusId: 1,
}, },
{ {
id: 3, id: 3,
name: "Deal 3", name: "Deal 3",
rank: "0|ssssss:", lexorank: "0|ssssss:",
statusId: 2, statusId: 2,
}, },
]; ];

View File

@ -4,8 +4,8 @@ import { throttle } from "lodash";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext"; import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import useGetNewRank from "@/app/deals/hooks/useGetNewRank"; import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId"; import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
import { StatusSchema } from "@/client";
import { DealSchema } from "@/types/DealSchema"; import { DealSchema } from "@/types/DealSchema";
import { StatusSchema } from "@/types/StatusSchema";
import { sortByLexorank } from "@/utils/lexorank"; import { sortByLexorank } from "@/utils/lexorank";
type Props = { type Props = {
@ -73,7 +73,7 @@ const useDnd = (props: Props) => {
? { ? {
...deal, ...deal,
statusId: overStatusId, statusId: overStatusId,
rank: newLexorank || deal.rank, lexorank: newLexorank || deal.lexorank,
} }
: deal : deal
) )
@ -100,7 +100,7 @@ const useDnd = (props: Props) => {
throttledSetStatuses(statuses => throttledSetStatuses(statuses =>
statuses.map(status => statuses.map(status =>
status.id === activeStatusId status.id === activeStatusId
? { ...status, rank: newRank } ? { ...status, lexorank: newRank }
: status : status
) )
); );

View File

@ -20,10 +20,10 @@ const useGetNewRank = () => {
: [overDealIndex, overDealIndex + 1]; : [overDealIndex, overDealIndex + 1];
const leftLexorank = const leftLexorank =
leftIndex >= 0 ? LexoRank.parse(deals[leftIndex].rank) : null; leftIndex >= 0 ? LexoRank.parse(deals[leftIndex].lexorank) : null;
const rightLexorank = const rightLexorank =
rightIndex < deals.length rightIndex < deals.length
? LexoRank.parse(deals[rightIndex].rank) ? LexoRank.parse(deals[rightIndex].lexorank)
: null; : null;
return getNewLexorank(leftLexorank, rightLexorank).toString(); return getNewLexorank(leftLexorank, rightLexorank).toString();
@ -35,9 +35,11 @@ const useGetNewRank = () => {
) => { ) => {
const leftLexorank = const leftLexorank =
overDealIndex > 0 overDealIndex > 0
? LexoRank.parse(statusDeals[overDealIndex - 1].rank) ? LexoRank.parse(statusDeals[overDealIndex - 1].lexorank)
: null; : null;
const rightLexorank = LexoRank.parse(statusDeals[overDealIndex].rank); const rightLexorank = LexoRank.parse(
statusDeals[overDealIndex].lexorank
);
return getNewLexorank(leftLexorank, rightLexorank).toString(); return getNewLexorank(leftLexorank, rightLexorank).toString();
}; };
@ -59,10 +61,12 @@ const useGetNewRank = () => {
: [overIndex, overIndex + 1]; : [overIndex, overIndex + 1];
const leftLexorank = const leftLexorank =
leftIndex >= 0 ? LexoRank.parse(statuses[leftIndex].rank) : null; leftIndex >= 0
? LexoRank.parse(statuses[leftIndex].lexorank)
: null;
const rightLexorank = const rightLexorank =
rightIndex < statuses.length rightIndex < statuses.length
? LexoRank.parse(statuses[rightIndex].rank) ? LexoRank.parse(statuses[rightIndex].lexorank)
: null; : null;
return getNewLexorank(leftLexorank, rightLexorank).toString(); return getNewLexorank(leftLexorank, rightLexorank).toString();

View File

@ -1,22 +0,0 @@
import { useEffect, useState } from "react";
import { StatusSchema } from "@/types/StatusSchema";
const useStatuses = () => {
const [statuses, setStatuses] = useState<StatusSchema[]>([]);
useEffect(() => {
setStatuses([
{ id: 1, name: "Todo", rank: "0|aaaaaa:" },
{ id: 2, name: "In progress", rank: "0|gggggg:" },
{ id: 3, name: "Testing", rank: "0|mmmmmm:" },
{ id: 4, name: "Ready", rank: "0|ssssss:" },
]);
}, []);
return {
statuses,
setStatuses,
};
};
export default useStatuses;

View File

@ -2,8 +2,8 @@
import { queryOptions } from "@tanstack/react-query"; import { queryOptions } from "@tanstack/react-query";
import { client as _heyApiClient } from "../client.gen"; import { client as _heyApiClient } from "../client.gen";
import { getProjects, type Options } from "../sdk.gen"; import { getBoards, getProjects, type Options } from "../sdk.gen";
import type { GetProjectsData } from "../types.gen"; import type { GetBoardsData, GetProjectsData } from "../types.gen";
export type QueryKey<TOptions extends Options> = [ export type QueryKey<TOptions extends Options> = [
Pick<TOptions, "baseURL" | "body" | "headers" | "path" | "query"> & { Pick<TOptions, "baseURL" | "body" | "headers" | "path" | "query"> & {
@ -61,3 +61,24 @@ export const getProjectsOptions = (options?: Options<GetProjectsData>) => {
queryKey: getProjectsQueryKey(options), queryKey: getProjectsQueryKey(options),
}); });
}; };
export const getBoardsQueryKey = (options: Options<GetBoardsData>) =>
createQueryKey("getBoards", options);
/**
* Get Boards
*/
export const getBoardsOptions = (options: Options<GetBoardsData>) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getBoards({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: getBoardsQueryKey(options),
});
};

View File

@ -2,7 +2,13 @@
import type { Client, Options as ClientOptions, TDataShape } from "./client"; import type { Client, Options as ClientOptions, TDataShape } from "./client";
import { client as _heyApiClient } from "./client.gen"; import { client as _heyApiClient } from "./client.gen";
import type { GetProjectsData, GetProjectsResponses } from "./types.gen"; import type {
GetBoardsData,
GetBoardsErrors,
GetBoardsResponses,
GetProjectsData,
GetProjectsResponses,
} from "./types.gen";
export type Options< export type Options<
TData extends TDataShape = TDataShape, TData extends TDataShape = TDataShape,
@ -37,3 +43,20 @@ export const getProjects = <ThrowOnError extends boolean = false>(
...options, ...options,
}); });
}; };
/**
* Get Boards
*/
export const getBoards = <ThrowOnError extends boolean = false>(
options: Options<GetBoardsData, ThrowOnError>
) => {
return (options.client ?? _heyApiClient).get<
GetBoardsResponses,
GetBoardsErrors,
ThrowOnError
>({
responseType: "json",
url: "/board/{project_id}",
...options,
});
};

View File

@ -1,5 +1,37 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
/**
* BoardSchema
*/
export type BoardSchema = {
/**
* Name
*/
name: string;
/**
* Id
*/
id: number;
/**
* Lexorank
*/
lexorank: string;
/**
* Statuses
*/
statuses: Array<StatusSchema>;
};
/**
* GetBoardsResponse
*/
export type GetBoardsResponse = {
/**
* Boards
*/
boards: Array<BoardSchema>;
};
/** /**
* GetProjectsResponse * GetProjectsResponse
*/ */
@ -10,6 +42,16 @@ export type GetProjectsResponse = {
projects: Array<ProjectSchema>; projects: Array<ProjectSchema>;
}; };
/**
* HTTPValidationError
*/
export type HttpValidationError = {
/**
* Detail
*/
detail?: Array<ValidationError>;
};
/** /**
* ProjectSchema * ProjectSchema
*/ */
@ -24,6 +66,42 @@ export type ProjectSchema = {
id: number; id: number;
}; };
/**
* StatusSchema
*/
export type StatusSchema = {
/**
* Name
*/
name: string;
/**
* Id
*/
id: number;
/**
* Lexorank
*/
lexorank: string;
};
/**
* ValidationError
*/
export type ValidationError = {
/**
* Location
*/
loc: Array<string | number>;
/**
* Message
*/
msg: string;
/**
* Error Type
*/
type: string;
};
export type GetProjectsData = { export type GetProjectsData = {
body?: never; body?: never;
path?: never; path?: never;
@ -41,6 +119,36 @@ export type GetProjectsResponses = {
export type GetProjectsResponse2 = export type GetProjectsResponse2 =
GetProjectsResponses[keyof GetProjectsResponses]; GetProjectsResponses[keyof GetProjectsResponses];
export type GetBoardsData = {
body?: never;
path: {
/**
* Project Id
*/
project_id: number;
};
query?: never;
url: "/board/{project_id}";
};
export type GetBoardsErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetBoardsError = GetBoardsErrors[keyof GetBoardsErrors];
export type GetBoardsResponses = {
/**
* Successful Response
*/
200: GetBoardsResponse;
};
export type GetBoardsResponse2 = GetBoardsResponses[keyof GetBoardsResponses];
export type ClientOptions = { export type ClientOptions = {
baseURL: "http://localhost:8000" | (string & {}); baseURL: "http://localhost:8000" | (string & {});
}; };

View File

@ -7,34 +7,25 @@ import React, {
useMemo, useMemo,
useState, useState,
} from "react"; } from "react";
import { import { Active, DndContext, DragEndEvent } from "@dnd-kit/core";
Active, import { SortableContext } from "@dnd-kit/sortable";
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
SortableContext,
sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import { LexoRank } from "lexorank"; import { LexoRank } from "lexorank";
import { Group } from "@mantine/core"; import { Box, Group } from "@mantine/core";
import useDndSensors from "@/app/deals/hooks/useSensors";
import { SortableItem } from "@/components/SortableDnd/SortableItem"; import { SortableItem } from "@/components/SortableDnd/SortableItem";
import { SortableOverlay } from "@/components/SortableDnd/SortableOverlay"; import { SortableOverlay } from "@/components/SortableDnd/SortableOverlay";
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank"; import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
type BaseItem = { type BaseItem = {
id: number; id: number;
rank: string; lexorank: string;
}; };
type Props<T extends BaseItem> = { type Props<T extends BaseItem> = {
initialItems: T[]; initialItems: T[];
renderItem: (item: T) => ReactNode; renderItem: (item: T) => ReactNode;
onDragEnd: (itemId: number, newLexorank: string) => void; onDragEnd: (itemId: number, newLexorank: string) => void;
onItemClick: (item: T) => void;
rowStyle?: CSSProperties; rowStyle?: CSSProperties;
itemStyle?: CSSProperties; itemStyle?: CSSProperties;
}; };
@ -43,6 +34,7 @@ const SortableDnd = <T extends BaseItem>({
initialItems, initialItems,
renderItem, renderItem,
onDragEnd, onDragEnd,
onItemClick,
rowStyle, rowStyle,
itemStyle, itemStyle,
}: Props<T>) => { }: Props<T>) => {
@ -57,12 +49,7 @@ const SortableDnd = <T extends BaseItem>({
setItems(sortByLexorank(initialItems)); setItems(sortByLexorank(initialItems));
}, [initialItems]); }, [initialItems]);
const sensors = useSensors( const sensors = useDndSensors();
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const onDragEndLocal = ({ active, over }: DragEndEvent) => { const onDragEndLocal = ({ active, over }: DragEndEvent) => {
if (over && active.id !== over?.id && activeItem) { if (over && active.id !== over?.id && activeItem) {
@ -81,10 +68,12 @@ const SortableDnd = <T extends BaseItem>({
} }
const leftLexorank: LexoRank | null = const leftLexorank: LexoRank | null =
leftIndex >= 0 ? LexoRank.parse(items[leftIndex].rank) : null; leftIndex >= 0
? LexoRank.parse(items[leftIndex].lexorank)
: null;
const rightLexorank: LexoRank | null = const rightLexorank: LexoRank | null =
rightIndex < items.length rightIndex < items.length
? LexoRank.parse(items[rightIndex].rank) ? LexoRank.parse(items[rightIndex].lexorank)
: null; : null;
const newLexorank = getNewLexorank( const newLexorank = getNewLexorank(
@ -92,7 +81,7 @@ const SortableDnd = <T extends BaseItem>({
rightLexorank rightLexorank
).toString(); ).toString();
items[activeIndex].rank = newLexorank; items[activeIndex].lexorank = newLexorank;
onDragEnd(items[activeIndex].id, newLexorank); onDragEnd(items[activeIndex].id, newLexorank);
const sortedItems = sortByLexorank(items); const sortedItems = sortByLexorank(items);
setItems([...sortedItems]); setItems([...sortedItems]);
@ -112,12 +101,19 @@ const SortableDnd = <T extends BaseItem>({
style={rowStyle} style={rowStyle}
role="application"> role="application">
{items.map((item, index) => ( {items.map((item, index) => (
<SortableItem <Box
key={index} key={index}
onClick={e => {
e.preventDefault();
e.stopPropagation();
onItemClick(item);
}}>
<SortableItem
itemStyle={itemStyle} itemStyle={itemStyle}
id={item.id}> id={item.id}>
{renderItem(item)} {renderItem(item)}
</SortableItem> </SortableItem>
</Box>
))} ))}
</Group> </Group>
</SortableContext> </SortableContext>

View File

@ -9,7 +9,11 @@ type Props = {
itemStyle?: CSSProperties; itemStyle?: CSSProperties;
}; };
export const SortableItem = ({ children, id }: PropsWithChildren<Props>) => { export const SortableItem = ({
children,
itemStyle,
id,
}: PropsWithChildren<Props>) => {
const { const {
attributes, attributes,
isDragging, isDragging,
@ -33,6 +37,7 @@ export const SortableItem = ({ children, id }: PropsWithChildren<Props>) => {
opacity: isDragging ? 0.4 : undefined, opacity: isDragging ? 0.4 : undefined,
transform: CSS.Translate.toString(transform), transform: CSS.Translate.toString(transform),
transition, transition,
...itemStyle,
}; };
return ( return (

View File

@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { BoardSchema } from "@/client";
import { getBoardsOptions } from "@/client/@tanstack/react-query.gen";
type Props = {
projectId?: number;
};
const useBoardsList = ({ projectId }: Props) => {
const [boards, setBoards] = useState<BoardSchema[]>([]);
const { data, refetch, isLoading } = useQuery({
...getBoardsOptions({ path: { project_id: projectId! } }),
enabled: projectId !== undefined,
});
useEffect(() => {
if (projectId === undefined) {
setBoards([]);
} else if (data?.boards) {
setBoards(data.boards);
}
}, [data?.boards, projectId]);
return { boards, setBoards, refetch, isLoading };
};
export default useBoardsList;

View File

@ -1,5 +0,0 @@
export type BoardSchema = {
id: number;
name: string;
rank: string;
};

View File

@ -1,6 +1,6 @@
export type DealSchema = { export type DealSchema = {
id: number; id: number;
name: string; name: string;
rank: string; lexorank: string;
statusId: number; statusId: number;
}; };

View File

@ -1,5 +0,0 @@
export type StatusSchema = {
id: number;
name: string;
rank: string;
};

View File

@ -1,17 +1,17 @@
import { LexoRank } from "lexorank"; import { LexoRank } from "lexorank";
type LexorankSortable = { type LexorankSortable = {
rank: string; lexorank: string;
}; };
export function compareByLexorank<T extends LexorankSortable>( export function compareByLexorank<T extends LexorankSortable>(
a: T, a: T,
b: T b: T
): -1 | 1 | 0 { ): -1 | 1 | 0 {
if (a.rank < b.rank) { if (a.lexorank < b.lexorank) {
return -1; return -1;
} }
if (a.rank > b.rank) { if (a.lexorank > b.lexorank) {
return 1; return 1;
} }
return 0; return 0;