Compare commits
1 Commits
a169600908
...
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",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"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": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@mantine/carousel": "^8.2.4",
|
|
||||||
"@mantine/core": "8.1.2",
|
"@mantine/core": "8.1.2",
|
||||||
"@mantine/form": "^8.1.3",
|
"@mantine/form": "^8.1.3",
|
||||||
"@mantine/hooks": "8.1.2",
|
"@mantine/hooks": "8.1.2",
|
||||||
@ -26,8 +24,6 @@
|
|||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"embla-carousel": "^8.6.0",
|
|
||||||
"embla-carousel-react": "^8.6.0",
|
|
||||||
"framer-motion": "^12.23.7",
|
"framer-motion": "^12.23.7",
|
||||||
"i18n-iso-countries": "^7.14.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"lexorank": "^1.0.5",
|
"lexorank": "^1.0.5",
|
||||||
@ -60,9 +56,7 @@
|
|||||||
"@types/node": "^22.13.11",
|
"@types/node": "^22.13.11",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-redux": "^7.1.34",
|
"@types/react-redux": "^7.1.34",
|
||||||
"@types/react-slick": "^0",
|
|
||||||
"@types/redux-persist": "^4.3.1",
|
"@types/redux-persist": "^4.3.1",
|
||||||
"@types/slick-carousel": "^1",
|
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"babel-loader": "^10.0.0",
|
"babel-loader": "^10.0.0",
|
||||||
"eslint": "^9.29.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 { Card } from "@mantine/core";
|
||||||
import { DealSchema } from "@/lib/client";
|
import { DealSchema } from "@/lib/client";
|
||||||
import styles from "./DealCard.module.css";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
deal: DealSchema;
|
deal: DealSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DealCard = ({ deal }: Props) => {
|
const DealCard = ({ deal }: Props) => {
|
||||||
return <Card className={styles.container}>{deal.name}</Card>;
|
return <Card>{deal.name}</Card>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DealCard;
|
export default DealCard;
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import React, { FC, useMemo } from "react";
|
import React, { FC, useMemo } from "react";
|
||||||
import { Box } from "@mantine/core";
|
import { Box } from "@mantine/core";
|
||||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||||
import SortableItem from "@/components/dnd/SortableItem";
|
|
||||||
import { DealSchema } from "@/lib/client";
|
import { DealSchema } from "@/lib/client";
|
||||||
|
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
deal: DealSchema;
|
deal: DealSchema;
|
||||||
@ -15,9 +15,9 @@ const DealContainer: FC<Props> = ({ deal }) => {
|
|||||||
<Box>
|
<Box>
|
||||||
<SortableItem
|
<SortableItem
|
||||||
dragHandleStyle={{ cursor: "pointer" }}
|
dragHandleStyle={{ cursor: "pointer" }}
|
||||||
id={deal.id}
|
id={deal.id}>
|
||||||
renderItem={() => dealBody}
|
{dealBody}
|
||||||
/>
|
</SortableItem>
|
||||||
</Box>
|
</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,12 +0,0 @@
|
|||||||
|
|
||||||
.container {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
border-bottom: solid dodgerblue 3px;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
@media (max-width: 48em) {
|
|
||||||
width: 80vw;
|
|
||||||
margin-right: 8vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +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,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import { BoardSchema } from "@/lib/client";
|
||||||
import useBoardsList from "@/hooks/useBoardsList";
|
import useBoardsList from "@/hooks/useBoardsList";
|
||||||
import { useBoardsOperations } from "@/hooks/useBoardsOperations";
|
|
||||||
import { BoardSchema, UpdateBoardSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type BoardsContextState = {
|
type BoardsContextState = {
|
||||||
boards: BoardSchema[];
|
boards: BoardSchema[];
|
||||||
@ -18,27 +17,16 @@ type BoardsContextState = {
|
|||||||
selectedBoard: BoardSchema | null;
|
selectedBoard: BoardSchema | null;
|
||||||
setSelectedBoard: React.Dispatch<React.SetStateAction<BoardSchema | null>>;
|
setSelectedBoard: React.Dispatch<React.SetStateAction<BoardSchema | null>>;
|
||||||
refetchBoards: () => void;
|
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 BoardsContext = createContext<BoardsContextState | undefined>(undefined);
|
||||||
|
|
||||||
const useBoardsContextState = () => {
|
const useBoardsContextState = () => {
|
||||||
const { selectedProject: project } = useProjectsContext();
|
const { selectedProject: project } = useProjectsContext();
|
||||||
const {
|
const { boards, setBoards, refetch: refetchBoards } = useBoardsList({ projectId: project?.id });
|
||||||
boards,
|
|
||||||
setBoards,
|
|
||||||
refetch: refetchBoards,
|
|
||||||
} = useBoardsList({ projectId: project?.id });
|
|
||||||
const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>(
|
const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (boards.length > 0 && selectedBoard === null) {
|
if (boards.length > 0 && selectedBoard === null) {
|
||||||
@ -46,24 +34,15 @@ const useBoardsContextState = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedBoard) return;
|
if (selectedBoard) {
|
||||||
|
|
||||||
let newBoard = boards.find(board => board.id === selectedBoard.id);
|
let newBoard = boards.find(board => board.id === selectedBoard.id);
|
||||||
|
|
||||||
if (!newBoard && boards.length > 0) {
|
if (!newBoard && boards.length > 0) {
|
||||||
newBoard = boards[0];
|
newBoard = boards[0];
|
||||||
}
|
}
|
||||||
setSelectedBoard(newBoard ?? null);
|
setSelectedBoard(newBoard ?? null);
|
||||||
}, [boards]);
|
|
||||||
|
|
||||||
const { onCreateBoard, onUpdateBoard, onDeleteBoard } = useBoardsOperations(
|
|
||||||
{
|
|
||||||
boards,
|
|
||||||
setBoards,
|
|
||||||
refetchBoards,
|
|
||||||
projectId: project?.id,
|
|
||||||
}
|
}
|
||||||
);
|
}, [boards]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
boards,
|
boards,
|
||||||
@ -71,11 +50,6 @@ const useBoardsContextState = () => {
|
|||||||
selectedBoard,
|
selectedBoard,
|
||||||
setSelectedBoard,
|
setSelectedBoard,
|
||||||
refetchBoards,
|
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,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { ProjectSchema } from "@/lib/client";
|
||||||
import useProjectsList from "@/hooks/useProjectsList";
|
import useProjectsList from "@/hooks/useProjectsList";
|
||||||
import { useProjectsOperations } from "@/hooks/useProjectsOperations";
|
|
||||||
import { ProjectSchema, UpdateProjectSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type ProjectsContextState = {
|
type ProjectsContextState = {
|
||||||
selectedProject: ProjectSchema | null;
|
selectedProject: ProjectSchema | null;
|
||||||
@ -17,11 +16,6 @@ type ProjectsContextState = {
|
|||||||
React.SetStateAction<ProjectSchema | null>
|
React.SetStateAction<ProjectSchema | null>
|
||||||
>;
|
>;
|
||||||
projects: ProjectSchema[];
|
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>(
|
const ProjectsContext = createContext<ProjectsContextState | undefined>(
|
||||||
@ -29,13 +23,7 @@ const ProjectsContext = createContext<ProjectsContextState | undefined>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const useProjectsContextState = () => {
|
const useProjectsContextState = () => {
|
||||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
const { projects } = useProjectsList();
|
||||||
useState<boolean>(false);
|
|
||||||
const {
|
|
||||||
projects,
|
|
||||||
setProjects,
|
|
||||||
refetch: refetchProjects,
|
|
||||||
} = useProjectsList();
|
|
||||||
const [selectedProject, setSelectedProject] =
|
const [selectedProject, setSelectedProject] =
|
||||||
useState<ProjectSchema | null>(null);
|
useState<ProjectSchema | null>(null);
|
||||||
|
|
||||||
@ -55,22 +43,10 @@ const useProjectsContextState = () => {
|
|||||||
setSelectedProject(null);
|
setSelectedProject(null);
|
||||||
}, [projects]);
|
}, [projects]);
|
||||||
|
|
||||||
const { onCreateProject, onUpdateProject, onDeleteProject } =
|
|
||||||
useProjectsOperations({
|
|
||||||
projects,
|
|
||||||
setProjects,
|
|
||||||
refetchProjects,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
setSelectedProject,
|
setSelectedProject,
|
||||||
onCreateProject,
|
|
||||||
onUpdateProject,
|
|
||||||
onDeleteProject,
|
|
||||||
isEditorDrawerOpened,
|
|
||||||
setIsEditorDrawerOpened,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,42 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, {
|
import React, { createContext, FC, useContext, useEffect } from "react";
|
||||||
createContext,
|
|
||||||
FC,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useMutation, UseMutationResult } from "@tanstack/react-query";
|
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
|
import useDealsList from "@/hooks/useDealsList";
|
||||||
import useStatusesList from "@/hooks/useStatusesList";
|
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 = {
|
type StatusesContextState = {
|
||||||
statuses: StatusSchema[];
|
statuses: StatusSchema[];
|
||||||
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
||||||
updateStatus: UseMutationResult<
|
deals: DealSchema[];
|
||||||
UpdateStatusResponse,
|
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
|
||||||
AxiosError<HttpValidationError>,
|
|
||||||
Options<UpdateStatusData>
|
|
||||||
>;
|
|
||||||
refetchStatuses: () => void;
|
refetchStatuses: () => void;
|
||||||
onCreateStatus: (name: string) => void;
|
refetchDeals: () => void;
|
||||||
onUpdateStatus: (statusId: number, status: UpdateStatusSchema) => void;
|
|
||||||
onDeleteStatus: (status: StatusSchema) => void;
|
|
||||||
isEditorDrawerOpened: boolean;
|
|
||||||
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusesContext = createContext<StatusesContextState | undefined>(
|
const StatusesContext = createContext<StatusesContextState | undefined>(
|
||||||
@ -52,42 +28,24 @@ const useStatusesContextState = () => {
|
|||||||
} = useStatusesList({
|
} = useStatusesList({
|
||||||
boardId: selectedBoard?.id,
|
boardId: selectedBoard?.id,
|
||||||
});
|
});
|
||||||
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
|
|
||||||
useState<boolean>(false);
|
const {
|
||||||
|
deals,
|
||||||
|
setDeals,
|
||||||
|
refetch: refetchDeals,
|
||||||
|
} = useDealsList({ boardId: selectedBoard?.id });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetchStatuses();
|
refetchStatuses();
|
||||||
}, [selectedBoard]);
|
}, [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 {
|
return {
|
||||||
statuses,
|
statuses,
|
||||||
setStatuses,
|
setStatuses,
|
||||||
updateStatus,
|
deals,
|
||||||
|
setDeals,
|
||||||
refetchStatuses,
|
refetchStatuses,
|
||||||
onCreateStatus,
|
refetchDeals,
|
||||||
onUpdateStatus,
|
|
||||||
onDeleteStatus,
|
|
||||||
isEditorDrawerOpened,
|
|
||||||
setIsEditorDrawerOpened,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 { useMemo, useState } from "react";
|
||||||
import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core";
|
import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core";
|
||||||
import { useDebouncedCallback } from "@mantine/hooks";
|
import { useDebouncedCallback } from "@mantine/hooks";
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
|
import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
|
||||||
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
||||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
import { sortByLexorank } from "@/utils/lexorank";
|
import { sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
type ReturnType = {
|
type Props = {
|
||||||
sortedStatuses: StatusSchema[];
|
onDealDragEnd: (
|
||||||
handleDragStart: ({ active }: DragStartEvent) => void;
|
dealId: number,
|
||||||
handleDragOver: ({ active, over }: DragOverEvent) => void;
|
statusId: number,
|
||||||
handleDragEnd: ({ active, over }: DragOverEvent) => void;
|
lexorank?: string
|
||||||
activeStatus: StatusSchema | null;
|
) => void;
|
||||||
activeDeal: DealSchema | null;
|
onStatusDragEnd: (statusId: number, lexorank: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useDealsAndStatusesDnd = (): ReturnType => {
|
const useDealsAndStatusesDnd = (props: Props) => {
|
||||||
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
||||||
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
||||||
const { statuses, setStatuses, updateStatus } = useStatusesContext();
|
const { statuses, deals, setDeals, setStatuses } = useStatusesContext();
|
||||||
const { deals, setDeals, updateDeal } = useDealsContext();
|
|
||||||
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -175,20 +173,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
|||||||
const newRank = getNewStatusRank(activeStatusId, overStatusId);
|
const newRank = getNewStatusRank(activeStatusId, overStatusId);
|
||||||
if (!newRank) return;
|
if (!newRank) return;
|
||||||
|
|
||||||
onStatusDragEnd?.(activeStatusId, newRank);
|
props.onStatusDragEnd?.(activeStatusId, newRank);
|
||||||
};
|
|
||||||
|
|
||||||
const onStatusDragEnd = (statusId: number, lexorank: string) => {
|
|
||||||
updateStatus.mutate({
|
|
||||||
path: {
|
|
||||||
statusId,
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
status: {
|
|
||||||
lexorank,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDealDragEnd = (activeId: number | string, over: Over) => {
|
const handleDealDragEnd = (activeId: number | string, over: Over) => {
|
||||||
@ -204,25 +189,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
|||||||
);
|
);
|
||||||
if (!overStatusId) return;
|
if (!overStatusId) return;
|
||||||
|
|
||||||
onDealDragEnd(activeDealId, overStatusId, newLexorank);
|
props.onDealDragEnd(activeDealId, overStatusId, newLexorank);
|
||||||
};
|
|
||||||
|
|
||||||
const onDealDragEnd = (
|
|
||||||
dealId: number,
|
|
||||||
statusId: number,
|
|
||||||
lexorank?: string
|
|
||||||
) => {
|
|
||||||
updateDeal.mutate({
|
|
||||||
path: {
|
|
||||||
dealId,
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
deal: {
|
|
||||||
statusId,
|
|
||||||
lexorank,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragStart = ({ active }: DragStartEvent) => {
|
const handleDragStart = ({ active }: DragStartEvent) => {
|
||||||
|
|||||||
@ -3,30 +3,14 @@ import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
|||||||
import { DealSchema } from "@/lib/client";
|
import { DealSchema } from "@/lib/client";
|
||||||
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
type NewRankGetters = {
|
const useGetNewRank = () => {
|
||||||
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 { statuses } = useStatusesContext();
|
const { statuses } = useStatusesContext();
|
||||||
|
|
||||||
const getNewRankForSameStatus = (
|
const getNewRankForSameStatus = (
|
||||||
statusDeals: DealSchema[],
|
statusDeals: DealSchema[],
|
||||||
overDealIndex: number,
|
overDealIndex: number,
|
||||||
activeDealId: number
|
activeDealId: number
|
||||||
): string => {
|
) => {
|
||||||
const activeDealIndex = statusDeals.findIndex(
|
const activeDealIndex = statusDeals.findIndex(
|
||||||
deal => deal.id === activeDealId
|
deal => deal.id === activeDealId
|
||||||
);
|
);
|
||||||
@ -50,7 +34,7 @@ const useGetNewRank = (): NewRankGetters => {
|
|||||||
const getNewRankForAnotherStatus = (
|
const getNewRankForAnotherStatus = (
|
||||||
statusDeals: DealSchema[],
|
statusDeals: DealSchema[],
|
||||||
overDealIndex: number
|
overDealIndex: number
|
||||||
): string => {
|
) => {
|
||||||
const leftLexorank =
|
const leftLexorank =
|
||||||
overDealIndex > 0
|
overDealIndex > 0
|
||||||
? LexoRank.parse(statusDeals[overDealIndex - 1].lexorank)
|
? LexoRank.parse(statusDeals[overDealIndex - 1].lexorank)
|
||||||
@ -62,10 +46,7 @@ const useGetNewRank = (): NewRankGetters => {
|
|||||||
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNewStatusRank = (
|
const getNewStatusRank = (activeStatusId: number, overStatusId: number) => {
|
||||||
activeStatusId: number,
|
|
||||||
overStatusId: number
|
|
||||||
): string | null => {
|
|
||||||
const sortedStatusList = sortByLexorank(statuses);
|
const sortedStatusList = sortByLexorank(statuses);
|
||||||
const overIndex = sortedStatusList.findIndex(
|
const overIndex = sortedStatusList.findIndex(
|
||||||
s => s.id === overStatusId
|
s => s.id === overStatusId
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
MouseSensor,
|
PointerSensor,
|
||||||
TouchSensor,
|
TouchSensor,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
@ -8,21 +8,18 @@ import {
|
|||||||
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
||||||
|
|
||||||
const useDndSensors = () => {
|
const useDndSensors = () => {
|
||||||
return useSensors(
|
const sensorOptions = {
|
||||||
useSensor(MouseSensor, {
|
|
||||||
activationConstraint: {
|
activationConstraint: {
|
||||||
distance: 8,
|
distance: 5,
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
|
|
||||||
|
return useSensors(
|
||||||
|
useSensor(PointerSensor, sensorOptions),
|
||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
}),
|
}),
|
||||||
useSensor(TouchSensor, {
|
useSensor(TouchSensor, sensorOptions)
|
||||||
activationConstraint: {
|
|
||||||
delay: 300,
|
|
||||||
tolerance: 5,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,37 +1,28 @@
|
|||||||
import Funnel from "@/app/deals/components/shared/Funnel/Funnel";
|
import { Divider } from "@mantine/core";
|
||||||
import Header from "@/app/deals/components/shared/Header/Header";
|
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 { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext";
|
||||||
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
||||||
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
|
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
|
||||||
import BoardStatusesEditorDrawer from "@/app/deals/drawers/BoardStatusesEditorDrawer";
|
import PageBlock from "@/components/PageBlock/PageBlock";
|
||||||
import ProjectBoardsEditorDrawer from "@/app/deals/drawers/ProjectBoardsEditorDrawer";
|
import PageContainer from "@/components/PageContainer/PageContainer";
|
||||||
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";
|
|
||||||
|
|
||||||
export default function DealsPage() {
|
export default function DealsPage() {
|
||||||
return (
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageBlock>
|
||||||
<ProjectsContextProvider>
|
<ProjectsContextProvider>
|
||||||
<BoardsContextProvider>
|
<BoardsContextProvider>
|
||||||
<PageContainer>
|
|
||||||
<PageBlock
|
|
||||||
transparent
|
|
||||||
style={{ padding: 0 }}>
|
|
||||||
<Header />
|
<Header />
|
||||||
</PageBlock>
|
<Boards />
|
||||||
<PageBlock className={"mobile-padding-height"}>
|
<Divider my={"xl"} />
|
||||||
<StatusesContextProvider>
|
<StatusesContextProvider>
|
||||||
<DealsContextProvider>
|
<StatusColumns />
|
||||||
<Funnel />
|
|
||||||
</DealsContextProvider>
|
|
||||||
<BoardStatusesEditorDrawer />
|
|
||||||
</StatusesContextProvider>
|
</StatusesContextProvider>
|
||||||
<ProjectBoardsEditorDrawer />
|
|
||||||
<ProjectsEditorDrawer />
|
|
||||||
</PageBlock>
|
|
||||||
</PageContainer>
|
|
||||||
</BoardsContextProvider>
|
</BoardsContextProvider>
|
||||||
</ProjectsContextProvider>
|
</ProjectsContextProvider>
|
||||||
|
</PageBlock>
|
||||||
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@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 {
|
body {
|
||||||
@mixin light {
|
@mixin light {
|
||||||
background-color: var(--color-light-gray-blue);
|
background-color: whitesmoke;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
import "@mantine/notifications/styles.css";
|
import "@mantine/notifications/styles.css";
|
||||||
import "@mantine/carousel/styles.css";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
ColorSchemeScript,
|
ColorSchemeScript,
|
||||||
@ -41,6 +40,10 @@ export default function RootLayout({ children }: Props) {
|
|||||||
rel="shortcut icon"
|
rel="shortcut icon"
|
||||||
href="/favicon.svg"
|
href="/favicon.svg"
|
||||||
/>
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="global.css"
|
||||||
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
|
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 { CSSProperties, FC, ReactNode } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styles from "./PageBlock.module.css";
|
import styles from "./PageBlock.module.css";
|
||||||
import { Box } from "@mantine/core";
|
5
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
className?: string;
|
|
||||||
fullHeight?: boolean;
|
fullHeight?: boolean;
|
||||||
fullHeightFixed?: boolean;
|
fullHeightFixed?: boolean;
|
||||||
noBorderRadius?: boolean;
|
noBorderRadius?: boolean;
|
||||||
fullScreenMobile?: boolean;
|
fullScreenMobile?: boolean;
|
||||||
transparent?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageBlock: FC<Props> = ({
|
const PageBlock: FC<Props> = ({
|
||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
className = "",
|
|
||||||
fullHeight = false,
|
fullHeight = false,
|
||||||
fullHeightFixed = false,
|
fullHeightFixed = false,
|
||||||
noBorderRadius = false,
|
noBorderRadius = false,
|
||||||
fullScreenMobile = false,
|
fullScreenMobile = false,
|
||||||
transparent = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<div
|
||||||
bdrs={"lg"}
|
|
||||||
style={style}
|
style={style}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.container,
|
styles.container,
|
||||||
fullHeight && styles["container-full-height"],
|
fullHeight && styles["container-full-height"],
|
||||||
fullHeightFixed && styles["container-full-height-fixed"],
|
fullHeightFixed && styles["container-full-height-fixed"],
|
||||||
noBorderRadius && styles["container-no-border-radius"],
|
noBorderRadius && styles["container-no-border-radius"],
|
||||||
fullScreenMobile && styles["container-full-screen-mobile"],
|
fullScreenMobile && styles["container-full-screen-mobile"]
|
||||||
transparent && styles.transparent,
|
|
||||||
styles[className]
|
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default PageBlock;
|
export default PageBlock;
|
||||||
@ -1,10 +1,7 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
|
||||||
background-color: transparent;
|
|
||||||
|
|
||||||
@media (min-width: 48em) {
|
|
||||||
gap: rem(10);
|
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) {
|
export function SortableOverlay({ children }: PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<DragOverlay dropAnimation={dropAnimationConfig}>
|
<DragOverlay dropAnimation={dropAnimationConfig}>
|
||||||
<div style={{ cursor: "grabbing" }}>{children}</div>
|
{children}
|
||||||
</DragOverlay>
|
</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,154 +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={"80%"}
|
|
||||||
slideGap={"md"}
|
|
||||||
pb={"xl"}
|
|
||||||
withControls={false}
|
|
||||||
withIndicators
|
|
||||||
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 => ({
|
export const createClientConfig: CreateClientConfig = config => ({
|
||||||
...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 { useQuery } from "@tanstack/react-query";
|
||||||
import { ProjectSchema } from "@/lib/client";
|
|
||||||
import { getProjectsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
import { getProjectsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
const useProjectsList = () => {
|
const useProjectsList = () => {
|
||||||
const [projects, setProjects] = useState<ProjectSchema[]>([]);
|
|
||||||
|
|
||||||
const { data, refetch, isLoading } = useQuery({
|
const { data, refetch, isLoading } = useQuery({
|
||||||
...getProjectsOptions(),
|
...getProjectsOptions(),
|
||||||
});
|
});
|
||||||
|
const projects = !data ? [] : data.projects;
|
||||||
useEffect(() => {
|
return { projects, refetch, isLoading };
|
||||||
if (data?.projects) {
|
|
||||||
setProjects(data.projects);
|
|
||||||
}
|
|
||||||
}, [data?.projects]);
|
|
||||||
|
|
||||||
return { projects, setProjects, refetch, isLoading };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useProjectsList;
|
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
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import { queryOptions, type UseMutationOptions } from "@tanstack/react-query";
|
import { type Options, getBoards, updateBoard, getDeals, updateDeal, getProjects, getStatuses, updateStatus } from '../sdk.gen';
|
||||||
import type { AxiosError } from "axios";
|
import { queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
||||||
import { client as _heyApiClient } from "../client.gen";
|
import type { GetBoardsData, UpdateBoardData, UpdateBoardError, UpdateBoardResponse2, GetDealsData, UpdateDealData, UpdateDealError, UpdateDealResponse2, GetProjectsData, GetStatusesData, UpdateStatusData, UpdateStatusError, UpdateStatusResponse2 } from '../types.gen';
|
||||||
import {
|
import type { AxiosError } from 'axios';
|
||||||
createBoard,
|
import { client as _heyApiClient } from '../client.gen';
|
||||||
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";
|
|
||||||
|
|
||||||
export type QueryKey<TOptions extends Options> = [
|
export type QueryKey<TOptions extends Options> = [
|
||||||
Pick<TOptions, "baseURL" | "body" | "headers" | "path" | "query"> & {
|
Pick<TOptions, 'baseURL' | 'body' | 'headers' | 'path' | 'query'> & {
|
||||||
_id: string;
|
_id: string;
|
||||||
_infinite?: boolean;
|
_infinite?: boolean;
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const createQueryKey = <TOptions extends Options>(
|
const createQueryKey = <TOptions extends Options>(id: string, options?: TOptions, infinite?: boolean): [
|
||||||
id: string,
|
QueryKey<TOptions>[0]
|
||||||
options?: TOptions,
|
] => {
|
||||||
infinite?: boolean
|
const params: QueryKey<TOptions>[0] = { _id: id, baseURL: options?.baseURL || (options?.client ?? _heyApiClient).getConfig().baseURL } as QueryKey<TOptions>[0];
|
||||||
): [QueryKey<TOptions>[0]] => {
|
|
||||||
const params: QueryKey<TOptions>[0] = {
|
|
||||||
_id: id,
|
|
||||||
baseURL:
|
|
||||||
options?.baseURL ||
|
|
||||||
(options?.client ?? _heyApiClient).getConfig().baseURL,
|
|
||||||
} as QueryKey<TOptions>[0];
|
|
||||||
if (infinite) {
|
if (infinite) {
|
||||||
params._infinite = infinite;
|
params._infinite = infinite;
|
||||||
}
|
}
|
||||||
@ -90,11 +32,12 @@ const createQueryKey = <TOptions extends Options>(
|
|||||||
if (options?.query) {
|
if (options?.query) {
|
||||||
params.query = options.query;
|
params.query = options.query;
|
||||||
}
|
}
|
||||||
return [params];
|
return [
|
||||||
|
params
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoardsQueryKey = (options: Options<GetBoardsData>) =>
|
export const getBoardsQueryKey = (options: Options<GetBoardsData>) => createQueryKey('getBoards', options);
|
||||||
createQueryKey("getBoards", options);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Boards
|
* Get Boards
|
||||||
@ -106,118 +49,32 @@ export const getBoardsOptions = (options: Options<GetBoardsData>) => {
|
|||||||
...options,
|
...options,
|
||||||
...queryKey[0],
|
...queryKey[0],
|
||||||
signal,
|
signal,
|
||||||
throwOnError: true,
|
throwOnError: true
|
||||||
});
|
});
|
||||||
return data;
|
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
|
* Update Board
|
||||||
*/
|
*/
|
||||||
export const updateBoardMutation = (
|
export const updateBoardMutation = (options?: Partial<Options<UpdateBoardData>>): UseMutationOptions<UpdateBoardResponse2, AxiosError<UpdateBoardError>, Options<UpdateBoardData>> => {
|
||||||
options?: Partial<Options<UpdateBoardData>>
|
const mutationOptions: UseMutationOptions<UpdateBoardResponse2, AxiosError<UpdateBoardError>, Options<UpdateBoardData>> = {
|
||||||
): UseMutationOptions<
|
mutationFn: async (localOptions) => {
|
||||||
UpdateBoardResponse2,
|
|
||||||
AxiosError<UpdateBoardError>,
|
|
||||||
Options<UpdateBoardData>
|
|
||||||
> => {
|
|
||||||
const mutationOptions: UseMutationOptions<
|
|
||||||
UpdateBoardResponse2,
|
|
||||||
AxiosError<UpdateBoardError>,
|
|
||||||
Options<UpdateBoardData>
|
|
||||||
> = {
|
|
||||||
mutationFn: async localOptions => {
|
|
||||||
const { data } = await updateBoard({
|
const { data } = await updateBoard({
|
||||||
...options,
|
...options,
|
||||||
...localOptions,
|
...localOptions,
|
||||||
throwOnError: true,
|
throwOnError: true
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDealsQueryKey = (options: Options<GetDealsData>) =>
|
export const getDealsQueryKey = (options: Options<GetDealsData>) => createQueryKey('getDeals', options);
|
||||||
createQueryKey("getDeals", options);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Deals
|
* Get Deals
|
||||||
@ -229,43 +86,32 @@ export const getDealsOptions = (options: Options<GetDealsData>) => {
|
|||||||
...options,
|
...options,
|
||||||
...queryKey[0],
|
...queryKey[0],
|
||||||
signal,
|
signal,
|
||||||
throwOnError: true,
|
throwOnError: true
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
queryKey: getDealsQueryKey(options),
|
queryKey: getDealsQueryKey(options)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update Deal
|
* Update Deal
|
||||||
*/
|
*/
|
||||||
export const updateDealMutation = (
|
export const updateDealMutation = (options?: Partial<Options<UpdateDealData>>): UseMutationOptions<UpdateDealResponse2, AxiosError<UpdateDealError>, Options<UpdateDealData>> => {
|
||||||
options?: Partial<Options<UpdateDealData>>
|
const mutationOptions: UseMutationOptions<UpdateDealResponse2, AxiosError<UpdateDealError>, Options<UpdateDealData>> = {
|
||||||
): UseMutationOptions<
|
mutationFn: async (localOptions) => {
|
||||||
UpdateDealResponse2,
|
|
||||||
AxiosError<UpdateDealError>,
|
|
||||||
Options<UpdateDealData>
|
|
||||||
> => {
|
|
||||||
const mutationOptions: UseMutationOptions<
|
|
||||||
UpdateDealResponse2,
|
|
||||||
AxiosError<UpdateDealError>,
|
|
||||||
Options<UpdateDealData>
|
|
||||||
> = {
|
|
||||||
mutationFn: async localOptions => {
|
|
||||||
const { data } = await updateDeal({
|
const { data } = await updateDeal({
|
||||||
...options,
|
...options,
|
||||||
...localOptions,
|
...localOptions,
|
||||||
throwOnError: true,
|
throwOnError: true
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProjectsQueryKey = (options?: Options<GetProjectsData>) =>
|
export const getProjectsQueryKey = (options?: Options<GetProjectsData>) => createQueryKey('getProjects', options);
|
||||||
createQueryKey("getProjects", options);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Projects
|
* Get Projects
|
||||||
@ -277,118 +123,15 @@ export const getProjectsOptions = (options?: Options<GetProjectsData>) => {
|
|||||||
...options,
|
...options,
|
||||||
...queryKey[0],
|
...queryKey[0],
|
||||||
signal,
|
signal,
|
||||||
throwOnError: true,
|
throwOnError: true
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
queryKey: getProjectsQueryKey(options),
|
queryKey: getProjectsQueryKey(options)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createProjectQueryKey = (options: Options<CreateProjectData>) =>
|
export const getStatusesQueryKey = (options: Options<GetStatusesData>) => createQueryKey('getStatuses', options);
|
||||||
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);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Statuses
|
* Get Statuses
|
||||||
@ -400,112 +143,27 @@ export const getStatusesOptions = (options: Options<GetStatusesData>) => {
|
|||||||
...options,
|
...options,
|
||||||
...queryKey[0],
|
...queryKey[0],
|
||||||
signal,
|
signal,
|
||||||
throwOnError: true,
|
throwOnError: true
|
||||||
});
|
});
|
||||||
return data;
|
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
|
* Update Status
|
||||||
*/
|
*/
|
||||||
export const updateStatusMutation = (
|
export const updateStatusMutation = (options?: Partial<Options<UpdateStatusData>>): UseMutationOptions<UpdateStatusResponse2, AxiosError<UpdateStatusError>, Options<UpdateStatusData>> => {
|
||||||
options?: Partial<Options<UpdateStatusData>>
|
const mutationOptions: UseMutationOptions<UpdateStatusResponse2, AxiosError<UpdateStatusError>, Options<UpdateStatusData>> = {
|
||||||
): UseMutationOptions<
|
mutationFn: async (localOptions) => {
|
||||||
UpdateStatusResponse2,
|
|
||||||
AxiosError<UpdateStatusError>,
|
|
||||||
Options<UpdateStatusData>
|
|
||||||
> => {
|
|
||||||
const mutationOptions: UseMutationOptions<
|
|
||||||
UpdateStatusResponse2,
|
|
||||||
AxiosError<UpdateStatusError>,
|
|
||||||
Options<UpdateStatusData>
|
|
||||||
> = {
|
|
||||||
mutationFn: async localOptions => {
|
|
||||||
const { data } = await updateStatus({
|
const { data } = await updateStatus({
|
||||||
...options,
|
...options,
|
||||||
...localOptions,
|
...localOptions,
|
||||||
throwOnError: true,
|
throwOnError: true
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
@ -1,13 +1,8 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import { createClientConfig } from "../../hey-api-config";
|
import type { ClientOptions } from './types.gen';
|
||||||
import {
|
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from './client';
|
||||||
createClient,
|
import { createClientConfig } from '../../hey-api-config';
|
||||||
createConfig,
|
|
||||||
type Config,
|
|
||||||
type ClientOptions as DefaultClientOptions,
|
|
||||||
} from "./client";
|
|
||||||
import type { ClientOptions } from "./types.gen";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `createClientConfig()` function will be called on client initialization
|
* 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
|
* `setConfig()`. This is useful for example if you're using Next.js
|
||||||
* to ensure your client always has the correct values.
|
* to ensure your client always has the correct values.
|
||||||
*/
|
*/
|
||||||
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> =
|
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;
|
||||||
(
|
|
||||||
override?: Config<DefaultClientOptions & T>
|
|
||||||
) => Config<Required<DefaultClientOptions> & T>;
|
|
||||||
|
|
||||||
export const client = createClient(
|
export const client = createClient(createClientConfig(createConfig<ClientOptions>({
|
||||||
createClientConfig(
|
baseURL: '/api'
|
||||||
createConfig<ClientOptions>({
|
})));
|
||||||
baseURL: "/api",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@ -1,13 +1,14 @@
|
|||||||
import type { AxiosError, RawAxiosRequestHeaders } from "axios";
|
import type { AxiosError, RawAxiosRequestHeaders } from 'axios';
|
||||||
import axios from "axios";
|
import axios from 'axios';
|
||||||
import type { Client, Config } from "./types";
|
|
||||||
|
import type { Client, Config } from './types';
|
||||||
import {
|
import {
|
||||||
buildUrl,
|
buildUrl,
|
||||||
createConfig,
|
createConfig,
|
||||||
mergeConfigs,
|
mergeConfigs,
|
||||||
mergeHeaders,
|
mergeHeaders,
|
||||||
setAuthParams,
|
setAuthParams,
|
||||||
} from "./utils";
|
} from './utils';
|
||||||
|
|
||||||
export const createClient = (config: Config = {}): Client => {
|
export const createClient = (config: Config = {}): Client => {
|
||||||
let _config = mergeConfigs(createConfig(), config);
|
let _config = mergeConfigs(createConfig(), config);
|
||||||
@ -30,7 +31,7 @@ export const createClient = (config: Config = {}): Client => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const request: Client["request"] = async options => {
|
const request: Client['request'] = async (options) => {
|
||||||
const opts = {
|
const opts = {
|
||||||
..._config,
|
..._config,
|
||||||
...options,
|
...options,
|
||||||
@ -72,7 +73,7 @@ export const createClient = (config: Config = {}): Client => {
|
|||||||
|
|
||||||
let { data } = response;
|
let { data } = response;
|
||||||
|
|
||||||
if (opts.responseType === "json") {
|
if (opts.responseType === 'json') {
|
||||||
if (opts.responseValidator) {
|
if (opts.responseValidator) {
|
||||||
await opts.responseValidator(data);
|
await opts.responseValidator(data);
|
||||||
}
|
}
|
||||||
@ -99,15 +100,15 @@ export const createClient = (config: Config = {}): Client => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
buildUrl,
|
buildUrl,
|
||||||
delete: options => request({ ...options, method: "DELETE" }),
|
delete: (options) => request({ ...options, method: 'DELETE' }),
|
||||||
get: options => request({ ...options, method: "GET" }),
|
get: (options) => request({ ...options, method: 'GET' }),
|
||||||
getConfig,
|
getConfig,
|
||||||
head: options => request({ ...options, method: "HEAD" }),
|
head: (options) => request({ ...options, method: 'HEAD' }),
|
||||||
instance,
|
instance,
|
||||||
options: options => request({ ...options, method: "OPTIONS" }),
|
options: (options) => request({ ...options, method: 'OPTIONS' }),
|
||||||
patch: options => request({ ...options, method: "PATCH" }),
|
patch: (options) => request({ ...options, method: 'PATCH' }),
|
||||||
post: options => request({ ...options, method: "POST" }),
|
post: (options) => request({ ...options, method: 'POST' }),
|
||||||
put: options => request({ ...options, method: "PUT" }),
|
put: (options) => request({ ...options, method: 'PUT' }),
|
||||||
request,
|
request,
|
||||||
setConfig,
|
setConfig,
|
||||||
} as Client;
|
} as Client;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
export type { Auth } from "../core/auth";
|
export type { Auth } from '../core/auth';
|
||||||
export type { QuerySerializerOptions } from "../core/bodySerializer";
|
export type { QuerySerializerOptions } from '../core/bodySerializer';
|
||||||
export {
|
export {
|
||||||
formDataBodySerializer,
|
formDataBodySerializer,
|
||||||
jsonBodySerializer,
|
jsonBodySerializer,
|
||||||
urlSearchParamsBodySerializer,
|
urlSearchParamsBodySerializer,
|
||||||
} from "../core/bodySerializer";
|
} from '../core/bodySerializer';
|
||||||
export { buildClientParams } from "../core/params";
|
export { buildClientParams } from '../core/params';
|
||||||
export { createClient } from "./client";
|
export { createClient } from './client';
|
||||||
export type {
|
export type {
|
||||||
Client,
|
Client,
|
||||||
ClientOptions,
|
ClientOptions,
|
||||||
@ -17,5 +17,5 @@ export type {
|
|||||||
RequestOptions,
|
RequestOptions,
|
||||||
RequestResult,
|
RequestResult,
|
||||||
TDataShape,
|
TDataShape,
|
||||||
} from "./types";
|
} from './types';
|
||||||
export { createConfig } from "./utils";
|
export { createConfig } from './utils';
|
||||||
|
|||||||
@ -5,15 +5,16 @@ import type {
|
|||||||
AxiosResponse,
|
AxiosResponse,
|
||||||
AxiosStatic,
|
AxiosStatic,
|
||||||
CreateAxiosDefaults,
|
CreateAxiosDefaults,
|
||||||
} from "axios";
|
} from 'axios';
|
||||||
import type { Auth } from "../core/auth";
|
|
||||||
import type { Client as CoreClient, Config as CoreConfig } from "../core/types";
|
import type { Auth } from '../core/auth';
|
||||||
|
import type {
|
||||||
|
Client as CoreClient,
|
||||||
|
Config as CoreConfig,
|
||||||
|
} from '../core/types';
|
||||||
|
|
||||||
export interface Config<T extends ClientOptions = ClientOptions>
|
export interface Config<T extends ClientOptions = ClientOptions>
|
||||||
extends Omit<
|
extends Omit<CreateAxiosDefaults, 'auth' | 'baseURL' | 'headers' | 'method'>,
|
||||||
CreateAxiosDefaults,
|
|
||||||
"auth" | "baseURL" | "headers" | "method"
|
|
||||||
>,
|
|
||||||
CoreConfig {
|
CoreConfig {
|
||||||
/**
|
/**
|
||||||
* Axios implementation. You can use this option to provide a custom
|
* 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.
|
* 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
|
* An object containing any HTTP headers that you want to pre-populate your
|
||||||
* `Headers` object with.
|
* `Headers` object with.
|
||||||
@ -49,7 +50,7 @@ export interface Config<T extends ClientOptions = ClientOptions>
|
|||||||
*
|
*
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
throwOnError?: T["throwOnError"];
|
throwOnError?: T['throwOnError'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestOptions<
|
export interface RequestOptions<
|
||||||
@ -85,14 +86,10 @@ export type RequestResult<
|
|||||||
>
|
>
|
||||||
: Promise<
|
: Promise<
|
||||||
| (AxiosResponse<
|
| (AxiosResponse<
|
||||||
TData extends Record<string, unknown>
|
TData extends Record<string, unknown> ? TData[keyof TData] : TData
|
||||||
? TData[keyof TData]
|
|
||||||
: TData
|
|
||||||
> & { error: undefined })
|
> & { error: undefined })
|
||||||
| (AxiosError<
|
| (AxiosError<
|
||||||
TError extends Record<string, unknown>
|
TError extends Record<string, unknown> ? TError[keyof TError] : TError
|
||||||
? TError[keyof TError]
|
|
||||||
: TError
|
|
||||||
> & {
|
> & {
|
||||||
data: undefined;
|
data: undefined;
|
||||||
error: TError extends Record<string, unknown>
|
error: TError extends Record<string, unknown>
|
||||||
@ -111,7 +108,7 @@ type MethodFn = <
|
|||||||
TError = unknown,
|
TError = unknown,
|
||||||
ThrowOnError extends boolean = false,
|
ThrowOnError extends boolean = false,
|
||||||
>(
|
>(
|
||||||
options: Omit<RequestOptions<ThrowOnError>, "method">
|
options: Omit<RequestOptions<ThrowOnError>, 'method'>,
|
||||||
) => RequestResult<TData, TError, ThrowOnError>;
|
) => RequestResult<TData, TError, ThrowOnError>;
|
||||||
|
|
||||||
type RequestFn = <
|
type RequestFn = <
|
||||||
@ -119,8 +116,8 @@ type RequestFn = <
|
|||||||
TError = unknown,
|
TError = unknown,
|
||||||
ThrowOnError extends boolean = false,
|
ThrowOnError extends boolean = false,
|
||||||
>(
|
>(
|
||||||
options: Omit<RequestOptions<ThrowOnError>, "method"> &
|
options: Omit<RequestOptions<ThrowOnError>, 'method'> &
|
||||||
Pick<Required<RequestOptions<ThrowOnError>>, "method">
|
Pick<Required<RequestOptions<ThrowOnError>>, 'method'>,
|
||||||
) => RequestResult<TData, TError, ThrowOnError>;
|
) => RequestResult<TData, TError, ThrowOnError>;
|
||||||
|
|
||||||
type BuildUrlFn = <
|
type BuildUrlFn = <
|
||||||
@ -131,7 +128,7 @@ type BuildUrlFn = <
|
|||||||
url: string;
|
url: string;
|
||||||
},
|
},
|
||||||
>(
|
>(
|
||||||
options: Pick<TData, "url"> & Omit<Options<TData>, "axios">
|
options: Pick<TData, 'url'> & Omit<Options<TData>, 'axios'>,
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
|
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.
|
* to ensure your client always has the correct values.
|
||||||
*/
|
*/
|
||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
||||||
override?: Config<ClientOptions & T>
|
override?: Config<ClientOptions & T>,
|
||||||
) => Config<Required<ClientOptions> & T>;
|
) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
export interface TDataShape {
|
export interface TDataShape {
|
||||||
@ -163,21 +160,20 @@ type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
|||||||
export type Options<
|
export type Options<
|
||||||
TData extends TDataShape = TDataShape,
|
TData extends TDataShape = TDataShape,
|
||||||
ThrowOnError extends boolean = boolean,
|
ThrowOnError extends boolean = boolean,
|
||||||
> = OmitKeys<RequestOptions<ThrowOnError>, "body" | "path" | "query" | "url"> &
|
> = OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'path' | 'query' | 'url'> &
|
||||||
Omit<TData, "url">;
|
Omit<TData, 'url'>;
|
||||||
|
|
||||||
export type OptionsLegacyParser<
|
export type OptionsLegacyParser<
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
ThrowOnError extends boolean = boolean,
|
ThrowOnError extends boolean = boolean,
|
||||||
> = TData extends { body?: any }
|
> = TData extends { body?: any }
|
||||||
? TData extends { headers?: any }
|
? TData extends { headers?: any }
|
||||||
? OmitKeys<RequestOptions<ThrowOnError>, "body" | "headers" | "url"> &
|
? OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'headers' | 'url'> & TData
|
||||||
TData
|
: OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'url'> &
|
||||||
: OmitKeys<RequestOptions<ThrowOnError>, "body" | "url"> &
|
|
||||||
TData &
|
TData &
|
||||||
Pick<RequestOptions<ThrowOnError>, "headers">
|
Pick<RequestOptions<ThrowOnError>, 'headers'>
|
||||||
: TData extends { headers?: any }
|
: TData extends { headers?: any }
|
||||||
? OmitKeys<RequestOptions<ThrowOnError>, "headers" | "url"> &
|
? OmitKeys<RequestOptions<ThrowOnError>, 'headers' | 'url'> &
|
||||||
TData &
|
TData &
|
||||||
Pick<RequestOptions<ThrowOnError>, "body">
|
Pick<RequestOptions<ThrowOnError>, 'body'>
|
||||||
: OmitKeys<RequestOptions<ThrowOnError>, "url"> & TData;
|
: OmitKeys<RequestOptions<ThrowOnError>, 'url'> & TData;
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { getAuthToken } from "../core/auth";
|
import { getAuthToken } from '../core/auth';
|
||||||
import type {
|
import type {
|
||||||
QuerySerializer,
|
QuerySerializer,
|
||||||
QuerySerializerOptions,
|
QuerySerializerOptions,
|
||||||
} from "../core/bodySerializer";
|
} from '../core/bodySerializer';
|
||||||
import type { ArraySeparatorStyle } from "../core/pathSerializer";
|
import type { ArraySeparatorStyle } from '../core/pathSerializer';
|
||||||
import {
|
import {
|
||||||
serializeArrayParam,
|
serializeArrayParam,
|
||||||
serializeObjectParam,
|
serializeObjectParam,
|
||||||
serializePrimitiveParam,
|
serializePrimitiveParam,
|
||||||
} from "../core/pathSerializer";
|
} from '../core/pathSerializer';
|
||||||
import type { Client, ClientOptions, Config, RequestOptions } from "./types";
|
import type { Client, ClientOptions, Config, RequestOptions } from './types';
|
||||||
|
|
||||||
interface PathSerializer {
|
interface PathSerializer {
|
||||||
path: Record<string, unknown>;
|
path: Record<string, unknown>;
|
||||||
@ -25,19 +25,19 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
|||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
let explode = false;
|
let explode = false;
|
||||||
let name = match.substring(1, match.length - 1);
|
let name = match.substring(1, match.length - 1);
|
||||||
let style: ArraySeparatorStyle = "simple";
|
let style: ArraySeparatorStyle = 'simple';
|
||||||
|
|
||||||
if (name.endsWith("*")) {
|
if (name.endsWith('*')) {
|
||||||
explode = true;
|
explode = true;
|
||||||
name = name.substring(0, name.length - 1);
|
name = name.substring(0, name.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.startsWith(".")) {
|
if (name.startsWith('.')) {
|
||||||
name = name.substring(1);
|
name = name.substring(1);
|
||||||
style = "label";
|
style = 'label';
|
||||||
} else if (name.startsWith(";")) {
|
} else if (name.startsWith(';')) {
|
||||||
name = name.substring(1);
|
name = name.substring(1);
|
||||||
style = "matrix";
|
style = 'matrix';
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = path[name];
|
const value = path[name];
|
||||||
@ -49,12 +49,12 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
|||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
url = url.replace(
|
url = url.replace(
|
||||||
match,
|
match,
|
||||||
serializeArrayParam({ explode, name, style, value })
|
serializeArrayParam({ explode, name, style, value }),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "object") {
|
if (typeof value === 'object') {
|
||||||
url = url.replace(
|
url = url.replace(
|
||||||
match,
|
match,
|
||||||
serializeObjectParam({
|
serializeObjectParam({
|
||||||
@ -63,24 +63,24 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
|||||||
style,
|
style,
|
||||||
value: value as Record<string, unknown>,
|
value: value as Record<string, unknown>,
|
||||||
valueOnly: true,
|
valueOnly: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (style === "matrix") {
|
if (style === 'matrix') {
|
||||||
url = url.replace(
|
url = url.replace(
|
||||||
match,
|
match,
|
||||||
`;${serializePrimitiveParam({
|
`;${serializePrimitiveParam({
|
||||||
name,
|
name,
|
||||||
value: value as string,
|
value: value as string,
|
||||||
})}`
|
})}`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaceValue = encodeURIComponent(
|
const replaceValue = encodeURIComponent(
|
||||||
style === "label" ? `.${value as string}` : (value as string)
|
style === 'label' ? `.${value as string}` : (value as string),
|
||||||
);
|
);
|
||||||
url = url.replace(match, replaceValue);
|
url = url.replace(match, replaceValue);
|
||||||
}
|
}
|
||||||
@ -95,7 +95,7 @@ export const createQuerySerializer = <T = unknown>({
|
|||||||
}: QuerySerializerOptions = {}) => {
|
}: QuerySerializerOptions = {}) => {
|
||||||
const querySerializer = (queryParams: T) => {
|
const querySerializer = (queryParams: T) => {
|
||||||
const search: string[] = [];
|
const search: string[] = [];
|
||||||
if (queryParams && typeof queryParams === "object") {
|
if (queryParams && typeof queryParams === 'object') {
|
||||||
for (const name in queryParams) {
|
for (const name in queryParams) {
|
||||||
const value = queryParams[name];
|
const value = queryParams[name];
|
||||||
|
|
||||||
@ -108,17 +108,17 @@ export const createQuerySerializer = <T = unknown>({
|
|||||||
allowReserved,
|
allowReserved,
|
||||||
explode: true,
|
explode: true,
|
||||||
name,
|
name,
|
||||||
style: "form",
|
style: 'form',
|
||||||
value,
|
value,
|
||||||
...array,
|
...array,
|
||||||
});
|
});
|
||||||
if (serializedArray) search.push(serializedArray);
|
if (serializedArray) search.push(serializedArray);
|
||||||
} else if (typeof value === "object") {
|
} else if (typeof value === 'object') {
|
||||||
const serializedObject = serializeObjectParam({
|
const serializedObject = serializeObjectParam({
|
||||||
allowReserved,
|
allowReserved,
|
||||||
explode: true,
|
explode: true,
|
||||||
name,
|
name,
|
||||||
style: "deepObject",
|
style: 'deepObject',
|
||||||
value: value as Record<string, unknown>,
|
value: value as Record<string, unknown>,
|
||||||
...object,
|
...object,
|
||||||
});
|
});
|
||||||
@ -133,7 +133,7 @@ export const createQuerySerializer = <T = unknown>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return search.join("&");
|
return search.join('&');
|
||||||
};
|
};
|
||||||
return querySerializer;
|
return querySerializer;
|
||||||
};
|
};
|
||||||
@ -141,8 +141,8 @@ export const createQuerySerializer = <T = unknown>({
|
|||||||
export const setAuthParams = async ({
|
export const setAuthParams = async ({
|
||||||
security,
|
security,
|
||||||
...options
|
...options
|
||||||
}: Pick<Required<RequestOptions>, "security"> &
|
}: Pick<Required<RequestOptions>, 'security'> &
|
||||||
Pick<RequestOptions, "auth" | "query"> & {
|
Pick<RequestOptions, 'auth' | 'query'> & {
|
||||||
headers: Record<any, unknown>;
|
headers: Record<any, unknown>;
|
||||||
}) => {
|
}) => {
|
||||||
for (const auth of security) {
|
for (const auth of security) {
|
||||||
@ -152,26 +152,25 @@ export const setAuthParams = async ({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = auth.name ?? "Authorization";
|
const name = auth.name ?? 'Authorization';
|
||||||
|
|
||||||
switch (auth.in) {
|
switch (auth.in) {
|
||||||
case "query":
|
case 'query':
|
||||||
if (!options.query) {
|
if (!options.query) {
|
||||||
options.query = {};
|
options.query = {};
|
||||||
}
|
}
|
||||||
options.query[name] = token;
|
options.query[name] = token;
|
||||||
break;
|
break;
|
||||||
case "cookie": {
|
case 'cookie': {
|
||||||
const value = `${name}=${token}`;
|
const value = `${name}=${token}`;
|
||||||
if ("Cookie" in options.headers && options.headers["Cookie"]) {
|
if ('Cookie' in options.headers && options.headers['Cookie']) {
|
||||||
options.headers["Cookie"] =
|
options.headers['Cookie'] = `${options.headers['Cookie']}; ${value}`;
|
||||||
`${options.headers["Cookie"]}; ${value}`;
|
|
||||||
} else {
|
} else {
|
||||||
options.headers["Cookie"] = value;
|
options.headers['Cookie'] = value;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "header":
|
case 'header':
|
||||||
default:
|
default:
|
||||||
options.headers[name] = token;
|
options.headers[name] = token;
|
||||||
break;
|
break;
|
||||||
@ -181,13 +180,13 @@ export const setAuthParams = async ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildUrl: Client["buildUrl"] = options => {
|
export const buildUrl: Client['buildUrl'] = (options) => {
|
||||||
const url = getUrl({
|
const url = getUrl({
|
||||||
path: options.path,
|
path: options.path,
|
||||||
// let `paramsSerializer()` handle query params if it exists
|
// let `paramsSerializer()` handle query params if it exists
|
||||||
query: !options.paramsSerializer ? options.query : undefined,
|
query: !options.paramsSerializer ? options.query : undefined,
|
||||||
querySerializer:
|
querySerializer:
|
||||||
typeof options.querySerializer === "function"
|
typeof options.querySerializer === 'function'
|
||||||
? options.querySerializer
|
? options.querySerializer
|
||||||
: createQuerySerializer(options.querySerializer),
|
: createQuerySerializer(options.querySerializer),
|
||||||
url: options.url,
|
url: options.url,
|
||||||
@ -206,13 +205,13 @@ export const getUrl = ({
|
|||||||
querySerializer: QuerySerializer;
|
querySerializer: QuerySerializer;
|
||||||
url: string;
|
url: string;
|
||||||
}) => {
|
}) => {
|
||||||
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
|
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
|
||||||
let url = pathUrl;
|
let url = pathUrl;
|
||||||
if (path) {
|
if (path) {
|
||||||
url = defaultPathSerializer({ path, url });
|
url = defaultPathSerializer({ path, url });
|
||||||
}
|
}
|
||||||
let search = query ? querySerializer(query) : "";
|
let search = query ? querySerializer(query) : '';
|
||||||
if (search.startsWith("?")) {
|
if (search.startsWith('?')) {
|
||||||
search = search.substring(1);
|
search = search.substring(1);
|
||||||
}
|
}
|
||||||
if (search) {
|
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.
|
* Special Axios headers keywords allowing to set headers by request method.
|
||||||
*/
|
*/
|
||||||
export const axiosHeadersKeywords = [
|
export const axiosHeadersKeywords = [
|
||||||
"common",
|
'common',
|
||||||
"delete",
|
'delete',
|
||||||
"get",
|
'get',
|
||||||
"head",
|
'head',
|
||||||
"patch",
|
'patch',
|
||||||
"post",
|
'post',
|
||||||
"put",
|
'put',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const mergeHeaders = (
|
export const mergeHeaders = (
|
||||||
...headers: Array<Required<Config>["headers"] | undefined>
|
...headers: Array<Required<Config>['headers'] | undefined>
|
||||||
): Record<any, unknown> => {
|
): Record<any, unknown> => {
|
||||||
const mergedHeaders: Record<any, unknown> = {};
|
const mergedHeaders: Record<any, unknown> = {};
|
||||||
for (const header of headers) {
|
for (const header of headers) {
|
||||||
if (!header || typeof header !== "object") {
|
if (!header || typeof header !== 'object') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,9 +253,9 @@ export const mergeHeaders = (
|
|||||||
for (const [key, value] of iterator) {
|
for (const [key, value] of iterator) {
|
||||||
if (
|
if (
|
||||||
axiosHeadersKeywords.includes(
|
axiosHeadersKeywords.includes(
|
||||||
key as (typeof axiosHeadersKeywords)[number]
|
key as (typeof axiosHeadersKeywords)[number],
|
||||||
) &&
|
) &&
|
||||||
typeof value === "object"
|
typeof value === 'object'
|
||||||
) {
|
) {
|
||||||
mergedHeaders[key] = {
|
mergedHeaders[key] = {
|
||||||
...(mergedHeaders[key] as Record<any, unknown>),
|
...(mergedHeaders[key] as Record<any, unknown>),
|
||||||
@ -267,18 +266,13 @@ export const mergeHeaders = (
|
|||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
for (const v of value) {
|
for (const v of value) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
mergedHeaders[key] = [
|
mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string];
|
||||||
...(mergedHeaders[key] ?? []),
|
|
||||||
v as string,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
} else if (value !== undefined) {
|
} else if (value !== undefined) {
|
||||||
// assume object headers are meant to be JSON stringified, i.e. their
|
// assume object headers are meant to be JSON stringified, i.e. their
|
||||||
// content value in OpenAPI specification is 'application/json'
|
// content value in OpenAPI specification is 'application/json'
|
||||||
mergedHeaders[key] =
|
mergedHeaders[key] =
|
||||||
typeof value === "object"
|
typeof value === 'object' ? JSON.stringify(value) : (value as string);
|
||||||
? JSON.stringify(value)
|
|
||||||
: (value as string);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -286,7 +280,7 @@ export const mergeHeaders = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
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> => ({
|
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
||||||
...override,
|
...override,
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user