Compare commits
1 Commits
3d213cb0d9
...
detached
| Author | SHA1 | Date | |
|---|---|---|---|
| 095f3dae76 |
@ -1,21 +0,0 @@
|
||||
.storybook
|
||||
tests
|
||||
__tests__
|
||||
*.spec.ts
|
||||
*.test.ts
|
||||
node_modules
|
||||
.yarn/cache
|
||||
.eslint
|
||||
.prettier
|
||||
.stylelint
|
||||
.env
|
||||
.idea
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
README.md
|
||||
*.log
|
||||
test
|
||||
docs
|
||||
coverage
|
||||
*.map
|
||||
@ -1 +1 @@
|
||||
NEXT_PUBLIC_API_URL=http://your.api/api
|
||||
NEXT_PUBLIC_API_URL=http://api.example.com/api
|
||||
48
Dockerfile
48
Dockerfile
@ -1,48 +0,0 @@
|
||||
FROM node:lts-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
RUN corepack enable
|
||||
COPY .yarn ./.yarn
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .yarnrc.yml ./
|
||||
|
||||
RUN yarn && rm -rf .yarn/cache .yarn/unplugged .yarn/build-state.yml
|
||||
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN yarn build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
CMD ["node", "server.js"]
|
||||
@ -7,13 +7,11 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client"
|
||||
"generate-client": "openapi-ts && prettier --write ./src/client/**/*.ts && git add ./src/client"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@mantine/carousel": "^8.2.4",
|
||||
"@mantine/core": "8.1.2",
|
||||
"@mantine/form": "^8.1.3",
|
||||
"@mantine/hooks": "8.1.2",
|
||||
@ -26,8 +24,6 @@
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"embla-carousel": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.7",
|
||||
"i18n-iso-countries": "^7.14.0",
|
||||
"lexorank": "^1.0.5",
|
||||
@ -60,9 +56,7 @@
|
||||
"@types/node": "^22.13.11",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"@types/react-slick": "^0",
|
||||
"@types/redux-persist": "^4.3.1",
|
||||
"@types/slick-carousel": "^1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-loader": "^10.0.0",
|
||||
"eslint": "^9.29.0",
|
||||
|
||||
13
src/app/deals/components/Board/Board.tsx
Normal file
13
src/app/deals/components/Board/Board.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React, { FC } from "react";
|
||||
import { Box } from "@mantine/core";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
board: BoardSchema;
|
||||
};
|
||||
|
||||
const Board: FC<Props> = ({ board }) => {
|
||||
return <Box miw={100} style={{ borderWidth: 1, margin: 0 }}>{board.name}</Box>;
|
||||
};
|
||||
|
||||
export default Board;
|
||||
65
src/app/deals/components/Boards/Boards.tsx
Normal file
65
src/app/deals/components/Boards/Boards.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ScrollArea } from "@mantine/core";
|
||||
import Board from "@/app/deals/components/Board/Board";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
import { updateBoardMutation } from "@/lib/client/@tanstack/react-query.gen";
|
||||
import SortableDnd from "@/components/SortableDnd";
|
||||
import { notifications } from "@/lib/notifications";
|
||||
|
||||
const Boards = () => {
|
||||
const { boards, setSelectedBoard, refetchBoards } = useBoardsContext();
|
||||
|
||||
const updateBoard = useMutation({
|
||||
...updateBoardMutation(),
|
||||
onError: error => {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: error.response?.data?.detail as string | undefined,
|
||||
});
|
||||
refetchBoards();
|
||||
},
|
||||
});
|
||||
|
||||
const renderBoard = (board: BoardSchema) => {
|
||||
return <Board board={board} />;
|
||||
};
|
||||
|
||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
||||
updateBoard.mutate({
|
||||
path: {
|
||||
boardId: itemId,
|
||||
},
|
||||
body: {
|
||||
board: {
|
||||
lexorank: newLexorank,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const selectBoard = (board: BoardSchema) => {
|
||||
setSelectedBoard(board);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
offsetScrollbars={"x"}
|
||||
scrollbars={"x"}
|
||||
scrollbarSize={0}
|
||||
w={"100%"}>
|
||||
<SortableDnd
|
||||
initialItems={boards}
|
||||
renderItem={renderBoard}
|
||||
onDragEnd={onDragEnd}
|
||||
onItemClick={selectBoard}
|
||||
rowStyle={{ flexWrap: "nowrap" }}
|
||||
/>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export default Boards;
|
||||
@ -1,13 +1,12 @@
|
||||
import { Card } from "@mantine/core";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
import styles from "./DealCard.module.css";
|
||||
|
||||
type Props = {
|
||||
deal: DealSchema;
|
||||
};
|
||||
|
||||
const DealCard = ({ deal }: Props) => {
|
||||
return <Card className={styles.container}>{deal.name}</Card>;
|
||||
return <Card>{deal.name}</Card>;
|
||||
};
|
||||
|
||||
export default DealCard;
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { FC, useMemo } from "react";
|
||||
import { Box } from "@mantine/core";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import SortableItem from "@/components/dnd/SortableItem";
|
||||
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||
|
||||
type Props = {
|
||||
deal: DealSchema;
|
||||
@ -15,9 +15,9 @@ const DealContainer: FC<Props> = ({ deal }) => {
|
||||
<Box>
|
||||
<SortableItem
|
||||
dragHandleStyle={{ cursor: "pointer" }}
|
||||
id={deal.id}
|
||||
renderItem={() => dealBody}
|
||||
/>
|
||||
id={deal.id}>
|
||||
{dealBody}
|
||||
</SortableItem>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
36
src/app/deals/components/DndOverlay/DndOverlay.tsx
Normal file
36
src/app/deals/components/DndOverlay/DndOverlay.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/core";
|
||||
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
activeDeal: DealSchema | null;
|
||||
activeStatus: StatusSchema | null;
|
||||
};
|
||||
|
||||
const DndOverlay = ({ activeStatus, activeDeal }: Props) => {
|
||||
const { deals } = useStatusesContext();
|
||||
|
||||
return (
|
||||
<DragOverlay dropAnimation={defaultDropAnimation}>
|
||||
<div style={{ cursor: "grabbing" }}>
|
||||
{activeDeal ? (
|
||||
<DealCard deal={activeDeal} />
|
||||
) : activeStatus ? (
|
||||
<StatusColumn
|
||||
id={`${activeStatus.id}-status`}
|
||||
status={activeStatus}
|
||||
deals={deals.filter(
|
||||
deal => deal.statusId === activeStatus.id
|
||||
)}
|
||||
isDragging
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</DragOverlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default DndOverlay;
|
||||
24
src/app/deals/components/Header/Header.tsx
Normal file
24
src/app/deals/components/Header/Header.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { Group } from "@mantine/core";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
||||
|
||||
const Header = () => {
|
||||
const { projects, setSelectedProject, selectedProject } =
|
||||
useProjectsContext();
|
||||
|
||||
return (
|
||||
<Group
|
||||
justify={"flex-end"}
|
||||
w={"100%"}>
|
||||
<ProjectSelect
|
||||
data={projects}
|
||||
value={selectedProject}
|
||||
onChange={value => value && setSelectedProject(value)}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
37
src/app/deals/components/SortableItem/SortableItem.tsx
Normal file
37
src/app/deals/components/SortableItem/SortableItem.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
id: string;
|
||||
};
|
||||
|
||||
const SortableItem = ({ children, id }: Props) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableItem;
|
||||
67
src/app/deals/components/StatusColumn/StatusColumn.tsx
Normal file
67
src/app/deals/components/StatusColumn/StatusColumn.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { Box, Stack, Text } from "@mantine/core";
|
||||
import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
import { sortByLexorank } from "@/utils/lexorank";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
status: StatusSchema;
|
||||
deals: DealSchema[];
|
||||
isDragging?: boolean;
|
||||
};
|
||||
|
||||
const StatusColumn = ({ id, status, deals, isDragging }: Props) => {
|
||||
const { setNodeRef } = useDroppable({ id });
|
||||
const sortedDeals = useMemo(
|
||||
() => sortByLexorank(deals.filter(deal => deal.statusId === status.id)),
|
||||
[deals]
|
||||
);
|
||||
|
||||
const columnBody = useMemo(() => {
|
||||
return (
|
||||
<SortableContext
|
||||
id={id}
|
||||
items={sortedDeals}
|
||||
strategy={verticalListSortingStrategy}>
|
||||
<Stack
|
||||
gap={"xs"}
|
||||
ref={setNodeRef}>
|
||||
{sortedDeals.map(deal => (
|
||||
<DealContainer
|
||||
key={deal.id}
|
||||
deal={deal}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</SortableContext>
|
||||
);
|
||||
}, [sortedDeals]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: "#eee",
|
||||
padding: 2,
|
||||
width: "15vw",
|
||||
minWidth: 150,
|
||||
}}>
|
||||
<Text
|
||||
style={{
|
||||
cursor: "grab",
|
||||
userSelect: "none",
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}}>
|
||||
{status.name}
|
||||
</Text>
|
||||
{columnBody}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusColumn;
|
||||
76
src/app/deals/components/StatusColumns/StatusColumns.tsx
Normal file
76
src/app/deals/components/StatusColumns/StatusColumns.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import StatusColumnsDnd from "@/app/deals/components/StatusColumnsDnd/StatusColumnsDnd";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import {
|
||||
updateDealMutation,
|
||||
updateStatusMutation,
|
||||
} from "@/lib/client/@tanstack/react-query.gen";
|
||||
import { notifications } from "@/lib/notifications";
|
||||
|
||||
const StatusColumns = () => {
|
||||
const { refetchStatuses, refetchDeals } = useStatusesContext();
|
||||
|
||||
const updateStatus = useMutation({
|
||||
...updateStatusMutation(),
|
||||
onError: error => {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: error.response?.data?.detail as string | undefined,
|
||||
});
|
||||
refetchStatuses();
|
||||
},
|
||||
});
|
||||
|
||||
const updateDeals = useMutation({
|
||||
...updateDealMutation(),
|
||||
onError: error => {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: error.response?.data?.detail as string | undefined,
|
||||
});
|
||||
refetchDeals();
|
||||
},
|
||||
});
|
||||
|
||||
const onDealDragEnd = (
|
||||
dealId: number,
|
||||
statusId: number,
|
||||
lexorank?: string
|
||||
) => {
|
||||
updateDeals.mutate({
|
||||
path: {
|
||||
dealId,
|
||||
},
|
||||
body: {
|
||||
deal: {
|
||||
statusId,
|
||||
lexorank,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onStatusDragEnd = (statusId: number, lexorank: string) => {
|
||||
updateStatus.mutate({
|
||||
path: {
|
||||
statusId,
|
||||
},
|
||||
body: {
|
||||
status: {
|
||||
lexorank,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StatusColumnsDnd
|
||||
onDealDragEnd={onDealDragEnd}
|
||||
onStatusDragEnd={onStatusDragEnd}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusColumns;
|
||||
@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { closestCorners, DndContext } from "@dnd-kit/core";
|
||||
import {
|
||||
horizontalListSortingStrategy,
|
||||
SortableContext,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { Group, ScrollArea } from "@mantine/core";
|
||||
import DndOverlay from "@/app/deals/components/DndOverlay/DndOverlay";
|
||||
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
||||
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||
import useDndSensors from "../../hooks/useSensors";
|
||||
|
||||
type Props = {
|
||||
onDealDragEnd: (
|
||||
dealId: number,
|
||||
statusId: number,
|
||||
lexorank?: string
|
||||
) => void;
|
||||
onStatusDragEnd: (statusId: number, lexorank: string) => void;
|
||||
};
|
||||
|
||||
const StatusColumnsDnd: FC<Props> = props => {
|
||||
const { deals } = useStatusesContext();
|
||||
|
||||
const {
|
||||
sortedStatuses,
|
||||
handleDragStart,
|
||||
handleDragOver,
|
||||
handleDragEnd,
|
||||
activeStatus,
|
||||
activeDeal,
|
||||
} = useDealsAndStatusesDnd(props);
|
||||
|
||||
const sensors = useDndSensors();
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
offsetScrollbars={"x"}
|
||||
scrollbarSize={"0.5rem"}>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}>
|
||||
<SortableContext
|
||||
items={sortedStatuses.map(status => `${status.id}-status`)}
|
||||
strategy={horizontalListSortingStrategy}>
|
||||
<Group
|
||||
gap={"xs"}
|
||||
wrap={"nowrap"}
|
||||
align={"start"}>
|
||||
{sortedStatuses.map(status => (
|
||||
<SortableItem
|
||||
key={status.id}
|
||||
id={`${status.id}-status`}>
|
||||
<StatusColumn
|
||||
id={`${status.id}-status`}
|
||||
status={status}
|
||||
deals={deals}
|
||||
isDragging={activeStatus?.id === status.id}
|
||||
/>
|
||||
</SortableItem>
|
||||
))}
|
||||
<DndOverlay
|
||||
activeStatus={activeStatus}
|
||||
activeDeal={activeDeal}
|
||||
/>
|
||||
</Group>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusColumnsDnd;
|
||||
@ -1,7 +0,0 @@
|
||||
|
||||
.board {
|
||||
min-width: 130px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { Box, Flex, Group, Text } from "@mantine/core";
|
||||
import styles from "@/app/deals/components/desktop/Board/Board.module.css";
|
||||
import BoardMenu from "@/app/deals/components/shared/BoardMenu/BoardMenu";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import SmallPageBlock from "@/components/layout/SmallPageBlock/SmallPageBlock";
|
||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
board: BoardSchema;
|
||||
};
|
||||
|
||||
const Board: FC<Props> = ({ board }) => {
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const { onUpdateBoard } = useBoardsContext();
|
||||
|
||||
return (
|
||||
<Flex p={"lg"}>
|
||||
<SmallPageBlock active={selectedBoard?.id === board.id}>
|
||||
<Group
|
||||
px={"md"}
|
||||
py={"xs"}
|
||||
bdrs={"lg"}
|
||||
justify={"space-between"}
|
||||
className={styles.board}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}>
|
||||
<InPlaceInput
|
||||
defaultValue={board.name}
|
||||
onComplete={value =>
|
||||
onUpdateBoard(board.id, { name: value })
|
||||
}
|
||||
inputStyles={{
|
||||
input: {
|
||||
height: 25,
|
||||
minHeight: 25,
|
||||
},
|
||||
}}
|
||||
getChildren={startEditing => (
|
||||
<>
|
||||
<Box>
|
||||
<Text>{board.name}</Text>
|
||||
</Box>
|
||||
<BoardMenu
|
||||
isHovered={isHovered}
|
||||
board={board}
|
||||
startEditing={startEditing}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
modalTitle={"Редактирование доски"}
|
||||
/>
|
||||
</Group>
|
||||
</SmallPageBlock>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Board;
|
||||
@ -1,50 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Group, ScrollArea } from "@mantine/core";
|
||||
import Board from "@/app/deals/components/desktop/Board/Board";
|
||||
import CreateBoardButton from "@/app/deals/components/desktop/CreateBoardButton/CreateBoardButton";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import SortableDnd from "@/components/dnd/SortableDnd";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
|
||||
const Boards = () => {
|
||||
const { boards, setSelectedBoard, onUpdateBoard } = useBoardsContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const renderBoard = (board: BoardSchema) => <Board board={board} />;
|
||||
|
||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
||||
onUpdateBoard(itemId, { lexorank: newLexorank });
|
||||
};
|
||||
|
||||
const selectBoard = (board: BoardSchema) => {
|
||||
setSelectedBoard(board);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
offsetScrollbars={"x"}
|
||||
scrollbars={"x"}
|
||||
scrollbarSize={0}
|
||||
w={"100%"}>
|
||||
<Group
|
||||
wrap={"nowrap"}
|
||||
gap={0}>
|
||||
<SortableDnd
|
||||
initialItems={boards}
|
||||
renderItem={renderBoard}
|
||||
onDragEnd={onDragEnd}
|
||||
onItemClick={selectBoard}
|
||||
containerStyle={{ flexWrap: "nowrap" }}
|
||||
dragHandleStyle={{ cursor: "pointer" }}
|
||||
disabled={isMobile}
|
||||
/>
|
||||
<CreateBoardButton />
|
||||
</Group>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export default Boards;
|
||||
@ -1,34 +0,0 @@
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { Box } from "@mantine/core";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import SmallPageBlock from "@/components/layout/SmallPageBlock/SmallPageBlock";
|
||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||
|
||||
const CreateBoardButton = () => {
|
||||
const { onCreateBoard } = useBoardsContext();
|
||||
|
||||
return (
|
||||
<SmallPageBlock style={{ cursor: "pointer" }}>
|
||||
<InPlaceInput
|
||||
placeholder={"Название доски"}
|
||||
onComplete={onCreateBoard}
|
||||
getChildren={startEditing => (
|
||||
<Box
|
||||
p={"sm"}
|
||||
onClick={startEditing}>
|
||||
<IconPlus />
|
||||
</Box>
|
||||
)}
|
||||
modalTitle={"Создание доски"}
|
||||
inputStyles={{
|
||||
wrapper: {
|
||||
marginLeft: 15,
|
||||
marginRight: 15,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</SmallPageBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateBoardButton;
|
||||
@ -1,14 +0,0 @@
|
||||
|
||||
.board-mobile {
|
||||
min-width: 50px;
|
||||
flex-wrap: nowrap;
|
||||
gap: 3px;
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px;
|
||||
border-bottom: 2px solid gray;
|
||||
}
|
||||
|
||||
.board-mobile-selected {
|
||||
border: 2px solid gray;
|
||||
border-bottom: 0;
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Box, Text } from "@mantine/core";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
import styles from "./BoardMobile.module.css";
|
||||
|
||||
type Props = {
|
||||
board: BoardSchema;
|
||||
};
|
||||
|
||||
const BoardMobile: FC<Props> = ({ board }) => {
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
|
||||
return (
|
||||
<Box
|
||||
px={"md"}
|
||||
py={"xs"}
|
||||
className={classNames(
|
||||
styles["board-mobile"],
|
||||
selectedBoard?.id === board.id &&
|
||||
styles["board-mobile-selected"]
|
||||
)}>
|
||||
<Text style={{ textWrap: "nowrap" }}>{board.name}</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardMobile;
|
||||
@ -1,51 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Group, ScrollArea } from "@mantine/core";
|
||||
import BoardMobile from "@/app/deals/components/mobile/BoardMobile/BoardMobile";
|
||||
import CreateBoardButtonMobile from "@/app/deals/components/mobile/CreateBoardButtonMobile/CreateBoardButtonMobile";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import SortableDnd from "@/components/dnd/SortableDnd";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
|
||||
const BoardsMobile = () => {
|
||||
const { boards, setSelectedBoard, onUpdateBoard } = useBoardsContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const renderBoard = (board: BoardSchema) => <BoardMobile board={board} />;
|
||||
|
||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
||||
onUpdateBoard(itemId, { lexorank: newLexorank });
|
||||
};
|
||||
|
||||
const selectBoard = (board: BoardSchema) => {
|
||||
setSelectedBoard(board);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
offsetScrollbars={"x"}
|
||||
scrollbars={"x"}
|
||||
scrollbarSize={0}
|
||||
w={"100vw"}
|
||||
mt={5}>
|
||||
<Group
|
||||
wrap={"nowrap"}
|
||||
gap={0}>
|
||||
<SortableDnd
|
||||
initialItems={boards}
|
||||
renderItem={renderBoard}
|
||||
onDragEnd={onDragEnd}
|
||||
onItemClick={selectBoard}
|
||||
containerStyle={{ flexWrap: "nowrap" }}
|
||||
dragHandleStyle={{ cursor: "pointer" }}
|
||||
disabled={isMobile}
|
||||
/>
|
||||
<CreateBoardButtonMobile />
|
||||
</Group>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardsMobile;
|
||||
@ -1,11 +0,0 @@
|
||||
|
||||
.create-button {
|
||||
padding: 10px 10px 11px 10px;
|
||||
border-bottom: 2px solid gray;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 46px;
|
||||
border-bottom: 2px solid gray;
|
||||
width: 100%;
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { Box, Space } from "@mantine/core";
|
||||
import styles from "@/app/deals/components/mobile/CreateBoardButtonMobile/CreateBoardButtonMobile.module.css";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||
|
||||
const CreateBoardButtonMobile = () => {
|
||||
const { onCreateBoard } = useBoardsContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<InPlaceInput
|
||||
placeholder={"Название доски"}
|
||||
onComplete={onCreateBoard}
|
||||
getChildren={startEditing => (
|
||||
<Box
|
||||
onClick={startEditing}
|
||||
className={styles["create-button"]}>
|
||||
<IconPlus />
|
||||
</Box>
|
||||
)}
|
||||
modalTitle={"Создание доски"}
|
||||
inputStyles={{
|
||||
wrapper: {
|
||||
marginLeft: 15,
|
||||
marginRight: 15,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Space className={styles.spacer} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateBoardButtonMobile;
|
||||
@ -1,55 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { Box, Group, Menu, Text } from "@mantine/core";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
board: BoardSchema;
|
||||
startEditing: () => void;
|
||||
isHovered?: boolean;
|
||||
};
|
||||
|
||||
const BoardMenu: FC<Props> = ({ board, startEditing, isHovered = true }) => {
|
||||
const { selectedBoard, onDeleteBoard } = useBoardsContext();
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Box
|
||||
style={{
|
||||
opacity:
|
||||
isHovered || selectedBoard?.id === board.id ? 1 : 0,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<IconDotsVertical />
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
startEditing();
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconEdit />
|
||||
<Text>Переименовать</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDeleteBoard(board);
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconTrash />
|
||||
<Text>Удалить</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardMenu;
|
||||
@ -1,11 +0,0 @@
|
||||
|
||||
.container {
|
||||
flex-wrap: nowrap;
|
||||
border-bottom: solid dodgerblue 3px;
|
||||
margin-bottom: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
@media (max-width: 48em) {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
import React from "react";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { Box, Center, Group, Stack, Text } from "@mantine/core";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
import styles from "./CreateStatusButton.module.css";
|
||||
|
||||
const CreateStatusButton = () => {
|
||||
const { onCreateStatus } = useStatusesContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Box className={styles.container}>
|
||||
<InPlaceInput
|
||||
placeholder={"Название колонки"}
|
||||
onComplete={onCreateStatus}
|
||||
getChildren={startEditing => (
|
||||
<Center
|
||||
p={"sm"}
|
||||
onClick={() => startEditing()}>
|
||||
<Group
|
||||
gap={"xs"}
|
||||
wrap={"nowrap"}
|
||||
align={"start"}>
|
||||
<IconPlus />
|
||||
{isMobile && <Text>Добавить</Text>}
|
||||
</Group>
|
||||
</Center>
|
||||
)}
|
||||
modalTitle={"Создание колонки"}
|
||||
inputStyles={{
|
||||
wrapper: {
|
||||
padding: 4,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateStatusButton;
|
||||
@ -1,9 +0,0 @@
|
||||
|
||||
.container {
|
||||
@mixin light {
|
||||
background-color: var(--color-light-aqua);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
}
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import { Group, ScrollArea } from "@mantine/core";
|
||||
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer";
|
||||
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
||||
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
import { sortByLexorank } from "@/utils/lexorank";
|
||||
|
||||
const Funnel: FC = () => {
|
||||
const { deals } = useDealsContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const {
|
||||
sortedStatuses,
|
||||
handleDragStart,
|
||||
handleDragOver,
|
||||
handleDragEnd,
|
||||
activeStatus,
|
||||
activeDeal,
|
||||
} = useDealsAndStatusesDnd();
|
||||
|
||||
const renderFunnelDnd = () => (
|
||||
<FunnelDnd
|
||||
containers={sortedStatuses}
|
||||
items={deals}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
getContainerId={(status: StatusSchema) => `${status.id}-status`}
|
||||
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
|
||||
sortByLexorank(
|
||||
items.filter(deal => deal.statusId === status.id)
|
||||
)
|
||||
}
|
||||
renderContainer={(
|
||||
status: StatusSchema,
|
||||
funnelColumnComponent: ReactNode
|
||||
) => (
|
||||
<StatusColumnWrapper
|
||||
status={status}
|
||||
isDragging={activeStatus?.id === status.id}>
|
||||
{funnelColumnComponent}
|
||||
</StatusColumnWrapper>
|
||||
)}
|
||||
renderItem={(deal: DealSchema) => (
|
||||
<DealContainer
|
||||
key={deal.id}
|
||||
deal={deal}
|
||||
/>
|
||||
)}
|
||||
activeContainer={activeStatus}
|
||||
activeItem={activeDeal}
|
||||
renderItemOverlay={(deal: DealSchema) => <DealCard deal={deal} />}
|
||||
renderContainerOverlay={(status: StatusSchema, children) => (
|
||||
<StatusColumnWrapper
|
||||
status={status}
|
||||
isDragging>
|
||||
{children}
|
||||
</StatusColumnWrapper>
|
||||
)}
|
||||
disabledColumns={isMobile}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isMobile) return renderFunnelDnd();
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
offsetScrollbars={"x"}
|
||||
scrollbarSize={"0.5rem"}>
|
||||
<Group
|
||||
align={"start"}
|
||||
wrap={"nowrap"}
|
||||
gap={"xs"}>
|
||||
{renderFunnelDnd()}
|
||||
<CreateStatusButton />
|
||||
</Group>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export default Funnel;
|
||||
@ -1,71 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { IconChevronLeft, IconSettings } from "@tabler/icons-react";
|
||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||
import Boards from "@/app/deals/components/desktop/Boards/Boards";
|
||||
import BoardsMobile from "@/app/deals/components/mobile/BoardsMobile/BoardsMobile";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
||||
import { ColorSchemeToggle } from "@/components/ui/ColorSchemeToggle/ColorSchemeToggle";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
|
||||
const Header = () => {
|
||||
const {
|
||||
projects,
|
||||
setSelectedProject,
|
||||
selectedProject,
|
||||
setIsEditorDrawerOpened: setIsProjectsDrawerOpened,
|
||||
} = useProjectsContext();
|
||||
const { setIsEditorDrawerOpened } = useBoardsContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const getDesktopHeader = () => {
|
||||
return (
|
||||
<Group
|
||||
w={"100%"}
|
||||
justify={"space-between"}
|
||||
wrap={"nowrap"}
|
||||
pr={"md"}>
|
||||
<Boards />
|
||||
<ColorSchemeToggle />
|
||||
<ProjectSelect
|
||||
data={projects}
|
||||
value={selectedProject}
|
||||
onChange={value => value && setSelectedProject(value)}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const getMobileHeader = () => {
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
<Group justify={"space-between"}>
|
||||
<Box
|
||||
p={"md"}
|
||||
onClick={() => setIsProjectsDrawerOpened(true)}>
|
||||
<IconChevronLeft />
|
||||
</Box>
|
||||
<Text>{selectedProject?.name}</Text>
|
||||
<Box
|
||||
p={"md"}
|
||||
onClick={() => setIsEditorDrawerOpened(true)}>
|
||||
<IconSettings />
|
||||
</Box>
|
||||
</Group>
|
||||
<BoardsMobile />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
justify={"flex-end"}
|
||||
w={"100%"}>
|
||||
{isMobile ? getMobileHeader() : getDesktopHeader()}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@ -1,13 +0,0 @@
|
||||
|
||||
.container {
|
||||
min-width: 150px;
|
||||
width: 15vw;
|
||||
|
||||
@media (max-width: 48em) {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
border-bottom: solid dodgerblue 3px;
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
import styles from "./StatusColumnWrapper.module.css";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
isDragging?: boolean;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const StatusColumnWrapper = ({
|
||||
status,
|
||||
children,
|
||||
isDragging = false,
|
||||
}: Props) => {
|
||||
const { onUpdateStatus } = useStatusesContext();
|
||||
|
||||
const handleSave = (value: string) => {
|
||||
const newValue = value.trim();
|
||||
if (newValue && newValue !== status.name) {
|
||||
onUpdateStatus(status.id, { name: newValue });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={styles.container}>
|
||||
<Group
|
||||
justify={"space-between"}
|
||||
p={"sm"}
|
||||
wrap={"nowrap"}
|
||||
mb={"xs"}
|
||||
className={styles.header}>
|
||||
<InPlaceInput
|
||||
defaultValue={status.name}
|
||||
onComplete={value => handleSave(value)}
|
||||
inputStyles={{
|
||||
input: {
|
||||
height: 25,
|
||||
minHeight: 25,
|
||||
},
|
||||
}}
|
||||
getChildren={startEditing => (
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
cursor: "grab",
|
||||
userSelect: "none",
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}}>
|
||||
{status.name}
|
||||
</Text>
|
||||
<StatusMenu
|
||||
status={status}
|
||||
handleEdit={startEditing}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
modalTitle={"Редактирование статуса"}
|
||||
/>
|
||||
</Group>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusColumnWrapper;
|
||||
@ -1,71 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconEdit,
|
||||
IconExchange,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import { Box, Group, Menu, Text } from "@mantine/core";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
handleEdit: () => void;
|
||||
};
|
||||
|
||||
const StatusMenu: FC<Props> = ({ status, handleEdit }) => {
|
||||
const isMobile = useIsMobile();
|
||||
const { onDeleteStatus, setIsEditorDrawerOpened } = useStatusesContext();
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Box
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<IconDotsVertical />
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleEdit();
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconEdit />
|
||||
<Text>Переименовать</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDeleteStatus(status);
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconTrash />
|
||||
<Text>Удалить</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
{isMobile && (
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setIsEditorDrawerOpened(true);
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconExchange />
|
||||
<Text>Изменить порядок</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusMenu;
|
||||
@ -8,9 +8,8 @@ import React, {
|
||||
useState,
|
||||
} from "react";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
import useBoardsList from "@/hooks/useBoardsList";
|
||||
import { useBoardsOperations } from "@/hooks/useBoardsOperations";
|
||||
import { BoardSchema, UpdateBoardSchema } from "@/lib/client";
|
||||
|
||||
type BoardsContextState = {
|
||||
boards: BoardSchema[];
|
||||
@ -18,27 +17,16 @@ type BoardsContextState = {
|
||||
selectedBoard: BoardSchema | null;
|
||||
setSelectedBoard: React.Dispatch<React.SetStateAction<BoardSchema | null>>;
|
||||
refetchBoards: () => void;
|
||||
onCreateBoard: (name: string) => void;
|
||||
onUpdateBoard: (boardId: number, board: UpdateBoardSchema) => void;
|
||||
onDeleteBoard: (board: BoardSchema) => void;
|
||||
isEditorDrawerOpened: boolean;
|
||||
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const BoardsContext = createContext<BoardsContextState | undefined>(undefined);
|
||||
|
||||
const useBoardsContextState = () => {
|
||||
const { selectedProject: project } = useProjectsContext();
|
||||
const {
|
||||
boards,
|
||||
setBoards,
|
||||
refetch: refetchBoards,
|
||||
} = useBoardsList({ projectId: project?.id });
|
||||
const { boards, setBoards, refetch: refetchBoards } = useBoardsList({ projectId: project?.id });
|
||||
const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>(
|
||||
null
|
||||
);
|
||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
||||
useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (boards.length > 0 && selectedBoard === null) {
|
||||
@ -46,24 +34,15 @@ const useBoardsContextState = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedBoard) return;
|
||||
|
||||
if (selectedBoard) {
|
||||
let newBoard = boards.find(board => board.id === selectedBoard.id);
|
||||
|
||||
if (!newBoard && boards.length > 0) {
|
||||
newBoard = boards[0];
|
||||
}
|
||||
setSelectedBoard(newBoard ?? null);
|
||||
}, [boards]);
|
||||
|
||||
const { onCreateBoard, onUpdateBoard, onDeleteBoard } = useBoardsOperations(
|
||||
{
|
||||
boards,
|
||||
setBoards,
|
||||
refetchBoards,
|
||||
projectId: project?.id,
|
||||
}
|
||||
);
|
||||
}, [boards]);
|
||||
|
||||
return {
|
||||
boards,
|
||||
@ -71,11 +50,6 @@ const useBoardsContextState = () => {
|
||||
selectedBoard,
|
||||
setSelectedBoard,
|
||||
refetchBoards,
|
||||
onCreateBoard,
|
||||
onUpdateBoard,
|
||||
onDeleteBoard,
|
||||
isEditorDrawerOpened,
|
||||
setIsEditorDrawerOpened,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,80 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, FC, useContext } from "react";
|
||||
import { useMutation, UseMutationResult } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import useDealsList from "@/hooks/useDealsList";
|
||||
import {
|
||||
DealSchema,
|
||||
HttpValidationError,
|
||||
Options,
|
||||
UpdateDealData,
|
||||
UpdateDealResponse,
|
||||
} from "@/lib/client";
|
||||
import { updateDealMutation } from "@/lib/client/@tanstack/react-query.gen";
|
||||
import { notifications } from "@/lib/notifications";
|
||||
|
||||
type DealsContextState = {
|
||||
deals: DealSchema[];
|
||||
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
|
||||
updateDeal: UseMutationResult<
|
||||
UpdateDealResponse,
|
||||
AxiosError<HttpValidationError>,
|
||||
Options<UpdateDealData>
|
||||
>;
|
||||
refetchDeals: () => void;
|
||||
};
|
||||
|
||||
const DealsContext = createContext<DealsContextState | undefined>(undefined);
|
||||
|
||||
const useDealsContextState = () => {
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
const {
|
||||
deals,
|
||||
setDeals,
|
||||
refetch: refetchDeals,
|
||||
} = useDealsList({ boardId: selectedBoard?.id });
|
||||
|
||||
const updateDeal = useMutation({
|
||||
...updateDealMutation(),
|
||||
onError: error => {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: error.response?.data?.detail as string | undefined,
|
||||
});
|
||||
refetchDeals();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deals,
|
||||
setDeals,
|
||||
updateDeal,
|
||||
refetchDeals,
|
||||
};
|
||||
};
|
||||
|
||||
type DealsContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const DealsContextProvider: FC<DealsContextProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const state = useDealsContextState();
|
||||
|
||||
return (
|
||||
<DealsContext.Provider value={state}>{children}</DealsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useDealsContext = () => {
|
||||
const context = useContext(DealsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useDealsContext must be used within a DealsContextProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@ -7,9 +7,8 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ProjectSchema } from "@/lib/client";
|
||||
import useProjectsList from "@/hooks/useProjectsList";
|
||||
import { useProjectsOperations } from "@/hooks/useProjectsOperations";
|
||||
import { ProjectSchema, UpdateProjectSchema } from "@/lib/client";
|
||||
|
||||
type ProjectsContextState = {
|
||||
selectedProject: ProjectSchema | null;
|
||||
@ -17,11 +16,6 @@ type ProjectsContextState = {
|
||||
React.SetStateAction<ProjectSchema | null>
|
||||
>;
|
||||
projects: ProjectSchema[];
|
||||
onCreateProject: (name: string) => void;
|
||||
onUpdateProject: (projectId: number, project: UpdateProjectSchema) => void;
|
||||
onDeleteProject: (project: ProjectSchema) => void;
|
||||
isEditorDrawerOpened: boolean;
|
||||
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const ProjectsContext = createContext<ProjectsContextState | undefined>(
|
||||
@ -29,13 +23,7 @@ const ProjectsContext = createContext<ProjectsContextState | undefined>(
|
||||
);
|
||||
|
||||
const useProjectsContextState = () => {
|
||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
||||
useState<boolean>(false);
|
||||
const {
|
||||
projects,
|
||||
setProjects,
|
||||
refetch: refetchProjects,
|
||||
} = useProjectsList();
|
||||
const { projects } = useProjectsList();
|
||||
const [selectedProject, setSelectedProject] =
|
||||
useState<ProjectSchema | null>(null);
|
||||
|
||||
@ -55,22 +43,10 @@ const useProjectsContextState = () => {
|
||||
setSelectedProject(null);
|
||||
}, [projects]);
|
||||
|
||||
const { onCreateProject, onUpdateProject, onDeleteProject } =
|
||||
useProjectsOperations({
|
||||
projects,
|
||||
setProjects,
|
||||
refetchProjects,
|
||||
});
|
||||
|
||||
return {
|
||||
projects,
|
||||
selectedProject,
|
||||
setSelectedProject,
|
||||
onCreateProject,
|
||||
onUpdateProject,
|
||||
onDeleteProject,
|
||||
isEditorDrawerOpened,
|
||||
setIsEditorDrawerOpened,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,42 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
FC,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useMutation, UseMutationResult } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import React, { createContext, FC, useContext, useEffect } from "react";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
import useDealsList from "@/hooks/useDealsList";
|
||||
import useStatusesList from "@/hooks/useStatusesList";
|
||||
import { useStatusesOperations } from "@/hooks/useStatusesOperations";
|
||||
import {
|
||||
HttpValidationError,
|
||||
Options,
|
||||
StatusSchema,
|
||||
UpdateStatusData,
|
||||
UpdateStatusResponse,
|
||||
UpdateStatusSchema,
|
||||
} from "@/lib/client";
|
||||
import { updateStatusMutation } from "@/lib/client/@tanstack/react-query.gen";
|
||||
import { notifications } from "@/lib/notifications";
|
||||
|
||||
type StatusesContextState = {
|
||||
statuses: StatusSchema[];
|
||||
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
||||
updateStatus: UseMutationResult<
|
||||
UpdateStatusResponse,
|
||||
AxiosError<HttpValidationError>,
|
||||
Options<UpdateStatusData>
|
||||
>;
|
||||
deals: DealSchema[];
|
||||
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
|
||||
refetchStatuses: () => void;
|
||||
onCreateStatus: (name: string) => void;
|
||||
onUpdateStatus: (statusId: number, status: UpdateStatusSchema) => void;
|
||||
onDeleteStatus: (status: StatusSchema) => void;
|
||||
isEditorDrawerOpened: boolean;
|
||||
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
refetchDeals: () => void;
|
||||
};
|
||||
|
||||
const StatusesContext = createContext<StatusesContextState | undefined>(
|
||||
@ -52,42 +28,24 @@ const useStatusesContextState = () => {
|
||||
} = useStatusesList({
|
||||
boardId: selectedBoard?.id,
|
||||
});
|
||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const {
|
||||
deals,
|
||||
setDeals,
|
||||
refetch: refetchDeals,
|
||||
} = useDealsList({ boardId: selectedBoard?.id });
|
||||
|
||||
useEffect(() => {
|
||||
refetchStatuses();
|
||||
}, [selectedBoard]);
|
||||
|
||||
const updateStatus = useMutation({
|
||||
...updateStatusMutation(),
|
||||
onError: error => {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: error.response?.data?.detail as string | undefined,
|
||||
});
|
||||
refetchStatuses();
|
||||
},
|
||||
});
|
||||
|
||||
const { onCreateStatus, onUpdateStatus, onDeleteStatus } =
|
||||
useStatusesOperations({
|
||||
statuses,
|
||||
setStatuses,
|
||||
refetchStatuses,
|
||||
boardId: selectedBoard?.id,
|
||||
});
|
||||
|
||||
return {
|
||||
statuses,
|
||||
setStatuses,
|
||||
updateStatus,
|
||||
deals,
|
||||
setDeals,
|
||||
refetchStatuses,
|
||||
onCreateStatus,
|
||||
onUpdateStatus,
|
||||
onDeleteStatus,
|
||||
isEditorDrawerOpened,
|
||||
setIsEditorDrawerOpened,
|
||||
refetchDeals,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
|
||||
import { Box, Center, Divider, Drawer, Group, rem, Text } from "@mantine/core";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import CreateStatusButton from "@/app/deals/drawers/BoardStatusesEditorDrawer/components/CreateStatusButton";
|
||||
import StatusMobile from "@/app/deals/drawers/BoardStatusesEditorDrawer/components/StatusMobile";
|
||||
import SortableDnd from "@/components/dnd/SortableDnd";
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
|
||||
const BoardStatusesEditorDrawer: FC = () => {
|
||||
const {
|
||||
statuses,
|
||||
onUpdateStatus,
|
||||
isEditorDrawerOpened,
|
||||
setIsEditorDrawerOpened,
|
||||
} = useStatusesContext();
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
const onClose = () => setIsEditorDrawerOpened(false);
|
||||
|
||||
const renderDraggable = () => (
|
||||
<Box p={"xs"}>
|
||||
<IconGripVertical />
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderStatus = (
|
||||
status: StatusSchema,
|
||||
renderDraggable?: (item: StatusSchema) => ReactNode
|
||||
) => {
|
||||
return (
|
||||
<Group wrap={"nowrap"}>
|
||||
{renderDraggable && renderDraggable(status)}
|
||||
<StatusMobile status={status} />
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
||||
onUpdateStatus(itemId, { lexorank: newLexorank });
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
size={"100%"}
|
||||
position={"right"}
|
||||
onClose={onClose}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
withCloseButton={false}
|
||||
opened={isEditorDrawerOpened}
|
||||
trapFocus={false}
|
||||
styles={{
|
||||
body: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: rem(10),
|
||||
},
|
||||
}}>
|
||||
<Group justify={"space-between"}>
|
||||
<Box
|
||||
onClick={onClose}
|
||||
p={"xs"}>
|
||||
<IconChevronLeft />
|
||||
</Box>
|
||||
<Center>
|
||||
<Text>{selectedBoard?.name}</Text>
|
||||
</Center>
|
||||
<Box p={"lg"} />
|
||||
</Group>
|
||||
<Divider />
|
||||
<SortableDnd
|
||||
initialItems={statuses}
|
||||
onDragEnd={onDragEnd}
|
||||
renderItem={renderStatus}
|
||||
renderDraggable={renderDraggable}
|
||||
dragHandleStyle={{ width: "auto" }}
|
||||
vertical
|
||||
/>
|
||||
<CreateStatusButton />
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardStatusesEditorDrawer;
|
||||
@ -1,32 +0,0 @@
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
|
||||
const CreateStatusButton = () => {
|
||||
const { onCreateStatus } = useStatusesContext();
|
||||
|
||||
const onStartCreating = () => {
|
||||
modals.openContextModal({
|
||||
modal: "enterNameModal",
|
||||
title: "Создание колонки",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
onComplete: onCreateStatus,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
ml={"xs"}
|
||||
onClick={onStartCreating}>
|
||||
<IconPlus />
|
||||
<Box mt={5}>
|
||||
<Text>Создать колонку</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateStatusButton;
|
||||
@ -1,45 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import BoardMenu from "@/app/deals/components/shared/BoardMenu/BoardMenu";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
};
|
||||
|
||||
const StatusMobile: FC<Props> = ({ status }) => {
|
||||
const { onUpdateStatus } = useStatusesContext();
|
||||
|
||||
const startEditing = () => {
|
||||
modals.openContextModal({
|
||||
modal: "enterNameModal",
|
||||
title: "Редактирование статуса",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
onComplete: name => onUpdateStatus(status.id, { name }),
|
||||
defaultValue: status.name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
w={"100%"}
|
||||
pr={"md"}
|
||||
py={"xs"}
|
||||
justify={"space-between"}
|
||||
wrap={"nowrap"}>
|
||||
<Box>
|
||||
<Text>{status.name}</Text>
|
||||
</Box>
|
||||
<BoardMenu
|
||||
board={status}
|
||||
startEditing={startEditing}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusMobile;
|
||||
@ -1,3 +0,0 @@
|
||||
import BoardStatusesEditorDrawer from "@/app/deals/drawers/BoardStatusesEditorDrawer/BoardStatusesEditorDrawer";
|
||||
|
||||
export default BoardStatusesEditorDrawer;
|
||||
@ -1,87 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import { IconChevronLeft, IconGripVertical } from "@tabler/icons-react";
|
||||
import { Box, Center, Divider, Drawer, Group, rem, Text } from "@mantine/core";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import BoardMobile from "@/app/deals/drawers/ProjectBoardsEditorDrawer/components/BoardMobile";
|
||||
import CreateBoardButton from "@/app/deals/drawers/ProjectBoardsEditorDrawer/components/CreateBoardButton";
|
||||
import SortableDnd from "@/components/dnd/SortableDnd";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
|
||||
const ProjectBoardsEditorDrawer: FC = () => {
|
||||
const {
|
||||
boards,
|
||||
onUpdateBoard,
|
||||
isEditorDrawerOpened,
|
||||
setIsEditorDrawerOpened,
|
||||
} = useBoardsContext();
|
||||
const { selectedProject } = useProjectsContext();
|
||||
const onClose = () => setIsEditorDrawerOpened(false);
|
||||
|
||||
const renderDraggable = () => (
|
||||
<Box p={"xs"}>
|
||||
<IconGripVertical />
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderBoard = (
|
||||
board: BoardSchema,
|
||||
renderDraggable?: (item: BoardSchema) => ReactNode
|
||||
) => {
|
||||
return (
|
||||
<Group wrap={"nowrap"}>
|
||||
{renderDraggable && renderDraggable(board)}
|
||||
<BoardMobile board={board} />
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
||||
onUpdateBoard(itemId, { lexorank: newLexorank });
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
size={"100%"}
|
||||
position={"right"}
|
||||
onClose={onClose}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
withCloseButton={false}
|
||||
opened={isEditorDrawerOpened}
|
||||
trapFocus={false}
|
||||
styles={{
|
||||
body: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: rem(10),
|
||||
},
|
||||
}}>
|
||||
<Group justify={"space-between"}>
|
||||
<Box
|
||||
onClick={onClose}
|
||||
p={"xs"}>
|
||||
<IconChevronLeft />
|
||||
</Box>
|
||||
<Center>
|
||||
<Text>{selectedProject?.name}</Text>
|
||||
</Center>
|
||||
<Box p={"lg"} />
|
||||
</Group>
|
||||
<Divider />
|
||||
<SortableDnd
|
||||
initialItems={boards}
|
||||
onDragEnd={onDragEnd}
|
||||
renderItem={renderBoard}
|
||||
renderDraggable={renderDraggable}
|
||||
dragHandleStyle={{ width: "auto" }}
|
||||
vertical
|
||||
/>
|
||||
<CreateBoardButton />
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectBoardsEditorDrawer;
|
||||
@ -1,46 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import BoardMenu from "@/app/deals/components/shared/BoardMenu/BoardMenu";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
board: BoardSchema;
|
||||
};
|
||||
|
||||
const BoardMobile: FC<Props> = ({ board }) => {
|
||||
const { onUpdateBoard } = useBoardsContext();
|
||||
|
||||
const startEditing = () => {
|
||||
modals.openContextModal({
|
||||
modal: "enterNameModal",
|
||||
title: "Редактирование доски",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
onComplete: name => onUpdateBoard(board.id, { name }),
|
||||
defaultValue: board.name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
w={"100%"}
|
||||
pr={"md"}
|
||||
py={"xs"}
|
||||
justify={"space-between"}
|
||||
wrap={"nowrap"}>
|
||||
<Box>
|
||||
<Text>{board.name}</Text>
|
||||
</Box>
|
||||
<BoardMenu
|
||||
board={board}
|
||||
startEditing={startEditing}
|
||||
menuIconSize={22}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardMobile;
|
||||
@ -1,30 +0,0 @@
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
|
||||
const CreateBoardButton = () => {
|
||||
const { onCreateBoard } = useBoardsContext();
|
||||
|
||||
const onStartCreating = () => {
|
||||
modals.openContextModal({
|
||||
modal: "enterNameModal",
|
||||
title: "Создание доски",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
onComplete: onCreateBoard,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Group ml={"xs"} onClick={onStartCreating}>
|
||||
<IconPlus />
|
||||
<Box mt={5}>
|
||||
<Text>Создать доску</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateBoardButton;
|
||||
@ -1,3 +0,0 @@
|
||||
import ProjectBoardsEditorDrawer from "@/app/deals/drawers/ProjectBoardsEditorDrawer/ProjectBoardsEditorDrawer";
|
||||
|
||||
export default ProjectBoardsEditorDrawer;
|
||||
@ -1,16 +0,0 @@
|
||||
.project:hover {
|
||||
@mixin light {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.project:hover:has(.menu-button:hover) {
|
||||
background: none; /* or revert to normal */
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { Center, Divider, Drawer, rem, Stack, Text } from "@mantine/core";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import CreateProjectButton from "@/app/deals/drawers/ProjectsEditorDrawer/components/CreateProjectButton";
|
||||
import ProjectMobile from "@/app/deals/drawers/ProjectsEditorDrawer/components/ProjectMobile";
|
||||
|
||||
const ProjectsEditorDrawer: FC = () => {
|
||||
const { projects, isEditorDrawerOpened, setIsEditorDrawerOpened } =
|
||||
useProjectsContext();
|
||||
const onClose = () => setIsEditorDrawerOpened(false);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
size={"100%"}
|
||||
position={"left"}
|
||||
onClose={onClose}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
withCloseButton={false}
|
||||
opened={isEditorDrawerOpened}
|
||||
trapFocus={false}
|
||||
styles={{
|
||||
body: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: rem(10),
|
||||
},
|
||||
}}>
|
||||
<Center>
|
||||
<Text>Проекты</Text>
|
||||
</Center>
|
||||
<Divider />
|
||||
<Stack gap={0}>
|
||||
{projects.map((project, index) => (
|
||||
<ProjectMobile
|
||||
key={index}
|
||||
project={project}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<CreateProjectButton />
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectsEditorDrawer;
|
||||
@ -1,32 +0,0 @@
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
|
||||
const CreateProjectButton = () => {
|
||||
const { onCreateProject } = useProjectsContext();
|
||||
|
||||
const onStartCreating = () => {
|
||||
modals.openContextModal({
|
||||
modal: "enterNameModal",
|
||||
title: "Создание проекта",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
onComplete: onCreateProject,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
ml={"xs"}
|
||||
onClick={onStartCreating}>
|
||||
<IconPlus />
|
||||
<Box mt={5}>
|
||||
<Text>Создать проект</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateProjectButton;
|
||||
@ -1,51 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { Box, Group, Menu, Text } from "@mantine/core";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import { ProjectSchema } from "@/lib/client";
|
||||
import styles from "./../ProjectsEditorDrawer.module.css";
|
||||
|
||||
type Props = {
|
||||
project: ProjectSchema;
|
||||
startEditing: () => void;
|
||||
};
|
||||
|
||||
const ProjectMenu: FC<Props> = ({ project, startEditing }) => {
|
||||
const { onDeleteProject } = useProjectsContext();
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Box
|
||||
className={styles["menu-button"]}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<IconDotsVertical />
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
startEditing();
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconEdit />
|
||||
<Text>Переименовать</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDeleteProject(project);
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconTrash />
|
||||
<Text>Удалить</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectMenu;
|
||||
@ -1,54 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import ProjectMenu from "@/app/deals/drawers/ProjectsEditorDrawer/components/ProjectMenu";
|
||||
import { ProjectSchema } from "@/lib/client";
|
||||
import styles from "./../ProjectsEditorDrawer.module.css";
|
||||
|
||||
type Props = {
|
||||
project: ProjectSchema;
|
||||
};
|
||||
|
||||
const ProjectMobile: FC<Props> = ({ project }) => {
|
||||
const { onUpdateProject, setSelectedProject, setIsEditorDrawerOpened } =
|
||||
useProjectsContext();
|
||||
|
||||
const startEditing = () => {
|
||||
modals.openContextModal({
|
||||
modal: "enterNameModal",
|
||||
title: "Редактирование проекта",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
onComplete: name => onUpdateProject(project.id, { name }),
|
||||
defaultValue: project.name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
setSelectedProject(project);
|
||||
setIsEditorDrawerOpened(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
w={"100%"}
|
||||
pl={"xs"}
|
||||
py={"xs"}
|
||||
justify={"space-between"}
|
||||
wrap={"nowrap"}
|
||||
className={styles.project}
|
||||
onClick={onClick}>
|
||||
<Box>
|
||||
<Text>{project.name}</Text>
|
||||
</Box>
|
||||
<ProjectMenu
|
||||
project={project}
|
||||
startEditing={startEditing}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectMobile;
|
||||
@ -1,3 +0,0 @@
|
||||
import ProjectsEditorDrawer from "@/app/deals/drawers/ProjectsEditorDrawer/ProjectsEditorDrawer";
|
||||
|
||||
export default ProjectsEditorDrawer;
|
||||
@ -1,27 +1,25 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core";
|
||||
import { useDebouncedCallback } from "@mantine/hooks";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
|
||||
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
import { sortByLexorank } from "@/utils/lexorank";
|
||||
|
||||
type ReturnType = {
|
||||
sortedStatuses: StatusSchema[];
|
||||
handleDragStart: ({ active }: DragStartEvent) => void;
|
||||
handleDragOver: ({ active, over }: DragOverEvent) => void;
|
||||
handleDragEnd: ({ active, over }: DragOverEvent) => void;
|
||||
activeStatus: StatusSchema | null;
|
||||
activeDeal: DealSchema | null;
|
||||
type Props = {
|
||||
onDealDragEnd: (
|
||||
dealId: number,
|
||||
statusId: number,
|
||||
lexorank?: string
|
||||
) => void;
|
||||
onStatusDragEnd: (statusId: number, lexorank: string) => void;
|
||||
};
|
||||
|
||||
const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
const useDealsAndStatusesDnd = (props: Props) => {
|
||||
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
||||
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
||||
const { statuses, setStatuses, updateStatus } = useStatusesContext();
|
||||
const { deals, setDeals, updateDeal } = useDealsContext();
|
||||
const { statuses, deals, setDeals, setStatuses } = useStatusesContext();
|
||||
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
||||
|
||||
const {
|
||||
@ -175,20 +173,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
const newRank = getNewStatusRank(activeStatusId, overStatusId);
|
||||
if (!newRank) return;
|
||||
|
||||
onStatusDragEnd?.(activeStatusId, newRank);
|
||||
};
|
||||
|
||||
const onStatusDragEnd = (statusId: number, lexorank: string) => {
|
||||
updateStatus.mutate({
|
||||
path: {
|
||||
statusId,
|
||||
},
|
||||
body: {
|
||||
status: {
|
||||
lexorank,
|
||||
},
|
||||
},
|
||||
});
|
||||
props.onStatusDragEnd?.(activeStatusId, newRank);
|
||||
};
|
||||
|
||||
const handleDealDragEnd = (activeId: number | string, over: Over) => {
|
||||
@ -204,25 +189,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
);
|
||||
if (!overStatusId) return;
|
||||
|
||||
onDealDragEnd(activeDealId, overStatusId, newLexorank);
|
||||
};
|
||||
|
||||
const onDealDragEnd = (
|
||||
dealId: number,
|
||||
statusId: number,
|
||||
lexorank?: string
|
||||
) => {
|
||||
updateDeal.mutate({
|
||||
path: {
|
||||
dealId,
|
||||
},
|
||||
body: {
|
||||
deal: {
|
||||
statusId,
|
||||
lexorank,
|
||||
},
|
||||
},
|
||||
});
|
||||
props.onDealDragEnd(activeDealId, overStatusId, newLexorank);
|
||||
};
|
||||
|
||||
const handleDragStart = ({ active }: DragStartEvent) => {
|
||||
|
||||
@ -3,30 +3,14 @@ import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||
|
||||
type NewRankGetters = {
|
||||
getNewRankForSameStatus: (
|
||||
statusDeals: DealSchema[],
|
||||
overDealIndex: number,
|
||||
activeDealId: number
|
||||
) => string;
|
||||
getNewRankForAnotherStatus: (
|
||||
statusDeals: DealSchema[],
|
||||
overDealIndex: number
|
||||
) => string;
|
||||
getNewStatusRank: (
|
||||
activeStatusId: number,
|
||||
overStatusId: number
|
||||
) => string | null;
|
||||
};
|
||||
|
||||
const useGetNewRank = (): NewRankGetters => {
|
||||
const useGetNewRank = () => {
|
||||
const { statuses } = useStatusesContext();
|
||||
|
||||
const getNewRankForSameStatus = (
|
||||
statusDeals: DealSchema[],
|
||||
overDealIndex: number,
|
||||
activeDealId: number
|
||||
): string => {
|
||||
) => {
|
||||
const activeDealIndex = statusDeals.findIndex(
|
||||
deal => deal.id === activeDealId
|
||||
);
|
||||
@ -50,7 +34,7 @@ const useGetNewRank = (): NewRankGetters => {
|
||||
const getNewRankForAnotherStatus = (
|
||||
statusDeals: DealSchema[],
|
||||
overDealIndex: number
|
||||
): string => {
|
||||
) => {
|
||||
const leftLexorank =
|
||||
overDealIndex > 0
|
||||
? LexoRank.parse(statusDeals[overDealIndex - 1].lexorank)
|
||||
@ -62,10 +46,7 @@ const useGetNewRank = (): NewRankGetters => {
|
||||
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
||||
};
|
||||
|
||||
const getNewStatusRank = (
|
||||
activeStatusId: number,
|
||||
overStatusId: number
|
||||
): string | null => {
|
||||
const getNewStatusRank = (activeStatusId: number, overStatusId: number) => {
|
||||
const sortedStatusList = sortByLexorank(statuses);
|
||||
const overIndex = sortedStatusList.findIndex(
|
||||
s => s.id === overStatusId
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
PointerSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
@ -8,21 +8,18 @@ import {
|
||||
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
||||
|
||||
const useDndSensors = () => {
|
||||
return useSensors(
|
||||
useSensor(MouseSensor, {
|
||||
const sensorOptions = {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
distance: 5,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
return useSensors(
|
||||
useSensor(PointerSensor, sensorOptions),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 300,
|
||||
tolerance: 5,
|
||||
},
|
||||
})
|
||||
useSensor(TouchSensor, sensorOptions)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,37 +1,28 @@
|
||||
import Funnel from "@/app/deals/components/shared/Funnel/Funnel";
|
||||
import Header from "@/app/deals/components/shared/Header/Header";
|
||||
import { Divider } from "@mantine/core";
|
||||
import Boards from "@/app/deals/components/Boards/Boards";
|
||||
import Header from "@/app/deals/components/Header/Header";
|
||||
import StatusColumns from "@/app/deals/components/StatusColumns/StatusColumns";
|
||||
import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext";
|
||||
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
||||
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
|
||||
import BoardStatusesEditorDrawer from "@/app/deals/drawers/BoardStatusesEditorDrawer";
|
||||
import ProjectBoardsEditorDrawer from "@/app/deals/drawers/ProjectBoardsEditorDrawer";
|
||||
import ProjectsEditorDrawer from "@/app/deals/drawers/ProjectsEditorDrawer";
|
||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
||||
import { DealsContextProvider } from "./contexts/DealsContext";
|
||||
import PageBlock from "@/components/PageBlock/PageBlock";
|
||||
import PageContainer from "@/components/PageContainer/PageContainer";
|
||||
|
||||
export default function DealsPage() {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageBlock>
|
||||
<ProjectsContextProvider>
|
||||
<BoardsContextProvider>
|
||||
<PageContainer>
|
||||
<PageBlock
|
||||
transparent
|
||||
style={{ padding: 0 }}>
|
||||
<Header />
|
||||
</PageBlock>
|
||||
<PageBlock className={"mobile-padding-height"}>
|
||||
<Boards />
|
||||
<Divider my={"xl"} />
|
||||
<StatusesContextProvider>
|
||||
<DealsContextProvider>
|
||||
<Funnel />
|
||||
</DealsContextProvider>
|
||||
<BoardStatusesEditorDrawer />
|
||||
<StatusColumns />
|
||||
</StatusesContextProvider>
|
||||
<ProjectBoardsEditorDrawer />
|
||||
<ProjectsEditorDrawer />
|
||||
</PageBlock>
|
||||
</PageContainer>
|
||||
</BoardsContextProvider>
|
||||
</ProjectsContextProvider>
|
||||
</PageBlock>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,20 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-light-gray-blue: #f4f7fd;
|
||||
--color-light-aqua: #e0f0f4;
|
||||
--color-light-whitesmoke: #fcfdff;
|
||||
--mantine-color-dark-7-5: #212121;
|
||||
/* Shadows */
|
||||
--light-shadow: 2px 2px 5px darkgray;
|
||||
--light-thick-shadow: 4px 4px 10px darkgray;
|
||||
--dark-shadow: 1px 1px 10px 1px var(--mantine-color-dark-6);
|
||||
--dark-thick-shadow: 5px 5px 10px 1px var(--mantine-color-dark-6);
|
||||
}
|
||||
|
||||
body {
|
||||
@mixin light {
|
||||
background-color: var(--color-light-gray-blue);
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/notifications/styles.css";
|
||||
import "@mantine/carousel/styles.css";
|
||||
import { ReactNode } from "react";
|
||||
import {
|
||||
ColorSchemeScript,
|
||||
@ -41,6 +40,10 @@ export default function RootLayout({ children }: Props) {
|
||||
rel="shortcut icon"
|
||||
href="/favicon.svg"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="global.css"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
|
||||
|
||||
28
src/components/ColorSchemeToggle/ColorSchemeToggle.tsx
Normal file
28
src/components/ColorSchemeToggle/ColorSchemeToggle.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Group, useMantineColorScheme } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
export function ColorSchemeToggle() {
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
|
||||
const openTestModal = () => {
|
||||
modals.openContextModal({
|
||||
modal: "testModal",
|
||||
title: "Тест",
|
||||
withCloseButton: false,
|
||||
innerProps: {},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
justify="center"
|
||||
mt="xl">
|
||||
<Button onClick={() => setColorScheme("light")}>Light</Button>
|
||||
<Button onClick={() => setColorScheme("dark")}>Dark</Button>
|
||||
<Button onClick={() => setColorScheme("auto")}>Auto</Button>
|
||||
<Button onClick={() => openTestModal()}>Modal</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
30
src/components/Draggable/Draggable.tsx
Normal file
30
src/components/Draggable/Draggable.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import { useDraggable } from "@dnd-kit/core";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const Draggable: FC<Props> = props => {
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: "draggable",
|
||||
});
|
||||
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...listeners}
|
||||
{...attributes}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Draggable;
|
||||
25
src/components/Droppable/Droppable.tsx
Normal file
25
src/components/Droppable/Droppable.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Droppable: FC<Props> = ({ children }) => {
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id: "droppable",
|
||||
});
|
||||
const style = {
|
||||
color: isOver ? "green" : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Droppable;
|
||||
41
src/components/PageBlock/PageBlock.module.css
Normal file
41
src/components/PageBlock/PageBlock.module.css
Normal file
@ -0,0 +1,41 @@
|
||||
.container {
|
||||
border-radius: rem(40);
|
||||
background-color: white;
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-8);
|
||||
box-shadow: 5px 5px 30px 1px var(--mantine-color-dark-6);
|
||||
}
|
||||
@mixin light {
|
||||
box-shadow: 5px 5px 24px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
padding: rem(35);
|
||||
}
|
||||
|
||||
.container-full-height {
|
||||
min-height: calc(100vh - (rem(20) * 2));
|
||||
}
|
||||
|
||||
.container-full-height-fixed {
|
||||
height: calc(100vh - (rem(20) * 2));
|
||||
}
|
||||
|
||||
.container-no-border-radius {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.container-full-screen-mobile {
|
||||
@media (max-width: 48em) {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
border-radius: 0 !important;
|
||||
padding: rem(40) rem(20) rem(20);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@ -1,44 +1,36 @@
|
||||
import { CSSProperties, FC, ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import styles from "./PageBlock.module.css";
|
||||
import { Box } from "@mantine/core";
|
||||
|
||||
5
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
fullHeight?: boolean;
|
||||
fullHeightFixed?: boolean;
|
||||
noBorderRadius?: boolean;
|
||||
fullScreenMobile?: boolean;
|
||||
transparent?: boolean;
|
||||
};
|
||||
|
||||
const PageBlock: FC<Props> = ({
|
||||
children,
|
||||
style,
|
||||
className = "",
|
||||
fullHeight = false,
|
||||
fullHeightFixed = false,
|
||||
noBorderRadius = false,
|
||||
fullScreenMobile = false,
|
||||
transparent = false,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
bdrs={"lg"}
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
styles.container,
|
||||
fullHeight && styles["container-full-height"],
|
||||
fullHeightFixed && styles["container-full-height-fixed"],
|
||||
noBorderRadius && styles["container-no-border-radius"],
|
||||
fullScreenMobile && styles["container-full-screen-mobile"],
|
||||
transparent && styles.transparent,
|
||||
styles[className]
|
||||
fullScreenMobile && styles["container-full-screen-mobile"]
|
||||
)}>
|
||||
{children}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default PageBlock;
|
||||
@ -1,10 +1,7 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: transparent;
|
||||
|
||||
@media (min-width: 48em) {
|
||||
gap: rem(10);
|
||||
}
|
||||
min-height: 86vh;
|
||||
background-color: transparent;
|
||||
}
|
||||
27
src/components/SortableDnd/DragHandle.tsx
Normal file
27
src/components/SortableDnd/DragHandle.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React, { CSSProperties, ReactNode, useContext } from "react";
|
||||
import SortableItemContext from "@/components/SortableDnd/SortableItemContext";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
const DragHandle = ({ children, style }: Props) => {
|
||||
const { attributes, listeners, ref } = useContext(SortableItemContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
style={{
|
||||
width: "100%",
|
||||
cursor: "grab",
|
||||
...style,
|
||||
}}
|
||||
ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DragHandle;
|
||||
130
src/components/SortableDnd/SortableDnd.tsx
Normal file
130
src/components/SortableDnd/SortableDnd.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import React, {
|
||||
CSSProperties,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Active, DndContext, DragEndEvent } from "@dnd-kit/core";
|
||||
import { SortableContext } from "@dnd-kit/sortable";
|
||||
import { LexoRank } from "lexorank";
|
||||
import { Box, Group } from "@mantine/core";
|
||||
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||
import { SortableOverlay } from "@/components/SortableDnd/SortableOverlay";
|
||||
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||
|
||||
type BaseItem = {
|
||||
id: number;
|
||||
lexorank: string;
|
||||
};
|
||||
|
||||
type Props<T extends BaseItem> = {
|
||||
initialItems: T[];
|
||||
renderItem: (item: T) => ReactNode;
|
||||
onDragEnd: (itemId: number, newLexorank: string) => void;
|
||||
onItemClick: (item: T) => void;
|
||||
rowStyle?: CSSProperties;
|
||||
itemStyle?: CSSProperties;
|
||||
};
|
||||
|
||||
const SortableDnd = <T extends BaseItem>({
|
||||
initialItems,
|
||||
renderItem,
|
||||
onDragEnd,
|
||||
onItemClick,
|
||||
rowStyle,
|
||||
itemStyle,
|
||||
}: Props<T>) => {
|
||||
const [active, setActive] = useState<Active | null>(null);
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
const activeItem = useMemo(
|
||||
() => initialItems.find(item => item.id === active?.id),
|
||||
[active, items]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setItems(sortByLexorank(initialItems));
|
||||
}, [initialItems]);
|
||||
|
||||
const sensors = useDndSensors();
|
||||
|
||||
const onDragEndLocal = ({ active, over }: DragEndEvent) => {
|
||||
if (over && active.id !== over?.id && activeItem) {
|
||||
const overIndex: number = items.findIndex(
|
||||
({ id }) => id === over.id
|
||||
);
|
||||
const activeIndex: number = items.findIndex(
|
||||
({ id }) => id === activeItem.id
|
||||
);
|
||||
|
||||
let leftIndex = overIndex;
|
||||
let rightIndex = overIndex + 1;
|
||||
if (overIndex < activeIndex) {
|
||||
leftIndex = overIndex - 1;
|
||||
rightIndex = overIndex;
|
||||
}
|
||||
|
||||
const leftLexorank: LexoRank | null =
|
||||
leftIndex >= 0
|
||||
? LexoRank.parse(items[leftIndex].lexorank)
|
||||
: null;
|
||||
const rightLexorank: LexoRank | null =
|
||||
rightIndex < items.length
|
||||
? LexoRank.parse(items[rightIndex].lexorank)
|
||||
: null;
|
||||
|
||||
const newLexorank = getNewLexorank(
|
||||
leftLexorank,
|
||||
rightLexorank
|
||||
).toString();
|
||||
|
||||
items[activeIndex].lexorank = newLexorank;
|
||||
onDragEnd(items[activeIndex].id, newLexorank);
|
||||
const sortedItems = sortByLexorank(items);
|
||||
setItems([...sortedItems]);
|
||||
}
|
||||
setActive(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
onDragStart={({ active }) => setActive(active)}
|
||||
onDragEnd={onDragEndLocal}
|
||||
onDragCancel={() => setActive(null)}>
|
||||
<SortableContext items={items}>
|
||||
<Group
|
||||
gap={0}
|
||||
style={rowStyle}
|
||||
role="application">
|
||||
{items.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onItemClick(item);
|
||||
}}>
|
||||
<SortableItem
|
||||
dragHandleStyle={{ cursor: "pointer" }}
|
||||
itemStyle={itemStyle}
|
||||
id={item.id}>
|
||||
{renderItem(item)}
|
||||
</SortableItem>
|
||||
</Box>
|
||||
))}
|
||||
</Group>
|
||||
</SortableContext>
|
||||
<SortableOverlay>
|
||||
<div style={{ cursor: "grabbing" }}>
|
||||
{activeItem ? renderItem(activeItem) : null}
|
||||
</div>
|
||||
</SortableOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableDnd;
|
||||
54
src/components/SortableDnd/SortableItem.tsx
Normal file
54
src/components/SortableDnd/SortableItem.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React, { CSSProperties, PropsWithChildren, useMemo } from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import DragHandle from "@/components/SortableDnd/DragHandle";
|
||||
import SortableItemContext from "./SortableItemContext";
|
||||
|
||||
type Props = {
|
||||
id: number | string;
|
||||
itemStyle?: CSSProperties;
|
||||
dragHandleStyle?: CSSProperties;
|
||||
};
|
||||
|
||||
export const SortableItem = ({
|
||||
children,
|
||||
itemStyle,
|
||||
id,
|
||||
dragHandleStyle,
|
||||
}: PropsWithChildren<Props>) => {
|
||||
const {
|
||||
attributes,
|
||||
isDragging,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
setActivatorNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
} = useSortable({ id });
|
||||
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
attributes,
|
||||
listeners,
|
||||
ref: setActivatorNodeRef,
|
||||
}),
|
||||
[attributes, listeners, setActivatorNodeRef]
|
||||
);
|
||||
|
||||
const style: CSSProperties = {
|
||||
opacity: isDragging ? 0.4 : undefined,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
...itemStyle,
|
||||
};
|
||||
|
||||
return (
|
||||
<SortableItemContext.Provider value={context}>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}>
|
||||
<DragHandle style={dragHandleStyle}>{children}</DragHandle>
|
||||
</div>
|
||||
</SortableItemContext.Provider>
|
||||
);
|
||||
};
|
||||
16
src/components/SortableDnd/SortableItemContext.tsx
Normal file
16
src/components/SortableDnd/SortableItemContext.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import type { DraggableSyntheticListeners } from "@dnd-kit/core";
|
||||
import { createContext } from "react";
|
||||
|
||||
interface Context {
|
||||
attributes: Record<string, any>;
|
||||
listeners: DraggableSyntheticListeners;
|
||||
ref: (node: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
const SortableItemContext = createContext<Context>({
|
||||
attributes: {},
|
||||
listeners: undefined,
|
||||
ref() {},
|
||||
});
|
||||
|
||||
export default SortableItemContext;
|
||||
@ -18,7 +18,7 @@ const dropAnimationConfig: DropAnimation = {
|
||||
export function SortableOverlay({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<DragOverlay dropAnimation={dropAnimationConfig}>
|
||||
<div style={{ cursor: "grabbing" }}>{children}</div>
|
||||
{children}
|
||||
</DragOverlay>
|
||||
);
|
||||
}
|
||||
3
src/components/SortableDnd/index.ts
Normal file
3
src/components/SortableDnd/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import SortableDnd from "@/components/SortableDnd/SortableDnd";
|
||||
|
||||
export default SortableDnd;
|
||||
@ -1,42 +0,0 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { Stack } from "@mantine/core";
|
||||
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||
|
||||
type Props<TItem> = {
|
||||
id: string;
|
||||
items: TItem[];
|
||||
renderItem: (item: TItem) => ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const FunnelColumn = <TItem extends BaseDraggable>({
|
||||
id,
|
||||
items,
|
||||
renderItem,
|
||||
children,
|
||||
}: Props<TItem>) => {
|
||||
const { setNodeRef } = useDroppable({ id });
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<SortableContext
|
||||
id={id}
|
||||
items={items}
|
||||
strategy={verticalListSortingStrategy}>
|
||||
<Stack
|
||||
gap="xs"
|
||||
ref={setNodeRef}>
|
||||
{items.map(renderItem)}
|
||||
</Stack>
|
||||
</SortableContext>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FunnelColumn;
|
||||
@ -1,33 +0,0 @@
|
||||
|
||||
.overlay {
|
||||
cursor: grabbing;
|
||||
border-radius: 10px;
|
||||
|
||||
@media (max-width: 48em) {
|
||||
@mixin dark {
|
||||
box-shadow: var(--dark-shadow);
|
||||
}
|
||||
@mixin light {
|
||||
box-shadow: var(--light-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicator {
|
||||
height: 4px;
|
||||
@mixin light {
|
||||
background-color: lightgray;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
@mixin light {
|
||||
background-color: gray;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,159 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import {
|
||||
closestCorners,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverEvent,
|
||||
DragStartEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
horizontalListSortingStrategy,
|
||||
SortableContext,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { Carousel } from "@mantine/carousel";
|
||||
import { Group } from "@mantine/core";
|
||||
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
|
||||
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||
import FunnelColumn from "@/components/dnd/FunnelDnd/FunnelColumn";
|
||||
import FunnelOverlay from "@/components/dnd/FunnelDnd/FunnelOverlay";
|
||||
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
import SortableItem from "../SortableItem";
|
||||
import styles from "./FunnelDnd.module.css";
|
||||
|
||||
type Props<TContainer, TItem> = {
|
||||
containers: TContainer[];
|
||||
items: TItem[];
|
||||
onDragStart: (event: DragStartEvent) => void;
|
||||
onDragOver: (event: DragOverEvent) => void;
|
||||
onDragEnd: (event: DragEndEvent) => void;
|
||||
renderContainer: (container: TContainer, children: ReactNode) => ReactNode;
|
||||
renderContainerOverlay: (
|
||||
container: TContainer,
|
||||
children: ReactNode
|
||||
) => ReactNode;
|
||||
renderItem: (item: TItem) => ReactNode;
|
||||
renderItemOverlay: (item: TItem) => ReactNode;
|
||||
getContainerId: (container: TContainer) => string;
|
||||
getItemsByContainer: (container: TContainer, items: TItem[]) => TItem[];
|
||||
activeContainer: TContainer | null;
|
||||
activeItem: TItem | null;
|
||||
disabledColumns?: boolean;
|
||||
};
|
||||
|
||||
const FunnelDnd = <
|
||||
TContainer extends BaseDraggable,
|
||||
TItem extends BaseDraggable,
|
||||
>({
|
||||
containers,
|
||||
items,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDragEnd,
|
||||
renderContainer,
|
||||
renderContainerOverlay,
|
||||
renderItem,
|
||||
renderItemOverlay,
|
||||
getContainerId,
|
||||
getItemsByContainer,
|
||||
activeContainer,
|
||||
activeItem,
|
||||
disabledColumns = false,
|
||||
}: Props<TContainer, TItem>) => {
|
||||
const sensors = useDndSensors();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const renderContainers = () =>
|
||||
containers.map(container => {
|
||||
const containerItems = getItemsByContainer(container, items);
|
||||
const containerId = getContainerId(container);
|
||||
const item = (
|
||||
<SortableItem
|
||||
key={containerId}
|
||||
id={containerId}
|
||||
disabled={disabledColumns}
|
||||
renderItem={() =>
|
||||
renderContainer(
|
||||
container,
|
||||
<FunnelColumn
|
||||
id={containerId}
|
||||
items={containerItems}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (!isMobile) return item;
|
||||
return <Carousel.Slide key={containerId}>{item}</Carousel.Slide>;
|
||||
});
|
||||
|
||||
const renderBody = () => {
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Carousel
|
||||
slideSize={"90%"}
|
||||
slideGap={"md"}
|
||||
pb={"xl"}
|
||||
withControls={false}
|
||||
withIndicators
|
||||
styles={{
|
||||
container: {
|
||||
marginInline: "10vw",
|
||||
},
|
||||
}}
|
||||
classNames={styles}>
|
||||
{renderContainers()}
|
||||
<CreateStatusButton />
|
||||
</Carousel>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Group
|
||||
gap={"xs"}
|
||||
wrap="nowrap"
|
||||
align="start">
|
||||
{renderContainers()}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}>
|
||||
<SortableContext
|
||||
items={containers.map(getContainerId)}
|
||||
strategy={horizontalListSortingStrategy}>
|
||||
{renderBody()}
|
||||
<FunnelOverlay
|
||||
activeContainer={activeContainer}
|
||||
activeItem={activeItem}
|
||||
renderContainer={container => {
|
||||
const containerItems = getItemsByContainer(
|
||||
container,
|
||||
items
|
||||
);
|
||||
const containerId = getContainerId(container);
|
||||
return renderContainerOverlay(
|
||||
container,
|
||||
<FunnelColumn
|
||||
id={containerId}
|
||||
items={containerItems}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
renderItem={renderItemOverlay}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default FunnelDnd;
|
||||
@ -1,31 +0,0 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/core";
|
||||
import styles from "@/components/dnd/FunnelDnd/FunnelDnd.module.css";
|
||||
|
||||
type Props<TContainer, TItem> = {
|
||||
activeContainer: TContainer | null;
|
||||
activeItem: TItem | null;
|
||||
renderContainer: (container: TContainer) => ReactNode;
|
||||
renderItem: (item: TItem) => ReactNode;
|
||||
};
|
||||
|
||||
const FunnelOverlay = <TContainer, TItem>({
|
||||
activeContainer,
|
||||
activeItem,
|
||||
renderContainer,
|
||||
renderItem,
|
||||
}: Props<TContainer, TItem>) => {
|
||||
return (
|
||||
<DragOverlay dropAnimation={defaultDropAnimation}>
|
||||
<div className={styles.overlay}>
|
||||
{activeItem
|
||||
? renderItem(activeItem)
|
||||
: activeContainer
|
||||
? renderContainer(activeContainer)
|
||||
: null}
|
||||
</div>
|
||||
</DragOverlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default FunnelOverlay;
|
||||
@ -1,3 +0,0 @@
|
||||
import FunnelDnd from "./FunnelDnd";
|
||||
|
||||
export default FunnelDnd;
|
||||
@ -1,151 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, {
|
||||
CSSProperties,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Active, DndContext, DragEndEvent } from "@dnd-kit/core";
|
||||
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
||||
import { SortableContext } from "@dnd-kit/sortable";
|
||||
import { LexoRank } from "lexorank";
|
||||
import { Box, Flex } from "@mantine/core";
|
||||
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||
import { SortableOverlay } from "@/components/dnd/SortableDnd/SortableOverlay";
|
||||
import SortableItem from "@/components/dnd/SortableItem";
|
||||
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||
|
||||
type BaseItem = {
|
||||
id: number;
|
||||
lexorank: string;
|
||||
};
|
||||
|
||||
type Props<T extends BaseItem> = {
|
||||
initialItems: T[];
|
||||
renderItem: (
|
||||
item: T,
|
||||
renderDraggable?: (item: T) => ReactNode
|
||||
) => ReactNode;
|
||||
renderDraggable?: (item: T) => ReactNode; // if not passed - the whole item renders as draggable
|
||||
dragHandleStyle?: CSSProperties;
|
||||
onDragEnd: (itemId: number, newLexorank: string) => void;
|
||||
onItemClick?: (item: T) => void;
|
||||
containerStyle?: CSSProperties;
|
||||
vertical?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const SortableDnd = <T extends BaseItem>({
|
||||
initialItems,
|
||||
renderItem,
|
||||
renderDraggable,
|
||||
dragHandleStyle,
|
||||
onDragEnd,
|
||||
onItemClick,
|
||||
containerStyle,
|
||||
vertical,
|
||||
disabled = false,
|
||||
}: Props<T>) => {
|
||||
const [active, setActive] = useState<Active | null>(null);
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
const activeItem = useMemo(
|
||||
() => initialItems.find(item => item.id === active?.id),
|
||||
[active, items]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setItems(sortByLexorank(initialItems));
|
||||
}, [initialItems]);
|
||||
|
||||
const sensors = useDndSensors();
|
||||
|
||||
const onDragEndLocal = ({ active, over }: DragEndEvent) => {
|
||||
if (!over || active.id === over?.id || !activeItem) {
|
||||
setActive(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const overIndex: number = items.findIndex(({ id }) => id === over.id);
|
||||
const activeIndex: number = items.findIndex(
|
||||
({ id }) => id === activeItem.id
|
||||
);
|
||||
|
||||
let leftIndex = overIndex;
|
||||
let rightIndex = overIndex + 1;
|
||||
if (overIndex < activeIndex) {
|
||||
leftIndex = overIndex - 1;
|
||||
rightIndex = overIndex;
|
||||
}
|
||||
|
||||
const leftLexorank: LexoRank | null =
|
||||
leftIndex >= 0 ? LexoRank.parse(items[leftIndex].lexorank) : null;
|
||||
const rightLexorank: LexoRank | null =
|
||||
rightIndex < items.length
|
||||
? LexoRank.parse(items[rightIndex].lexorank)
|
||||
: null;
|
||||
|
||||
const newLexorank = getNewLexorank(
|
||||
leftLexorank,
|
||||
rightLexorank
|
||||
).toString();
|
||||
|
||||
items[activeIndex].lexorank = newLexorank;
|
||||
onDragEnd(items[activeIndex].id, newLexorank);
|
||||
const sortedItems = sortByLexorank(items);
|
||||
setItems([...sortedItems]);
|
||||
setActive(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
modifiers={[restrictToHorizontalAxis]}
|
||||
sensors={sensors}
|
||||
onDragStart={({ active }) => setActive(active)}
|
||||
onDragEnd={onDragEndLocal}
|
||||
onDragCancel={() => setActive(null)}>
|
||||
<SortableContext
|
||||
items={items}
|
||||
disabled={disabled}>
|
||||
<Flex
|
||||
gap={0}
|
||||
style={{
|
||||
flexWrap: "nowrap",
|
||||
flexDirection: vertical ? "column" : "row",
|
||||
...containerStyle,
|
||||
}}
|
||||
role="application">
|
||||
{items.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
onClick={e => {
|
||||
if (!onItemClick) return;
|
||||
e.stopPropagation();
|
||||
onItemClick(item);
|
||||
}}>
|
||||
<SortableItem
|
||||
id={item.id}
|
||||
disabled={disabled}
|
||||
renderItem={renderDraggable =>
|
||||
renderItem(item, renderDraggable)
|
||||
}
|
||||
renderDraggable={
|
||||
renderDraggable
|
||||
? () => renderDraggable(item)
|
||||
: undefined
|
||||
}
|
||||
dragHandleStyle={dragHandleStyle}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
</SortableContext>
|
||||
<SortableOverlay>
|
||||
{activeItem ? renderItem(activeItem, renderDraggable) : null}
|
||||
</SortableOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableDnd;
|
||||
@ -1,3 +0,0 @@
|
||||
import SortableDnd from "@/components/dnd/SortableDnd/SortableDnd";
|
||||
|
||||
export default SortableDnd;
|
||||
@ -1,32 +0,0 @@
|
||||
import React, { CSSProperties, ReactNode } from "react";
|
||||
import { useDraggable } from "@dnd-kit/core";
|
||||
|
||||
type Props = {
|
||||
id: number | string;
|
||||
children: ReactNode;
|
||||
style?: CSSProperties;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const DragHandle = ({ id, children, style, disabled }: Props) => {
|
||||
const { attributes, listeners, setNodeRef } = useDraggable({
|
||||
id,
|
||||
disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
style={{
|
||||
width: "100wv",
|
||||
cursor: disabled ? "default" : "grab",
|
||||
...style,
|
||||
}}
|
||||
ref={setNodeRef}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DragHandle;
|
||||
@ -1,59 +0,0 @@
|
||||
import React, { CSSProperties, ReactNode } from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import DragHandle from "./DragHandle";
|
||||
|
||||
type Props = {
|
||||
id: number | string;
|
||||
renderItem: (renderDraggable?: () => ReactNode) => ReactNode;
|
||||
renderDraggable?: () => ReactNode; // if not passed - the whole item renders as draggable
|
||||
disabled?: boolean;
|
||||
dragHandleStyle?: CSSProperties;
|
||||
};
|
||||
|
||||
const SortableItem = ({
|
||||
renderItem,
|
||||
dragHandleStyle,
|
||||
renderDraggable,
|
||||
id,
|
||||
disabled = false,
|
||||
}: Props) => {
|
||||
const { isDragging, setNodeRef, transform, transition } = useSortable({
|
||||
id,
|
||||
animateLayoutChanges: () => false,
|
||||
});
|
||||
|
||||
const style: CSSProperties = {
|
||||
opacity: isDragging ? 0.4 : undefined,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
const renderDragHandle = () => (
|
||||
<DragHandle
|
||||
id={id}
|
||||
style={dragHandleStyle}
|
||||
disabled={disabled}>
|
||||
{renderDraggable && renderDraggable()}
|
||||
</DragHandle>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}>
|
||||
{renderDraggable ? (
|
||||
renderItem(renderDragHandle)
|
||||
) : (
|
||||
<DragHandle
|
||||
id={id}
|
||||
style={dragHandleStyle}
|
||||
disabled={disabled}>
|
||||
{renderItem()}
|
||||
</DragHandle>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableItem;
|
||||
@ -1,3 +0,0 @@
|
||||
import SortableItem from "./SortableItem";
|
||||
|
||||
export default SortableItem;
|
||||
@ -1,3 +0,0 @@
|
||||
export type BaseDraggable = {
|
||||
id: number;
|
||||
};
|
||||
@ -1,61 +0,0 @@
|
||||
.container {
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-8);
|
||||
}
|
||||
@mixin light {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 48em) {
|
||||
padding: var(--mantine-spacing-md);
|
||||
|
||||
@mixin dark {
|
||||
box-shadow: var(--dark-thick-shadow);
|
||||
}
|
||||
@mixin light {
|
||||
box-shadow: var(--light-thick-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-padding-height {
|
||||
height: 100% !important;
|
||||
@media (min-width: 48em) {
|
||||
height: 89vh;
|
||||
}
|
||||
}
|
||||
|
||||
.container-full-height {
|
||||
min-height: calc(100vh - (var(--mantine-spacing-md) * 2));
|
||||
}
|
||||
|
||||
.container-full-height-fixed {
|
||||
height: calc(100vh - (var(--mantine-spacing-md) * 2));
|
||||
}
|
||||
|
||||
.container-no-border-radius {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.container-full-screen-mobile {
|
||||
@media (max-width: 48em) {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
border-radius: 0 !important;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.transparent {
|
||||
@media (min-width: 48em) {
|
||||
box-shadow: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
|
||||
.container {
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7-5);
|
||||
box-shadow: var(--dark-shadow);
|
||||
}
|
||||
@mixin light {
|
||||
background-color: var(--color-light-whitesmoke);
|
||||
box-shadow: var(--light-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.container-active {
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-8);
|
||||
box-shadow: var(--dark-thick-shadow);
|
||||
}
|
||||
@mixin light {
|
||||
background-color: white;
|
||||
box-shadow: var(--light-thick-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-8);
|
||||
box-shadow: var(--dark-thick-shadow);
|
||||
}
|
||||
@mixin light {
|
||||
background-color: white;
|
||||
box-shadow: var(--light-thick-shadow);
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import { CSSProperties, FC, ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import styles from "@/components/layout/SmallPageBlock/SmallPageBlock.module.css";
|
||||
import { Box } from "@mantine/core";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
style?: CSSProperties;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
const SmallPageBlock: FC<Props> = ({ children, style, active = false }) => {
|
||||
return (
|
||||
<Box
|
||||
bdrs={"lg"}
|
||||
className={classNames(
|
||||
styles.container,
|
||||
active && styles["container-active"]
|
||||
)}
|
||||
style={style}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default SmallPageBlock;
|
||||
@ -1,26 +0,0 @@
|
||||
.container {
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: rem(18px);
|
||||
height: rem(18px);
|
||||
}
|
||||
|
||||
.light {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global([data-mantine-color-scheme='dark']) .light {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global([data-mantine-color-scheme='dark']) .dark {
|
||||
display: block;
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { IconMoon, IconSun } from "@tabler/icons-react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
ActionIcon,
|
||||
useComputedColorScheme,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import style from "./ColorSchemeToggle.module.css";
|
||||
|
||||
export function ColorSchemeToggle() {
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
const computedColorScheme = useComputedColorScheme(undefined, {
|
||||
getInitialValueInEffect: true,
|
||||
});
|
||||
|
||||
const toggleColorScheme = () => {
|
||||
setColorScheme(computedColorScheme === "light" ? "dark" : "light");
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
onClick={toggleColorScheme}
|
||||
variant="default"
|
||||
size="xl"
|
||||
radius="lg"
|
||||
aria-label="Toggle color scheme"
|
||||
className={style.container}>
|
||||
<IconSun
|
||||
className={classNames(style.icon, style.light)}
|
||||
stroke={1.5}
|
||||
/>
|
||||
<IconMoon
|
||||
className={classNames(style.icon, style.dark)}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import { Styles } from "@mantine/core/lib/core/styles-api/styles-api.types";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
import InPlaceInputDesktop from "./InPlaceInputDesktop";
|
||||
import InPlaceInputMobile from "./InPlaceInputMobile";
|
||||
|
||||
type Props = {
|
||||
defaultValue?: string;
|
||||
onComplete: (value: string) => void;
|
||||
placeholder?: string;
|
||||
getChildren: (startEditing: () => void) => ReactNode;
|
||||
inputStyles?: Styles<any>;
|
||||
modalTitle?: string;
|
||||
};
|
||||
|
||||
const InPlaceInput: FC<Props> = (props) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (isMobile) {
|
||||
return <InPlaceInputMobile {...props} />;
|
||||
}
|
||||
|
||||
return <InPlaceInputDesktop {...props} />;
|
||||
};
|
||||
|
||||
export default InPlaceInput;
|
||||
@ -1,86 +0,0 @@
|
||||
import React, { FC, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { TextInput } from "@mantine/core";
|
||||
import { Styles } from "@mantine/core/lib/core/styles-api/styles-api.types";
|
||||
|
||||
type Props = {
|
||||
defaultValue?: string;
|
||||
onComplete: (value: string) => void;
|
||||
placeholder?: string;
|
||||
getChildren: (startEditing: () => void) => ReactNode;
|
||||
inputStyles?: Styles<any>;
|
||||
};
|
||||
|
||||
const InPlaceInputDesktop: FC<Props> = ({
|
||||
onComplete,
|
||||
placeholder,
|
||||
inputStyles,
|
||||
getChildren,
|
||||
defaultValue = "",
|
||||
}) => {
|
||||
const [isWriting, setIsWriting] = useState<boolean>(false);
|
||||
const [value, setValue] = useState<string>(defaultValue);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isWriting && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isWriting]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
isWriting &&
|
||||
inputRef.current &&
|
||||
!inputRef.current.contains(event.target as Node)
|
||||
) {
|
||||
onCompleteCreating();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () =>
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [isWriting, value]);
|
||||
|
||||
const onStartCreating = () => {
|
||||
setValue(defaultValue);
|
||||
setIsWriting(true);
|
||||
};
|
||||
|
||||
const onCancelCreating = () => {
|
||||
setValue(defaultValue);
|
||||
setIsWriting(false);
|
||||
};
|
||||
|
||||
const onCompleteCreating = () => {
|
||||
const localValue = value.trim();
|
||||
if (localValue) {
|
||||
onComplete(localValue);
|
||||
}
|
||||
setIsWriting(false);
|
||||
};
|
||||
|
||||
if (isWriting) {
|
||||
return (
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
placeholder={placeholder}
|
||||
variant={"unstyled"}
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
if (e.key === "Enter") onCompleteCreating();
|
||||
if (e.key === "Escape") onCancelCreating();
|
||||
}}
|
||||
styles={inputStyles}
|
||||
miw={150}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return getChildren(onStartCreating);
|
||||
};
|
||||
|
||||
export default InPlaceInputDesktop;
|
||||
@ -1,32 +0,0 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
type Props = {
|
||||
defaultValue?: string;
|
||||
onComplete: (value: string) => void;
|
||||
getChildren: (startEditing: () => void) => ReactNode;
|
||||
modalTitle?: string;
|
||||
};
|
||||
|
||||
const InPlaceInputMobile: FC<Props> = ({
|
||||
onComplete,
|
||||
getChildren,
|
||||
modalTitle = "",
|
||||
defaultValue = "",
|
||||
}) => {
|
||||
const onStartCreating = () => {
|
||||
modals.openContextModal({
|
||||
modal: "enterNameModal",
|
||||
title: modalTitle,
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
onComplete,
|
||||
defaultValue,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return getChildren(onStartCreating);
|
||||
};
|
||||
|
||||
export default InPlaceInputMobile;
|
||||
@ -1,6 +1,6 @@
|
||||
import type { CreateClientConfig } from "@/lib/client/client.gen";
|
||||
import type {CreateClientConfig} from "@/lib/client/client.gen";
|
||||
|
||||
export const createClientConfig: CreateClientConfig = config => ({
|
||||
...config,
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
||||
});
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
import React from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import { LexoRank } from "lexorank";
|
||||
import { Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import {
|
||||
BoardSchema,
|
||||
HttpValidationError,
|
||||
UpdateBoardSchema,
|
||||
} from "@/lib/client";
|
||||
import {
|
||||
createBoardMutation,
|
||||
deleteBoardMutation,
|
||||
updateBoardMutation,
|
||||
} from "@/lib/client/@tanstack/react-query.gen";
|
||||
import { notifications } from "@/lib/notifications";
|
||||
import { getMaxByLexorank, getNewLexorank } from "@/utils/lexorank";
|
||||
|
||||
type UseBoardsOperationsProps = {
|
||||
boards: BoardSchema[];
|
||||
setBoards: React.Dispatch<React.SetStateAction<BoardSchema[]>>;
|
||||
refetchBoards: () => void;
|
||||
projectId?: number;
|
||||
};
|
||||
|
||||
type BoardsOperations = {
|
||||
onCreateBoard: (name: string) => void;
|
||||
onUpdateBoard: (boardId: number, board: UpdateBoardSchema) => void;
|
||||
onDeleteBoard: (board: BoardSchema) => void;
|
||||
};
|
||||
|
||||
export const useBoardsOperations = ({
|
||||
boards,
|
||||
setBoards,
|
||||
refetchBoards,
|
||||
projectId,
|
||||
}: UseBoardsOperationsProps): BoardsOperations => {
|
||||
const onError = (error: AxiosError<HttpValidationError>) => {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: error.response?.data?.detail as string | undefined,
|
||||
});
|
||||
refetchBoards();
|
||||
};
|
||||
|
||||
const createBoard = useMutation({
|
||||
...createBoardMutation(),
|
||||
onError,
|
||||
onSuccess: res => {
|
||||
setBoards([...boards, res.board]);
|
||||
},
|
||||
});
|
||||
|
||||
const updateBoard = useMutation({
|
||||
...updateBoardMutation(),
|
||||
onError,
|
||||
});
|
||||
|
||||
const deleteBoard = useMutation({
|
||||
...deleteBoardMutation(),
|
||||
onError,
|
||||
});
|
||||
|
||||
const onCreateBoard = (name: string) => {
|
||||
if (!projectId) return;
|
||||
const lastBoard = getMaxByLexorank(boards);
|
||||
const newLexorank = getNewLexorank(
|
||||
lastBoard ? LexoRank.parse(lastBoard.lexorank) : null
|
||||
);
|
||||
|
||||
createBoard.mutate({
|
||||
body: {
|
||||
board: {
|
||||
name,
|
||||
projectId,
|
||||
lexorank: newLexorank.toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onUpdateBoard = (boardId: number, board: UpdateBoardSchema) => {
|
||||
updateBoard.mutate({
|
||||
path: { boardId },
|
||||
body: { board },
|
||||
});
|
||||
|
||||
setBoards(boards =>
|
||||
boards.map(oldBoard =>
|
||||
oldBoard.id !== boardId
|
||||
? oldBoard
|
||||
: {
|
||||
id: oldBoard.id,
|
||||
name: board.name ? board.name : oldBoard.name,
|
||||
lexorank: board.lexorank
|
||||
? board.lexorank
|
||||
: oldBoard.lexorank,
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const onDeleteBoard = (board: BoardSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление доски",
|
||||
children: (
|
||||
<Text>
|
||||
Вы уверены, что хотите удалить доску "{board.name}"?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => {
|
||||
deleteBoard.mutate({
|
||||
path: { boardId: board.id },
|
||||
});
|
||||
setBoards(boards => boards.filter(b => b.id !== board.id));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onCreateBoard,
|
||||
onUpdateBoard,
|
||||
onDeleteBoard,
|
||||
};
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
|
||||
const useIsMobile = (): boolean => {
|
||||
return useMediaQuery("(max-width: 48em)");
|
||||
};
|
||||
|
||||
export default useIsMobile;
|
||||
@ -1,22 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ProjectSchema } from "@/lib/client";
|
||||
import { getProjectsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||
|
||||
const useProjectsList = () => {
|
||||
const [projects, setProjects] = useState<ProjectSchema[]>([]);
|
||||
|
||||
const { data, refetch, isLoading } = useQuery({
|
||||
...getProjectsOptions(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.projects) {
|
||||
setProjects(data.projects);
|
||||
}
|
||||
}, [data?.projects]);
|
||||
|
||||
return { projects, setProjects, refetch, isLoading };
|
||||
const projects = !data ? [] : data.projects;
|
||||
return { projects, refetch, isLoading };
|
||||
};
|
||||
|
||||
export default useProjectsList;
|
||||
|
||||
@ -1,118 +0,0 @@
|
||||
import React from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import { Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import {
|
||||
HttpValidationError,
|
||||
ProjectSchema,
|
||||
UpdateProjectSchema,
|
||||
} from "@/lib/client";
|
||||
import {
|
||||
createProjectMutation,
|
||||
deleteProjectMutation,
|
||||
updateProjectMutation,
|
||||
} from "@/lib/client/@tanstack/react-query.gen";
|
||||
import { notifications } from "@/lib/notifications";
|
||||
|
||||
type Props = {
|
||||
projects: ProjectSchema[];
|
||||
setProjects: React.Dispatch<React.SetStateAction<ProjectSchema[]>>;
|
||||
refetchProjects: () => void;
|
||||
};
|
||||
|
||||
type ProjectsOperations = {
|
||||
onCreateProject: (name: string) => void;
|
||||
onUpdateProject: (projectId: number, project: UpdateProjectSchema) => void;
|
||||
onDeleteProject: (project: ProjectSchema) => void;
|
||||
};
|
||||
|
||||
export const useProjectsOperations = ({
|
||||
projects,
|
||||
setProjects,
|
||||
refetchProjects,
|
||||
}: Props): ProjectsOperations => {
|
||||
const onError = (error: AxiosError<HttpValidationError>) => {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: error.response?.data?.detail as string | undefined,
|
||||
});
|
||||
refetchProjects();
|
||||
};
|
||||
|
||||
const createProject = useMutation({
|
||||
...createProjectMutation(),
|
||||
onError,
|
||||
onSuccess: res => {
|
||||
setProjects([...projects, res.project]);
|
||||
},
|
||||
});
|
||||
|
||||
const updateProject = useMutation({
|
||||
...updateProjectMutation(),
|
||||
onError,
|
||||
});
|
||||
|
||||
const deleteProject = useMutation({
|
||||
...deleteProjectMutation(),
|
||||
onError,
|
||||
});
|
||||
|
||||
const onCreateProject = (name: string) => {
|
||||
createProject.mutate({
|
||||
body: {
|
||||
project: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onUpdateProject = (
|
||||
projectId: number,
|
||||
project: UpdateProjectSchema
|
||||
) => {
|
||||
updateProject.mutate({
|
||||
query: { projectId },
|
||||
body: { project },
|
||||
});
|
||||
|
||||
setProjects(boards =>
|
||||
boards.map(oldProject =>
|
||||
oldProject.id !== projectId
|
||||
? oldProject
|
||||
: {
|
||||
id: oldProject.id,
|
||||
name: project.name ? project.name : oldProject.name,
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const onDeleteProject = (project: ProjectSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление проекта",
|
||||
children: (
|
||||
<Text>
|
||||
Вы уверены, что хотите удалить проект "{project.name}"?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => {
|
||||
deleteProject.mutate({
|
||||
query: { projectId: project.id },
|
||||
});
|
||||
setProjects(projects =>
|
||||
projects.filter(p => p.id !== project.id)
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onCreateProject,
|
||||
onUpdateProject,
|
||||
onDeleteProject,
|
||||
};
|
||||
};
|
||||
@ -1,130 +0,0 @@
|
||||
import React from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import { LexoRank } from "lexorank";
|
||||
import { Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import {
|
||||
HttpValidationError,
|
||||
StatusSchema,
|
||||
UpdateStatusSchema,
|
||||
} from "@/lib/client";
|
||||
import {
|
||||
createStatusMutation,
|
||||
deleteStatusMutation,
|
||||
updateStatusMutation,
|
||||
} from "@/lib/client/@tanstack/react-query.gen";
|
||||
import { notifications } from "@/lib/notifications";
|
||||
import { getMaxByLexorank, getNewLexorank } from "@/utils/lexorank";
|
||||
|
||||
type Props = {
|
||||
statuses: StatusSchema[];
|
||||
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
||||
refetchStatuses: () => void;
|
||||
boardId?: number;
|
||||
};
|
||||
|
||||
type StatusesOperations = {
|
||||
onCreateStatus: (name: string) => void;
|
||||
onUpdateStatus: (statusId: number, status: UpdateStatusSchema) => void;
|
||||
onDeleteStatus: (status: StatusSchema) => void;
|
||||
};
|
||||
|
||||
export const useStatusesOperations = ({
|
||||
statuses,
|
||||
setStatuses,
|
||||
refetchStatuses,
|
||||
boardId,
|
||||
}: Props): StatusesOperations => {
|
||||
const onError = (error: AxiosError<HttpValidationError>) => {
|
||||
console.error(error);
|
||||
notifications.error({
|
||||
message: error.response?.data?.detail as string | undefined,
|
||||
});
|
||||
refetchStatuses();
|
||||
};
|
||||
|
||||
const createStatus = useMutation({
|
||||
...createStatusMutation(),
|
||||
onError,
|
||||
onSuccess: res => {
|
||||
setStatuses([...statuses, res.status]);
|
||||
},
|
||||
});
|
||||
|
||||
const updateStatus = useMutation({
|
||||
...updateStatusMutation(),
|
||||
onError,
|
||||
});
|
||||
|
||||
const deleteStatus = useMutation({
|
||||
...deleteStatusMutation(),
|
||||
onError,
|
||||
});
|
||||
|
||||
const onCreateStatus = (name: string) => {
|
||||
if (!boardId) return;
|
||||
const lastStatus = getMaxByLexorank(statuses);
|
||||
const newLexorank = getNewLexorank(
|
||||
lastStatus ? LexoRank.parse(lastStatus.lexorank) : null
|
||||
);
|
||||
|
||||
createStatus.mutate({
|
||||
body: {
|
||||
status: {
|
||||
name,
|
||||
boardId,
|
||||
lexorank: newLexorank.toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onUpdateStatus = (statusId: number, status: UpdateStatusSchema) => {
|
||||
updateStatus.mutate({
|
||||
path: { statusId },
|
||||
body: { status },
|
||||
});
|
||||
|
||||
setStatuses(statuses =>
|
||||
statuses.map(oldStatus =>
|
||||
oldStatus.id !== statusId
|
||||
? oldStatus
|
||||
: {
|
||||
id: oldStatus.id,
|
||||
name: status.name ? status.name : oldStatus.name,
|
||||
lexorank: status.lexorank
|
||||
? status.lexorank
|
||||
: oldStatus.lexorank,
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const onDeleteStatus = (status: StatusSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление колонки",
|
||||
children: (
|
||||
<Text>
|
||||
Вы уверены, что хотите удалить колонку "{status.name}"?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => {
|
||||
deleteStatus.mutate({
|
||||
path: { statusId: status.id },
|
||||
});
|
||||
setStatuses(statuses =>
|
||||
statuses.filter(s => s.id !== status.id)
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onCreateStatus,
|
||||
onUpdateStatus,
|
||||
onDeleteStatus,
|
||||
};
|
||||
};
|
||||
@ -1,80 +1,22 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { queryOptions, type UseMutationOptions } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { client as _heyApiClient } from "../client.gen";
|
||||
import {
|
||||
createBoard,
|
||||
createProject,
|
||||
createStatus,
|
||||
deleteBoard,
|
||||
deleteProject,
|
||||
deleteStatus,
|
||||
getBoards,
|
||||
getDeals,
|
||||
getProjects,
|
||||
getStatuses,
|
||||
updateBoard,
|
||||
updateDeal,
|
||||
updateProject,
|
||||
updateStatus,
|
||||
type Options,
|
||||
} from "../sdk.gen";
|
||||
import type {
|
||||
CreateBoardData,
|
||||
CreateBoardError,
|
||||
CreateBoardResponse2,
|
||||
CreateProjectData,
|
||||
CreateProjectError,
|
||||
CreateProjectResponse2,
|
||||
CreateStatusData,
|
||||
CreateStatusError,
|
||||
CreateStatusResponse2,
|
||||
DeleteBoardData,
|
||||
DeleteBoardError,
|
||||
DeleteBoardResponse2,
|
||||
DeleteProjectData,
|
||||
DeleteProjectError,
|
||||
DeleteProjectResponse2,
|
||||
DeleteStatusData,
|
||||
DeleteStatusError,
|
||||
DeleteStatusResponse2,
|
||||
GetBoardsData,
|
||||
GetDealsData,
|
||||
GetProjectsData,
|
||||
GetStatusesData,
|
||||
UpdateBoardData,
|
||||
UpdateBoardError,
|
||||
UpdateBoardResponse2,
|
||||
UpdateDealData,
|
||||
UpdateDealError,
|
||||
UpdateDealResponse2,
|
||||
UpdateProjectData,
|
||||
UpdateProjectError,
|
||||
UpdateProjectResponse2,
|
||||
UpdateStatusData,
|
||||
UpdateStatusError,
|
||||
UpdateStatusResponse2,
|
||||
} from "../types.gen";
|
||||
import { type Options, getBoards, updateBoard, getDeals, updateDeal, getProjects, getStatuses, updateStatus } from '../sdk.gen';
|
||||
import { queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
||||
import type { GetBoardsData, UpdateBoardData, UpdateBoardError, UpdateBoardResponse2, GetDealsData, UpdateDealData, UpdateDealError, UpdateDealResponse2, GetProjectsData, GetStatusesData, UpdateStatusData, UpdateStatusError, UpdateStatusResponse2 } from '../types.gen';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { client as _heyApiClient } from '../client.gen';
|
||||
|
||||
export type QueryKey<TOptions extends Options> = [
|
||||
Pick<TOptions, "baseURL" | "body" | "headers" | "path" | "query"> & {
|
||||
Pick<TOptions, 'baseURL' | 'body' | 'headers' | 'path' | 'query'> & {
|
||||
_id: string;
|
||||
_infinite?: boolean;
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
const createQueryKey = <TOptions extends Options>(
|
||||
id: string,
|
||||
options?: TOptions,
|
||||
infinite?: boolean
|
||||
): [QueryKey<TOptions>[0]] => {
|
||||
const params: QueryKey<TOptions>[0] = {
|
||||
_id: id,
|
||||
baseURL:
|
||||
options?.baseURL ||
|
||||
(options?.client ?? _heyApiClient).getConfig().baseURL,
|
||||
} as QueryKey<TOptions>[0];
|
||||
const createQueryKey = <TOptions extends Options>(id: string, options?: TOptions, infinite?: boolean): [
|
||||
QueryKey<TOptions>[0]
|
||||
] => {
|
||||
const params: QueryKey<TOptions>[0] = { _id: id, baseURL: options?.baseURL || (options?.client ?? _heyApiClient).getConfig().baseURL } as QueryKey<TOptions>[0];
|
||||
if (infinite) {
|
||||
params._infinite = infinite;
|
||||
}
|
||||
@ -90,11 +32,12 @@ const createQueryKey = <TOptions extends Options>(
|
||||
if (options?.query) {
|
||||
params.query = options.query;
|
||||
}
|
||||
return [params];
|
||||
return [
|
||||
params
|
||||
];
|
||||
};
|
||||
|
||||
export const getBoardsQueryKey = (options: Options<GetBoardsData>) =>
|
||||
createQueryKey("getBoards", options);
|
||||
export const getBoardsQueryKey = (options: Options<GetBoardsData>) => createQueryKey('getBoards', options);
|
||||
|
||||
/**
|
||||
* Get Boards
|
||||
@ -106,118 +49,32 @@ export const getBoardsOptions = (options: Options<GetBoardsData>) => {
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: getBoardsQueryKey(options),
|
||||
queryKey: getBoardsQueryKey(options)
|
||||
});
|
||||
};
|
||||
|
||||
export const createBoardQueryKey = (options: Options<CreateBoardData>) =>
|
||||
createQueryKey("createBoard", options);
|
||||
|
||||
/**
|
||||
* Create Board
|
||||
*/
|
||||
export const createBoardOptions = (options: Options<CreateBoardData>) => {
|
||||
return queryOptions({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await createBoard({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: createBoardQueryKey(options),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Board
|
||||
*/
|
||||
export const createBoardMutation = (
|
||||
options?: Partial<Options<CreateBoardData>>
|
||||
): UseMutationOptions<
|
||||
CreateBoardResponse2,
|
||||
AxiosError<CreateBoardError>,
|
||||
Options<CreateBoardData>
|
||||
> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
CreateBoardResponse2,
|
||||
AxiosError<CreateBoardError>,
|
||||
Options<CreateBoardData>
|
||||
> = {
|
||||
mutationFn: async localOptions => {
|
||||
const { data } = await createBoard({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete Board
|
||||
*/
|
||||
export const deleteBoardMutation = (
|
||||
options?: Partial<Options<DeleteBoardData>>
|
||||
): UseMutationOptions<
|
||||
DeleteBoardResponse2,
|
||||
AxiosError<DeleteBoardError>,
|
||||
Options<DeleteBoardData>
|
||||
> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
DeleteBoardResponse2,
|
||||
AxiosError<DeleteBoardError>,
|
||||
Options<DeleteBoardData>
|
||||
> = {
|
||||
mutationFn: async localOptions => {
|
||||
const { data } = await deleteBoard({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update Board
|
||||
*/
|
||||
export const updateBoardMutation = (
|
||||
options?: Partial<Options<UpdateBoardData>>
|
||||
): UseMutationOptions<
|
||||
UpdateBoardResponse2,
|
||||
AxiosError<UpdateBoardError>,
|
||||
Options<UpdateBoardData>
|
||||
> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
UpdateBoardResponse2,
|
||||
AxiosError<UpdateBoardError>,
|
||||
Options<UpdateBoardData>
|
||||
> = {
|
||||
mutationFn: async localOptions => {
|
||||
export const updateBoardMutation = (options?: Partial<Options<UpdateBoardData>>): UseMutationOptions<UpdateBoardResponse2, AxiosError<UpdateBoardError>, Options<UpdateBoardData>> => {
|
||||
const mutationOptions: UseMutationOptions<UpdateBoardResponse2, AxiosError<UpdateBoardError>, Options<UpdateBoardData>> = {
|
||||
mutationFn: async (localOptions) => {
|
||||
const { data } = await updateBoard({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
},
|
||||
}
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const getDealsQueryKey = (options: Options<GetDealsData>) =>
|
||||
createQueryKey("getDeals", options);
|
||||
export const getDealsQueryKey = (options: Options<GetDealsData>) => createQueryKey('getDeals', options);
|
||||
|
||||
/**
|
||||
* Get Deals
|
||||
@ -229,43 +86,32 @@ export const getDealsOptions = (options: Options<GetDealsData>) => {
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: getDealsQueryKey(options),
|
||||
queryKey: getDealsQueryKey(options)
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update Deal
|
||||
*/
|
||||
export const updateDealMutation = (
|
||||
options?: Partial<Options<UpdateDealData>>
|
||||
): UseMutationOptions<
|
||||
UpdateDealResponse2,
|
||||
AxiosError<UpdateDealError>,
|
||||
Options<UpdateDealData>
|
||||
> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
UpdateDealResponse2,
|
||||
AxiosError<UpdateDealError>,
|
||||
Options<UpdateDealData>
|
||||
> = {
|
||||
mutationFn: async localOptions => {
|
||||
export const updateDealMutation = (options?: Partial<Options<UpdateDealData>>): UseMutationOptions<UpdateDealResponse2, AxiosError<UpdateDealError>, Options<UpdateDealData>> => {
|
||||
const mutationOptions: UseMutationOptions<UpdateDealResponse2, AxiosError<UpdateDealError>, Options<UpdateDealData>> = {
|
||||
mutationFn: async (localOptions) => {
|
||||
const { data } = await updateDeal({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
},
|
||||
}
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const getProjectsQueryKey = (options?: Options<GetProjectsData>) =>
|
||||
createQueryKey("getProjects", options);
|
||||
export const getProjectsQueryKey = (options?: Options<GetProjectsData>) => createQueryKey('getProjects', options);
|
||||
|
||||
/**
|
||||
* Get Projects
|
||||
@ -277,118 +123,15 @@ export const getProjectsOptions = (options?: Options<GetProjectsData>) => {
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: getProjectsQueryKey(options),
|
||||
queryKey: getProjectsQueryKey(options)
|
||||
});
|
||||
};
|
||||
|
||||
export const createProjectQueryKey = (options: Options<CreateProjectData>) =>
|
||||
createQueryKey("createProject", options);
|
||||
|
||||
/**
|
||||
* Create Project
|
||||
*/
|
||||
export const createProjectOptions = (options: Options<CreateProjectData>) => {
|
||||
return queryOptions({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await createProject({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: createProjectQueryKey(options),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Project
|
||||
*/
|
||||
export const createProjectMutation = (
|
||||
options?: Partial<Options<CreateProjectData>>
|
||||
): UseMutationOptions<
|
||||
CreateProjectResponse2,
|
||||
AxiosError<CreateProjectError>,
|
||||
Options<CreateProjectData>
|
||||
> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
CreateProjectResponse2,
|
||||
AxiosError<CreateProjectError>,
|
||||
Options<CreateProjectData>
|
||||
> = {
|
||||
mutationFn: async localOptions => {
|
||||
const { data } = await createProject({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete Project
|
||||
*/
|
||||
export const deleteProjectMutation = (
|
||||
options?: Partial<Options<DeleteProjectData>>
|
||||
): UseMutationOptions<
|
||||
DeleteProjectResponse2,
|
||||
AxiosError<DeleteProjectError>,
|
||||
Options<DeleteProjectData>
|
||||
> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
DeleteProjectResponse2,
|
||||
AxiosError<DeleteProjectError>,
|
||||
Options<DeleteProjectData>
|
||||
> = {
|
||||
mutationFn: async localOptions => {
|
||||
const { data } = await deleteProject({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update Project
|
||||
*/
|
||||
export const updateProjectMutation = (
|
||||
options?: Partial<Options<UpdateProjectData>>
|
||||
): UseMutationOptions<
|
||||
UpdateProjectResponse2,
|
||||
AxiosError<UpdateProjectError>,
|
||||
Options<UpdateProjectData>
|
||||
> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
UpdateProjectResponse2,
|
||||
AxiosError<UpdateProjectError>,
|
||||
Options<UpdateProjectData>
|
||||
> = {
|
||||
mutationFn: async localOptions => {
|
||||
const { data } = await updateProject({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const getStatusesQueryKey = (options: Options<GetStatusesData>) =>
|
||||
createQueryKey("getStatuses", options);
|
||||
export const getStatusesQueryKey = (options: Options<GetStatusesData>) => createQueryKey('getStatuses', options);
|
||||
|
||||
/**
|
||||
* Get Statuses
|
||||
@ -400,112 +143,27 @@ export const getStatusesOptions = (options: Options<GetStatusesData>) => {
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: getStatusesQueryKey(options),
|
||||
queryKey: getStatusesQueryKey(options)
|
||||
});
|
||||
};
|
||||
|
||||
export const createStatusQueryKey = (options: Options<CreateStatusData>) =>
|
||||
createQueryKey("createStatus", options);
|
||||
|
||||
/**
|
||||
* Create Status
|
||||
*/
|
||||
export const createStatusOptions = (options: Options<CreateStatusData>) => {
|
||||
return queryOptions({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await createStatus({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: createStatusQueryKey(options),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Status
|
||||
*/
|
||||
export const createStatusMutation = (
|
||||
options?: Partial<Options<CreateStatusData>>
|
||||
): UseMutationOptions<
|
||||
CreateStatusResponse2,
|
||||
AxiosError<CreateStatusError>,
|
||||
Options<CreateStatusData>
|
||||
> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
CreateStatusResponse2,
|
||||
AxiosError<CreateStatusError>,
|
||||
Options<CreateStatusData>
|
||||
> = {
|
||||
mutationFn: async localOptions => {
|
||||
const { data } = await createStatus({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete Status
|
||||
*/
|
||||
export const deleteStatusMutation = (
|
||||
options?: Partial<Options<DeleteStatusData>>
|
||||
): UseMutationOptions<
|
||||
DeleteStatusResponse2,
|
||||
AxiosError<DeleteStatusError>,
|
||||
Options<DeleteStatusData>
|
||||
> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
DeleteStatusResponse2,
|
||||
AxiosError<DeleteStatusError>,
|
||||
Options<DeleteStatusData>
|
||||
> = {
|
||||
mutationFn: async localOptions => {
|
||||
const { data } = await deleteStatus({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update Status
|
||||
*/
|
||||
export const updateStatusMutation = (
|
||||
options?: Partial<Options<UpdateStatusData>>
|
||||
): UseMutationOptions<
|
||||
UpdateStatusResponse2,
|
||||
AxiosError<UpdateStatusError>,
|
||||
Options<UpdateStatusData>
|
||||
> => {
|
||||
const mutationOptions: UseMutationOptions<
|
||||
UpdateStatusResponse2,
|
||||
AxiosError<UpdateStatusError>,
|
||||
Options<UpdateStatusData>
|
||||
> = {
|
||||
mutationFn: async localOptions => {
|
||||
export const updateStatusMutation = (options?: Partial<Options<UpdateStatusData>>): UseMutationOptions<UpdateStatusResponse2, AxiosError<UpdateStatusError>, Options<UpdateStatusData>> => {
|
||||
const mutationOptions: UseMutationOptions<UpdateStatusResponse2, AxiosError<UpdateStatusError>, Options<UpdateStatusData>> = {
|
||||
mutationFn: async (localOptions) => {
|
||||
const { data } = await updateStatus({
|
||||
...options,
|
||||
...localOptions,
|
||||
throwOnError: true,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
},
|
||||
}
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
@ -1,13 +1,8 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { createClientConfig } from "../../hey-api-config";
|
||||
import {
|
||||
createClient,
|
||||
createConfig,
|
||||
type Config,
|
||||
type ClientOptions as DefaultClientOptions,
|
||||
} from "./client";
|
||||
import type { ClientOptions } from "./types.gen";
|
||||
import type { ClientOptions } from './types.gen';
|
||||
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from './client';
|
||||
import { createClientConfig } from '../../hey-api-config';
|
||||
|
||||
/**
|
||||
* The `createClientConfig()` function will be called on client initialization
|
||||
@ -17,15 +12,8 @@ import type { ClientOptions } from "./types.gen";
|
||||
* `setConfig()`. This is useful for example if you're using Next.js
|
||||
* to ensure your client always has the correct values.
|
||||
*/
|
||||
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> =
|
||||
(
|
||||
override?: Config<DefaultClientOptions & T>
|
||||
) => Config<Required<DefaultClientOptions> & T>;
|
||||
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;
|
||||
|
||||
export const client = createClient(
|
||||
createClientConfig(
|
||||
createConfig<ClientOptions>({
|
||||
baseURL: "/api",
|
||||
})
|
||||
)
|
||||
);
|
||||
export const client = createClient(createClientConfig(createConfig<ClientOptions>({
|
||||
baseURL: '/api'
|
||||
})));
|
||||
@ -1,13 +1,14 @@
|
||||
import type { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||
import axios from "axios";
|
||||
import type { Client, Config } from "./types";
|
||||
import type { AxiosError, RawAxiosRequestHeaders } from 'axios';
|
||||
import axios from 'axios';
|
||||
|
||||
import type { Client, Config } from './types';
|
||||
import {
|
||||
buildUrl,
|
||||
createConfig,
|
||||
mergeConfigs,
|
||||
mergeHeaders,
|
||||
setAuthParams,
|
||||
} from "./utils";
|
||||
} from './utils';
|
||||
|
||||
export const createClient = (config: Config = {}): Client => {
|
||||
let _config = mergeConfigs(createConfig(), config);
|
||||
@ -30,7 +31,7 @@ export const createClient = (config: Config = {}): Client => {
|
||||
};
|
||||
|
||||
// @ts-expect-error
|
||||
const request: Client["request"] = async options => {
|
||||
const request: Client['request'] = async (options) => {
|
||||
const opts = {
|
||||
..._config,
|
||||
...options,
|
||||
@ -72,7 +73,7 @@ export const createClient = (config: Config = {}): Client => {
|
||||
|
||||
let { data } = response;
|
||||
|
||||
if (opts.responseType === "json") {
|
||||
if (opts.responseType === 'json') {
|
||||
if (opts.responseValidator) {
|
||||
await opts.responseValidator(data);
|
||||
}
|
||||
@ -99,15 +100,15 @@ export const createClient = (config: Config = {}): Client => {
|
||||
|
||||
return {
|
||||
buildUrl,
|
||||
delete: options => request({ ...options, method: "DELETE" }),
|
||||
get: options => request({ ...options, method: "GET" }),
|
||||
delete: (options) => request({ ...options, method: 'DELETE' }),
|
||||
get: (options) => request({ ...options, method: 'GET' }),
|
||||
getConfig,
|
||||
head: options => request({ ...options, method: "HEAD" }),
|
||||
head: (options) => request({ ...options, method: 'HEAD' }),
|
||||
instance,
|
||||
options: options => request({ ...options, method: "OPTIONS" }),
|
||||
patch: options => request({ ...options, method: "PATCH" }),
|
||||
post: options => request({ ...options, method: "POST" }),
|
||||
put: options => request({ ...options, method: "PUT" }),
|
||||
options: (options) => request({ ...options, method: 'OPTIONS' }),
|
||||
patch: (options) => request({ ...options, method: 'PATCH' }),
|
||||
post: (options) => request({ ...options, method: 'POST' }),
|
||||
put: (options) => request({ ...options, method: 'PUT' }),
|
||||
request,
|
||||
setConfig,
|
||||
} as Client;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
export type { Auth } from "../core/auth";
|
||||
export type { QuerySerializerOptions } from "../core/bodySerializer";
|
||||
export type { Auth } from '../core/auth';
|
||||
export type { QuerySerializerOptions } from '../core/bodySerializer';
|
||||
export {
|
||||
formDataBodySerializer,
|
||||
jsonBodySerializer,
|
||||
urlSearchParamsBodySerializer,
|
||||
} from "../core/bodySerializer";
|
||||
export { buildClientParams } from "../core/params";
|
||||
export { createClient } from "./client";
|
||||
} from '../core/bodySerializer';
|
||||
export { buildClientParams } from '../core/params';
|
||||
export { createClient } from './client';
|
||||
export type {
|
||||
Client,
|
||||
ClientOptions,
|
||||
@ -17,5 +17,5 @@ export type {
|
||||
RequestOptions,
|
||||
RequestResult,
|
||||
TDataShape,
|
||||
} from "./types";
|
||||
export { createConfig } from "./utils";
|
||||
} from './types';
|
||||
export { createConfig } from './utils';
|
||||
|
||||
@ -5,15 +5,16 @@ import type {
|
||||
AxiosResponse,
|
||||
AxiosStatic,
|
||||
CreateAxiosDefaults,
|
||||
} from "axios";
|
||||
import type { Auth } from "../core/auth";
|
||||
import type { Client as CoreClient, Config as CoreConfig } from "../core/types";
|
||||
} from 'axios';
|
||||
|
||||
import type { Auth } from '../core/auth';
|
||||
import type {
|
||||
Client as CoreClient,
|
||||
Config as CoreConfig,
|
||||
} from '../core/types';
|
||||
|
||||
export interface Config<T extends ClientOptions = ClientOptions>
|
||||
extends Omit<
|
||||
CreateAxiosDefaults,
|
||||
"auth" | "baseURL" | "headers" | "method"
|
||||
>,
|
||||
extends Omit<CreateAxiosDefaults, 'auth' | 'baseURL' | 'headers' | 'method'>,
|
||||
CoreConfig {
|
||||
/**
|
||||
* Axios implementation. You can use this option to provide a custom
|
||||
@ -25,7 +26,7 @@ export interface Config<T extends ClientOptions = ClientOptions>
|
||||
/**
|
||||
* Base URL for all requests made by this client.
|
||||
*/
|
||||
baseURL?: T["baseURL"];
|
||||
baseURL?: T['baseURL'];
|
||||
/**
|
||||
* An object containing any HTTP headers that you want to pre-populate your
|
||||
* `Headers` object with.
|
||||
@ -49,7 +50,7 @@ export interface Config<T extends ClientOptions = ClientOptions>
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
throwOnError?: T["throwOnError"];
|
||||
throwOnError?: T['throwOnError'];
|
||||
}
|
||||
|
||||
export interface RequestOptions<
|
||||
@ -85,14 +86,10 @@ export type RequestResult<
|
||||
>
|
||||
: Promise<
|
||||
| (AxiosResponse<
|
||||
TData extends Record<string, unknown>
|
||||
? TData[keyof TData]
|
||||
: TData
|
||||
TData extends Record<string, unknown> ? TData[keyof TData] : TData
|
||||
> & { error: undefined })
|
||||
| (AxiosError<
|
||||
TError extends Record<string, unknown>
|
||||
? TError[keyof TError]
|
||||
: TError
|
||||
TError extends Record<string, unknown> ? TError[keyof TError] : TError
|
||||
> & {
|
||||
data: undefined;
|
||||
error: TError extends Record<string, unknown>
|
||||
@ -111,7 +108,7 @@ type MethodFn = <
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
>(
|
||||
options: Omit<RequestOptions<ThrowOnError>, "method">
|
||||
options: Omit<RequestOptions<ThrowOnError>, 'method'>,
|
||||
) => RequestResult<TData, TError, ThrowOnError>;
|
||||
|
||||
type RequestFn = <
|
||||
@ -119,8 +116,8 @@ type RequestFn = <
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
>(
|
||||
options: Omit<RequestOptions<ThrowOnError>, "method"> &
|
||||
Pick<Required<RequestOptions<ThrowOnError>>, "method">
|
||||
options: Omit<RequestOptions<ThrowOnError>, 'method'> &
|
||||
Pick<Required<RequestOptions<ThrowOnError>>, 'method'>,
|
||||
) => RequestResult<TData, TError, ThrowOnError>;
|
||||
|
||||
type BuildUrlFn = <
|
||||
@ -131,7 +128,7 @@ type BuildUrlFn = <
|
||||
url: string;
|
||||
},
|
||||
>(
|
||||
options: Pick<TData, "url"> & Omit<Options<TData>, "axios">
|
||||
options: Pick<TData, 'url'> & Omit<Options<TData>, 'axios'>,
|
||||
) => string;
|
||||
|
||||
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
|
||||
@ -147,7 +144,7 @@ export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
|
||||
* to ensure your client always has the correct values.
|
||||
*/
|
||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
||||
override?: Config<ClientOptions & T>
|
||||
override?: Config<ClientOptions & T>,
|
||||
) => Config<Required<ClientOptions> & T>;
|
||||
|
||||
export interface TDataShape {
|
||||
@ -163,21 +160,20 @@ type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
export type Options<
|
||||
TData extends TDataShape = TDataShape,
|
||||
ThrowOnError extends boolean = boolean,
|
||||
> = OmitKeys<RequestOptions<ThrowOnError>, "body" | "path" | "query" | "url"> &
|
||||
Omit<TData, "url">;
|
||||
> = OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'path' | 'query' | 'url'> &
|
||||
Omit<TData, 'url'>;
|
||||
|
||||
export type OptionsLegacyParser<
|
||||
TData = unknown,
|
||||
ThrowOnError extends boolean = boolean,
|
||||
> = TData extends { body?: any }
|
||||
? TData extends { headers?: any }
|
||||
? OmitKeys<RequestOptions<ThrowOnError>, "body" | "headers" | "url"> &
|
||||
TData
|
||||
: OmitKeys<RequestOptions<ThrowOnError>, "body" | "url"> &
|
||||
? OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'headers' | 'url'> & TData
|
||||
: OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'url'> &
|
||||
TData &
|
||||
Pick<RequestOptions<ThrowOnError>, "headers">
|
||||
Pick<RequestOptions<ThrowOnError>, 'headers'>
|
||||
: TData extends { headers?: any }
|
||||
? OmitKeys<RequestOptions<ThrowOnError>, "headers" | "url"> &
|
||||
? OmitKeys<RequestOptions<ThrowOnError>, 'headers' | 'url'> &
|
||||
TData &
|
||||
Pick<RequestOptions<ThrowOnError>, "body">
|
||||
: OmitKeys<RequestOptions<ThrowOnError>, "url"> & TData;
|
||||
Pick<RequestOptions<ThrowOnError>, 'body'>
|
||||
: OmitKeys<RequestOptions<ThrowOnError>, 'url'> & TData;
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { getAuthToken } from "../core/auth";
|
||||
import { getAuthToken } from '../core/auth';
|
||||
import type {
|
||||
QuerySerializer,
|
||||
QuerySerializerOptions,
|
||||
} from "../core/bodySerializer";
|
||||
import type { ArraySeparatorStyle } from "../core/pathSerializer";
|
||||
} from '../core/bodySerializer';
|
||||
import type { ArraySeparatorStyle } from '../core/pathSerializer';
|
||||
import {
|
||||
serializeArrayParam,
|
||||
serializeObjectParam,
|
||||
serializePrimitiveParam,
|
||||
} from "../core/pathSerializer";
|
||||
import type { Client, ClientOptions, Config, RequestOptions } from "./types";
|
||||
} from '../core/pathSerializer';
|
||||
import type { Client, ClientOptions, Config, RequestOptions } from './types';
|
||||
|
||||
interface PathSerializer {
|
||||
path: Record<string, unknown>;
|
||||
@ -25,19 +25,19 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||
for (const match of matches) {
|
||||
let explode = false;
|
||||
let name = match.substring(1, match.length - 1);
|
||||
let style: ArraySeparatorStyle = "simple";
|
||||
let style: ArraySeparatorStyle = 'simple';
|
||||
|
||||
if (name.endsWith("*")) {
|
||||
if (name.endsWith('*')) {
|
||||
explode = true;
|
||||
name = name.substring(0, name.length - 1);
|
||||
}
|
||||
|
||||
if (name.startsWith(".")) {
|
||||
if (name.startsWith('.')) {
|
||||
name = name.substring(1);
|
||||
style = "label";
|
||||
} else if (name.startsWith(";")) {
|
||||
style = 'label';
|
||||
} else if (name.startsWith(';')) {
|
||||
name = name.substring(1);
|
||||
style = "matrix";
|
||||
style = 'matrix';
|
||||
}
|
||||
|
||||
const value = path[name];
|
||||
@ -49,12 +49,12 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||
if (Array.isArray(value)) {
|
||||
url = url.replace(
|
||||
match,
|
||||
serializeArrayParam({ explode, name, style, value })
|
||||
serializeArrayParam({ explode, name, style, value }),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (typeof value === 'object') {
|
||||
url = url.replace(
|
||||
match,
|
||||
serializeObjectParam({
|
||||
@ -63,24 +63,24 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||
style,
|
||||
value: value as Record<string, unknown>,
|
||||
valueOnly: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (style === "matrix") {
|
||||
if (style === 'matrix') {
|
||||
url = url.replace(
|
||||
match,
|
||||
`;${serializePrimitiveParam({
|
||||
name,
|
||||
value: value as string,
|
||||
})}`
|
||||
})}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const replaceValue = encodeURIComponent(
|
||||
style === "label" ? `.${value as string}` : (value as string)
|
||||
style === 'label' ? `.${value as string}` : (value as string),
|
||||
);
|
||||
url = url.replace(match, replaceValue);
|
||||
}
|
||||
@ -95,7 +95,7 @@ export const createQuerySerializer = <T = unknown>({
|
||||
}: QuerySerializerOptions = {}) => {
|
||||
const querySerializer = (queryParams: T) => {
|
||||
const search: string[] = [];
|
||||
if (queryParams && typeof queryParams === "object") {
|
||||
if (queryParams && typeof queryParams === 'object') {
|
||||
for (const name in queryParams) {
|
||||
const value = queryParams[name];
|
||||
|
||||
@ -108,17 +108,17 @@ export const createQuerySerializer = <T = unknown>({
|
||||
allowReserved,
|
||||
explode: true,
|
||||
name,
|
||||
style: "form",
|
||||
style: 'form',
|
||||
value,
|
||||
...array,
|
||||
});
|
||||
if (serializedArray) search.push(serializedArray);
|
||||
} else if (typeof value === "object") {
|
||||
} else if (typeof value === 'object') {
|
||||
const serializedObject = serializeObjectParam({
|
||||
allowReserved,
|
||||
explode: true,
|
||||
name,
|
||||
style: "deepObject",
|
||||
style: 'deepObject',
|
||||
value: value as Record<string, unknown>,
|
||||
...object,
|
||||
});
|
||||
@ -133,7 +133,7 @@ export const createQuerySerializer = <T = unknown>({
|
||||
}
|
||||
}
|
||||
}
|
||||
return search.join("&");
|
||||
return search.join('&');
|
||||
};
|
||||
return querySerializer;
|
||||
};
|
||||
@ -141,8 +141,8 @@ export const createQuerySerializer = <T = unknown>({
|
||||
export const setAuthParams = async ({
|
||||
security,
|
||||
...options
|
||||
}: Pick<Required<RequestOptions>, "security"> &
|
||||
Pick<RequestOptions, "auth" | "query"> & {
|
||||
}: Pick<Required<RequestOptions>, 'security'> &
|
||||
Pick<RequestOptions, 'auth' | 'query'> & {
|
||||
headers: Record<any, unknown>;
|
||||
}) => {
|
||||
for (const auth of security) {
|
||||
@ -152,26 +152,25 @@ export const setAuthParams = async ({
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = auth.name ?? "Authorization";
|
||||
const name = auth.name ?? 'Authorization';
|
||||
|
||||
switch (auth.in) {
|
||||
case "query":
|
||||
case 'query':
|
||||
if (!options.query) {
|
||||
options.query = {};
|
||||
}
|
||||
options.query[name] = token;
|
||||
break;
|
||||
case "cookie": {
|
||||
case 'cookie': {
|
||||
const value = `${name}=${token}`;
|
||||
if ("Cookie" in options.headers && options.headers["Cookie"]) {
|
||||
options.headers["Cookie"] =
|
||||
`${options.headers["Cookie"]}; ${value}`;
|
||||
if ('Cookie' in options.headers && options.headers['Cookie']) {
|
||||
options.headers['Cookie'] = `${options.headers['Cookie']}; ${value}`;
|
||||
} else {
|
||||
options.headers["Cookie"] = value;
|
||||
options.headers['Cookie'] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "header":
|
||||
case 'header':
|
||||
default:
|
||||
options.headers[name] = token;
|
||||
break;
|
||||
@ -181,13 +180,13 @@ export const setAuthParams = async ({
|
||||
}
|
||||
};
|
||||
|
||||
export const buildUrl: Client["buildUrl"] = options => {
|
||||
export const buildUrl: Client['buildUrl'] = (options) => {
|
||||
const url = getUrl({
|
||||
path: options.path,
|
||||
// let `paramsSerializer()` handle query params if it exists
|
||||
query: !options.paramsSerializer ? options.query : undefined,
|
||||
querySerializer:
|
||||
typeof options.querySerializer === "function"
|
||||
typeof options.querySerializer === 'function'
|
||||
? options.querySerializer
|
||||
: createQuerySerializer(options.querySerializer),
|
||||
url: options.url,
|
||||
@ -206,13 +205,13 @@ export const getUrl = ({
|
||||
querySerializer: QuerySerializer;
|
||||
url: string;
|
||||
}) => {
|
||||
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
|
||||
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
|
||||
let url = pathUrl;
|
||||
if (path) {
|
||||
url = defaultPathSerializer({ path, url });
|
||||
}
|
||||
let search = query ? querySerializer(query) : "";
|
||||
if (search.startsWith("?")) {
|
||||
let search = query ? querySerializer(query) : '';
|
||||
if (search.startsWith('?')) {
|
||||
search = search.substring(1);
|
||||
}
|
||||
if (search) {
|
||||
@ -231,21 +230,21 @@ export const mergeConfigs = (a: Config, b: Config): Config => {
|
||||
* Special Axios headers keywords allowing to set headers by request method.
|
||||
*/
|
||||
export const axiosHeadersKeywords = [
|
||||
"common",
|
||||
"delete",
|
||||
"get",
|
||||
"head",
|
||||
"patch",
|
||||
"post",
|
||||
"put",
|
||||
'common',
|
||||
'delete',
|
||||
'get',
|
||||
'head',
|
||||
'patch',
|
||||
'post',
|
||||
'put',
|
||||
] as const;
|
||||
|
||||
export const mergeHeaders = (
|
||||
...headers: Array<Required<Config>["headers"] | undefined>
|
||||
...headers: Array<Required<Config>['headers'] | undefined>
|
||||
): Record<any, unknown> => {
|
||||
const mergedHeaders: Record<any, unknown> = {};
|
||||
for (const header of headers) {
|
||||
if (!header || typeof header !== "object") {
|
||||
if (!header || typeof header !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -254,9 +253,9 @@ export const mergeHeaders = (
|
||||
for (const [key, value] of iterator) {
|
||||
if (
|
||||
axiosHeadersKeywords.includes(
|
||||
key as (typeof axiosHeadersKeywords)[number]
|
||||
key as (typeof axiosHeadersKeywords)[number],
|
||||
) &&
|
||||
typeof value === "object"
|
||||
typeof value === 'object'
|
||||
) {
|
||||
mergedHeaders[key] = {
|
||||
...(mergedHeaders[key] as Record<any, unknown>),
|
||||
@ -267,18 +266,13 @@ export const mergeHeaders = (
|
||||
} else if (Array.isArray(value)) {
|
||||
for (const v of value) {
|
||||
// @ts-expect-error
|
||||
mergedHeaders[key] = [
|
||||
...(mergedHeaders[key] ?? []),
|
||||
v as string,
|
||||
];
|
||||
mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string];
|
||||
}
|
||||
} else if (value !== undefined) {
|
||||
// assume object headers are meant to be JSON stringified, i.e. their
|
||||
// content value in OpenAPI specification is 'application/json'
|
||||
mergedHeaders[key] =
|
||||
typeof value === "object"
|
||||
? JSON.stringify(value)
|
||||
: (value as string);
|
||||
typeof value === 'object' ? JSON.stringify(value) : (value as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -286,7 +280,7 @@ export const mergeHeaders = (
|
||||
};
|
||||
|
||||
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
||||
override: Config<Omit<ClientOptions, keyof T> & T> = {}
|
||||
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
||||
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
||||
...override,
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user