fix: fixed columns draggables and styles
This commit is contained in:
@ -29,7 +29,8 @@ const BoardsMobile = () => {
|
||||
scrollbars={"x"}
|
||||
scrollbarSize={0}
|
||||
w={"100vw"}
|
||||
mt={5}>
|
||||
mt={5}
|
||||
mb={"sm"}>
|
||||
<Group
|
||||
wrap={"nowrap"}
|
||||
gap={0}>
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
|
||||
.container {
|
||||
border-radius: var(--mantine-spacing-md);
|
||||
flex-wrap: nowrap;
|
||||
border-bottom: solid dodgerblue 3px;
|
||||
margin-bottom: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
@mixin light {
|
||||
background-color: var(--color-light-aqua);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
@ -12,7 +12,9 @@ const CreateStatusButton = () => {
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Box className={styles.container}>
|
||||
<Box
|
||||
className={styles.container}
|
||||
style={{ width: "fit-content", minWidth: "auto" }}>
|
||||
<InPlaceInput
|
||||
placeholder={"Название колонки"}
|
||||
onComplete={onCreateStatus}
|
||||
@ -32,7 +34,8 @@ const CreateStatusButton = () => {
|
||||
modalTitle={"Создание колонки"}
|
||||
inputStyles={{
|
||||
wrapper: {
|
||||
padding: 4,
|
||||
paddingInline: "var(--mantine-spacing-md)",
|
||||
paddingBlock: "var(--mantine-spacing-xs)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
|
||||
.container {
|
||||
@mixin light {
|
||||
background-color: var(--color-light-aqua);
|
||||
background-color: var(--color-light-white-blue);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer";
|
||||
import StatusColumnHeader from "@/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader";
|
||||
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
@ -43,14 +44,21 @@ const Funnel: FC = () => {
|
||||
}
|
||||
renderContainer={(
|
||||
status: StatusSchema,
|
||||
funnelColumnComponent: ReactNode
|
||||
funnelColumnComponent: ReactNode,
|
||||
renderDraggable
|
||||
) => (
|
||||
<StatusColumnWrapper
|
||||
status={status}
|
||||
isDragging={activeStatus?.id === status.id}>
|
||||
renderHeader={renderDraggable}>
|
||||
{funnelColumnComponent}
|
||||
</StatusColumnWrapper>
|
||||
)}
|
||||
renderContainerHeader={status => (
|
||||
<StatusColumnHeader
|
||||
status={status}
|
||||
isDragging={activeStatus?.id === status.id}
|
||||
/>
|
||||
)}
|
||||
renderItem={(deal: DealSchema) => (
|
||||
<DealContainer
|
||||
key={deal.id}
|
||||
@ -63,7 +71,12 @@ const Funnel: FC = () => {
|
||||
renderContainerOverlay={(status: StatusSchema, children) => (
|
||||
<StatusColumnWrapper
|
||||
status={status}
|
||||
isDragging>
|
||||
renderHeader={() => (
|
||||
<StatusColumnHeader
|
||||
status={status}
|
||||
isDragging={activeStatus?.id === status.id}
|
||||
/>
|
||||
)}>
|
||||
{children}
|
||||
</StatusColumnWrapper>
|
||||
)}
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
|
||||
.header {
|
||||
border-bottom: solid dodgerblue 3px;
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import React, { FC } from "react";
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import styles from "./StatusColumnHeader.module.css";
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
||||
const { onUpdateStatus } = useStatusesContext();
|
||||
|
||||
const handleSave = (value: string) => {
|
||||
const newValue = value.trim();
|
||||
if (newValue && newValue !== status.name) {
|
||||
onUpdateStatus(status.id, { name: newValue });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusColumnHeader;
|
||||
@ -1,10 +1,20 @@
|
||||
|
||||
.container {
|
||||
height: calc(100vh - 150px);
|
||||
|
||||
@media (max-width: 48em) {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
border-bottom: solid dodgerblue 3px;
|
||||
.inner-container {
|
||||
border-radius: var(--mantine-spacing-md);
|
||||
gap: 0;
|
||||
|
||||
@mixin light {
|
||||
background-color: var(--color-light-aqua);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,68 +1,24 @@
|
||||
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 { Box, Stack } from "@mantine/core";
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
import styles from "./StatusColumnWrapper.module.css";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
isDragging?: boolean;
|
||||
renderHeader: () => ReactNode;
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
const StatusColumnWrapper = ({ renderHeader, children }: Props) => {
|
||||
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}
|
||||
<Stack
|
||||
px={"xs"}
|
||||
pb={"xs"}
|
||||
className={styles["inner-container"]}>
|
||||
{renderHeader()}
|
||||
{children}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { RefObject, useMemo, useRef, useState } from "react";
|
||||
import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core";
|
||||
import { SwiperRef } from "swiper/swiper-react";
|
||||
import { useDebouncedCallback } from "@mantine/hooks";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
|
||||
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
import { sortByLexorank } from "@/utils/lexorank";
|
||||
import { SwiperRef } from "swiper/swiper-react";
|
||||
|
||||
type ReturnType = {
|
||||
sortedStatuses: StatusSchema[];
|
||||
@ -26,6 +27,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
const { statuses, setStatuses, updateStatus } = useStatusesContext();
|
||||
const { deals, setDeals, updateDeal } = useDealsContext();
|
||||
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const {
|
||||
getNewRankForSameStatus,
|
||||
@ -42,32 +44,50 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
||||
return statuses.find(status => status.id === deal.statusId);
|
||||
};
|
||||
|
||||
const swipeSliderDuringDrag = (activeId: number, over: Over) => {
|
||||
const activeStatusLexorank = getStatusByDealId(
|
||||
Number(activeId)
|
||||
)?.lexorank;
|
||||
let overStatusLexorank: string | undefined;
|
||||
|
||||
if (typeof over.id === "string" && isStatusId(over.id)) {
|
||||
const overStatusId = getStatusId(over.id);
|
||||
overStatusLexorank = statuses.find(
|
||||
s => s.id === overStatusId
|
||||
)?.lexorank;
|
||||
} else {
|
||||
overStatusLexorank = getStatusByDealId(Number(over.id))?.lexorank;
|
||||
}
|
||||
|
||||
if (
|
||||
!activeStatusLexorank ||
|
||||
!overStatusLexorank ||
|
||||
!swiperRef.current?.swiper
|
||||
)
|
||||
return;
|
||||
|
||||
const activeIndex = sortedStatuses.findIndex(
|
||||
s => s.lexorank === activeStatusLexorank
|
||||
);
|
||||
const overIndex = sortedStatuses.findIndex(
|
||||
s => s.lexorank === overStatusLexorank
|
||||
);
|
||||
|
||||
if (activeIndex > overIndex) {
|
||||
swiperRef.current.swiper.slidePrev();
|
||||
return;
|
||||
}
|
||||
if (activeIndex < overIndex) {
|
||||
swiperRef.current.swiper.slideNext();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = ({ active, over }: DragOverEvent) => {
|
||||
if (!over) return;
|
||||
const activeId = active.id as string | number;
|
||||
|
||||
// Only perform swiper navigation for deal drag (not status column drag)
|
||||
if (typeof activeId !== "string") {
|
||||
const activeStatusLexorank = getStatusByDealId(Number(activeId))?.lexorank;
|
||||
let overStatusLexorank: string | undefined;
|
||||
|
||||
if (typeof over.id === "string" && isStatusId(over.id)) {
|
||||
const overStatusId = getStatusId(over.id);
|
||||
overStatusLexorank = statuses.find(s => s.id === overStatusId)?.lexorank;
|
||||
} else {
|
||||
overStatusLexorank = getStatusByDealId(Number(over.id))?.lexorank;
|
||||
}
|
||||
|
||||
if (activeStatusLexorank && overStatusLexorank && swiperRef.current?.swiper) {
|
||||
const activeIndex = sortedStatuses.findIndex(s => s.lexorank === activeStatusLexorank);
|
||||
const overIndex = sortedStatuses.findIndex(s => s.lexorank === overStatusLexorank);
|
||||
|
||||
if (activeIndex > overIndex) {
|
||||
swiperRef.current.swiper.slidePrev();
|
||||
} else if (activeIndex < overIndex) {
|
||||
swiperRef.current.swiper.slideNext();
|
||||
}
|
||||
}
|
||||
if (isMobile && typeof activeId !== "string") {
|
||||
swipeSliderDuringDrag(activeId, over);
|
||||
}
|
||||
|
||||
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
/* Colors */
|
||||
--color-light-gray-blue: #f4f7fd;
|
||||
--color-light-aqua: #e0f0f4;
|
||||
--color-light-white-blue: #f5fbfc;
|
||||
--color-light-whitesmoke: #fcfdff;
|
||||
--mantine-color-dark-7-5: #212121;
|
||||
/* Shadows */
|
||||
|
||||
Reference in New Issue
Block a user