feat: status creating
This commit is contained in:
@ -1,7 +1,8 @@
|
|||||||
import React, { FC, useEffect, useRef, useState } from "react";
|
import React, { FC, useState } from "react";
|
||||||
import { Group, Text, TextInput } from "@mantine/core";
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
import BoardMenu from "@/app/deals/components/Board/BoardMenu";
|
import BoardMenu from "@/app/deals/components/Board/BoardMenu";
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||||
import { BoardSchema } from "@/lib/client";
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -10,48 +11,7 @@ type Props = {
|
|||||||
|
|
||||||
const Board: FC<Props> = ({ board }) => {
|
const Board: FC<Props> = ({ board }) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [editValue, setEditValue] = useState(board.name);
|
|
||||||
const { onUpdateBoard } = useBoardsContext();
|
const { onUpdateBoard } = useBoardsContext();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEditing && inputRef.current) {
|
|
||||||
inputRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [isEditing]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
isEditing &&
|
|
||||||
inputRef.current &&
|
|
||||||
!inputRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
handleSave();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () =>
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, [isEditing, editValue]);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
const newValue = editValue.trim();
|
|
||||||
if (newValue && newValue !== board.name) {
|
|
||||||
onUpdateBoard(board.id, { name: newValue });
|
|
||||||
}
|
|
||||||
setIsEditing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
setEditValue(board.name);
|
|
||||||
setIsEditing(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
@ -63,33 +23,27 @@ const Board: FC<Props> = ({ board }) => {
|
|||||||
style={{ borderWidth: 1 }}
|
style={{ borderWidth: 1 }}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}>
|
onMouseLeave={() => setIsHovered(false)}>
|
||||||
{isEditing ? (
|
<InPlaceInput
|
||||||
<TextInput
|
defaultValue={board.name}
|
||||||
ref={inputRef}
|
onComplete={value => onUpdateBoard(board.id, { name: value })}
|
||||||
value={editValue}
|
inputStyles={{
|
||||||
onChange={e => setEditValue(e.target.value)}
|
input: {
|
||||||
onKeyDown={e => {
|
height: 25,
|
||||||
if (e.key === "Enter") handleSave();
|
minHeight: 25,
|
||||||
if (e.key === "Escape") {
|
},
|
||||||
setEditValue(board.name);
|
}}
|
||||||
setIsEditing(false);
|
getChildren={startEditing => (
|
||||||
}
|
<>
|
||||||
}}
|
<Box>
|
||||||
variant="unstyled"
|
<Text>{board.name}</Text>
|
||||||
styles={{
|
</Box>
|
||||||
input: {
|
<BoardMenu
|
||||||
height: 25,
|
isHovered={isHovered}
|
||||||
minHeight: 25,
|
board={board}
|
||||||
},
|
startEditing={startEditing}
|
||||||
}}
|
/>
|
||||||
/>
|
</>
|
||||||
) : (
|
)}
|
||||||
<Text>{board.name}</Text>
|
|
||||||
)}
|
|
||||||
<BoardMenu
|
|
||||||
isHovered={isHovered}
|
|
||||||
board={board}
|
|
||||||
handleEdit={handleEdit}
|
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,10 +7,10 @@ import { BoardSchema } from "@/lib/client";
|
|||||||
type Props = {
|
type Props = {
|
||||||
isHovered: boolean;
|
isHovered: boolean;
|
||||||
board: BoardSchema;
|
board: BoardSchema;
|
||||||
handleEdit: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
startEditing: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoardMenu: FC<Props> = ({ isHovered, board, handleEdit }) => {
|
const BoardMenu: FC<Props> = ({ isHovered, board, startEditing }) => {
|
||||||
const { selectedBoard, onDeleteBoard } = useBoardsContext();
|
const { selectedBoard, onDeleteBoard } = useBoardsContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -30,7 +30,12 @@ const BoardMenu: FC<Props> = ({ isHovered, board, handleEdit }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item onClick={handleEdit}>
|
<Menu.Item
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
startEditing();
|
||||||
|
}}>
|
||||||
<Group wrap={"nowrap"}>
|
<Group wrap={"nowrap"}>
|
||||||
<IconEdit />
|
<IconEdit />
|
||||||
<Text>Переименовать</Text>
|
<Text>Переименовать</Text>
|
||||||
|
|||||||
@ -1,52 +1,22 @@
|
|||||||
import { useState } from "react";
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
import { IconCheck, IconPlus, IconX } from "@tabler/icons-react";
|
import { Box } from "@mantine/core";
|
||||||
import { Box, Group, TextInput } from "@mantine/core";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||||
|
|
||||||
const CreateBoardButton = () => {
|
const CreateBoardButton = () => {
|
||||||
const { onCreateBoard } = useBoardsContext();
|
const { onCreateBoard } = useBoardsContext();
|
||||||
const [isWriting, setIsWriting] = useState<boolean>(false);
|
|
||||||
const [name, setName] = useState<string>("");
|
|
||||||
|
|
||||||
const onStartCreating = () => {
|
|
||||||
setName("");
|
|
||||||
setIsWriting(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCancelCreating = () => setIsWriting(false);
|
|
||||||
|
|
||||||
const onCompleteCreating = () => {
|
|
||||||
if (name) {
|
|
||||||
onCreateBoard(name);
|
|
||||||
}
|
|
||||||
setIsWriting(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={{ cursor: "pointer" }}>
|
<Box style={{ cursor: "pointer" }}>
|
||||||
{isWriting ? (
|
<InPlaceInput
|
||||||
<Group>
|
placeholder={"Название доски"}
|
||||||
<TextInput
|
onComplete={onCreateBoard}
|
||||||
placeholder={"Название доски"}
|
getChildren={startEditing => (
|
||||||
variant={"unstyled"}
|
<Box onClick={startEditing}>
|
||||||
value={name}
|
<IconPlus />
|
||||||
onChange={e => setName(e.target.value)}
|
|
||||||
onKeyDown={e =>
|
|
||||||
e.key === "Enter" && onCompleteCreating()
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Box onClick={onCompleteCreating}>
|
|
||||||
<IconCheck />
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box onClick={onCancelCreating}>
|
)}
|
||||||
<IconX />
|
/>
|
||||||
</Box>
|
|
||||||
</Group>
|
|
||||||
) : (
|
|
||||||
<Box onClick={onStartCreating}>
|
|
||||||
<IconPlus />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box, Text } from "@mantine/core";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||||
|
|
||||||
|
const CreateStatusButton = () => {
|
||||||
|
const { onCreateStatus } = useStatusesContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: "dashed",
|
||||||
|
borderColor: "gray",
|
||||||
|
width: "15vw",
|
||||||
|
minWidth: 150,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}>
|
||||||
|
<InPlaceInput
|
||||||
|
placeholder={"Название доски"}
|
||||||
|
onComplete={onCreateStatus}
|
||||||
|
getChildren={startEditing => (
|
||||||
|
<Box
|
||||||
|
p={9}
|
||||||
|
onClick={() => startEditing()}>
|
||||||
|
<Text>Создать колонку</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateStatusButton;
|
||||||
@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC, ReactNode } from "react";
|
||||||
|
import { Group, ScrollArea } from "@mantine/core";
|
||||||
|
import CreateStatusButton from "@/app/deals/components/CreateStatusButton/CreateStatusButton";
|
||||||
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||||
import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
|
import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
|
||||||
import StatusColumnWrapper from "@/app/deals/components/StatusColumnWrapper/StatusColumnWrapper";
|
import StatusColumnWrapper from "@/app/deals/components/StatusColumnWrapper/StatusColumnWrapper";
|
||||||
@ -23,45 +25,62 @@ const Funnel: FC = () => {
|
|||||||
} = useDealsAndStatusesDnd();
|
} = useDealsAndStatusesDnd();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FunnelDnd
|
<ScrollArea
|
||||||
containers={sortedStatuses}
|
offsetScrollbars={"x"}
|
||||||
items={deals}
|
scrollbarSize={"0.5rem"}>
|
||||||
onDragStart={handleDragStart}
|
<Group align={"start"}>
|
||||||
onDragOver={handleDragOver}
|
<FunnelDnd
|
||||||
onDragEnd={handleDragEnd}
|
containers={sortedStatuses}
|
||||||
getContainerId={(status: StatusSchema) => `${status.id}-status`}
|
items={deals}
|
||||||
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
|
onDragStart={handleDragStart}
|
||||||
sortByLexorank(
|
onDragOver={handleDragOver}
|
||||||
items.filter(deal => deal.statusId === status.id)
|
onDragEnd={handleDragEnd}
|
||||||
)
|
getContainerId={(status: StatusSchema) =>
|
||||||
}
|
`${status.id}-status`
|
||||||
renderContainer={(
|
}
|
||||||
status: StatusSchema,
|
getItemsByContainer={(
|
||||||
funnelColumnComponent: ReactNode
|
status: StatusSchema,
|
||||||
) => (
|
items: DealSchema[]
|
||||||
<StatusColumnWrapper
|
) =>
|
||||||
status={status}
|
sortByLexorank(
|
||||||
isDragging={activeStatus?.id === status.id}>
|
items.filter(deal => deal.statusId === status.id)
|
||||||
{funnelColumnComponent}
|
)
|
||||||
</StatusColumnWrapper>
|
}
|
||||||
)}
|
renderContainer={(
|
||||||
renderItem={(deal: DealSchema) => (
|
status: StatusSchema,
|
||||||
<DealContainer
|
funnelColumnComponent: ReactNode
|
||||||
key={deal.id}
|
) => (
|
||||||
deal={deal}
|
<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>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
<CreateStatusButton />
|
||||||
activeContainer={activeStatus}
|
</Group>
|
||||||
activeItem={activeDeal}
|
</ScrollArea>
|
||||||
renderItemOverlay={(deal: DealSchema) => <DealCard deal={deal} />}
|
|
||||||
renderContainerOverlay={(status: StatusSchema, children) => (
|
|
||||||
<StatusColumnWrapper
|
|
||||||
status={status}
|
|
||||||
isDragging>
|
|
||||||
{children}
|
|
||||||
</StatusColumnWrapper>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import React, { ReactNode, useEffect, useRef, useState } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { Box, Group, Text, TextInput } from "@mantine/core";
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
import StatusMenu from "@/app/deals/components/StatusColumnWrapper/StatusMenu";
|
import StatusMenu from "@/app/deals/components/StatusColumnWrapper/StatusMenu";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||||
import { StatusSchema } from "@/lib/client";
|
import { StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -16,46 +17,12 @@ const StatusColumnWrapper = ({
|
|||||||
isDragging = false,
|
isDragging = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { onUpdateStatus } = useStatusesContext();
|
const { onUpdateStatus } = useStatusesContext();
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [editValue, setEditValue] = useState(status.name);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSave = (value: string) => {
|
||||||
if (isEditing && inputRef.current) {
|
const newValue = value.trim();
|
||||||
inputRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [isEditing]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
isEditing &&
|
|
||||||
inputRef.current &&
|
|
||||||
!inputRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
handleSave();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () =>
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, [isEditing, editValue]);
|
|
||||||
|
|
||||||
const handleEdit = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
setEditValue(status.name);
|
|
||||||
setIsEditing(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
const newValue = editValue.trim();
|
|
||||||
if (newValue && newValue !== status.name) {
|
if (newValue && newValue !== status.name) {
|
||||||
onUpdateStatus(status.id, { name: newValue });
|
onUpdateStatus(status.id, { name: newValue });
|
||||||
}
|
}
|
||||||
setIsEditing(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -72,39 +39,31 @@ const StatusColumnWrapper = ({
|
|||||||
justify={"space-between"}
|
justify={"space-between"}
|
||||||
ml={"xs"}
|
ml={"xs"}
|
||||||
mb={"xs"}>
|
mb={"xs"}>
|
||||||
{isEditing ? (
|
<InPlaceInput
|
||||||
<TextInput
|
defaultValue={status.name}
|
||||||
ref={inputRef}
|
onComplete={value => handleSave(value)}
|
||||||
value={editValue}
|
inputStyles={{
|
||||||
onChange={e => setEditValue(e.target.value)}
|
input: {
|
||||||
onKeyDown={e => {
|
height: 25,
|
||||||
if (e.key === "Enter") handleSave();
|
minHeight: 25,
|
||||||
if (e.key === "Escape") {
|
},
|
||||||
setEditValue(status.name);
|
}}
|
||||||
setIsEditing(false);
|
getChildren={startEditing => (
|
||||||
}
|
<>
|
||||||
}}
|
<Text
|
||||||
variant="unstyled"
|
style={{
|
||||||
styles={{
|
cursor: "grab",
|
||||||
input: {
|
userSelect: "none",
|
||||||
height: 25,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
minHeight: 25,
|
}}>
|
||||||
},
|
{status.name}
|
||||||
}}
|
</Text>
|
||||||
/>
|
<StatusMenu
|
||||||
) : (
|
status={status}
|
||||||
<Text
|
handleEdit={startEditing}
|
||||||
style={{
|
/>
|
||||||
cursor: "grab",
|
</>
|
||||||
userSelect: "none",
|
)}
|
||||||
opacity: isDragging ? 0.5 : 1,
|
|
||||||
}}>
|
|
||||||
{status.name}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<StatusMenu
|
|
||||||
status={status}
|
|
||||||
handleEdit={handleEdit}
|
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { StatusSchema } from "@/lib/client";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: StatusSchema;
|
status: StatusSchema;
|
||||||
handleEdit: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
handleEdit: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusMenu: FC<Props> = ({ status, handleEdit }) => {
|
const StatusMenu: FC<Props> = ({ status, handleEdit }) => {
|
||||||
@ -28,7 +28,12 @@ const StatusMenu: FC<Props> = ({ status, handleEdit }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item onClick={handleEdit}>
|
<Menu.Item
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEdit();
|
||||||
|
}}>
|
||||||
<Group wrap={"nowrap"}>
|
<Group wrap={"nowrap"}>
|
||||||
<IconEdit />
|
<IconEdit />
|
||||||
<Text>Переименовать</Text>
|
<Text>Переименовать</Text>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
horizontalListSortingStrategy,
|
horizontalListSortingStrategy,
|
||||||
SortableContext,
|
SortableContext,
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { Group, ScrollArea } from "@mantine/core";
|
import { Group } from "@mantine/core";
|
||||||
import useDndSensors from "@/app/deals/hooks/useSensors";
|
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||||
import SortableItem from "@/components/dnd/SortableItem";
|
import SortableItem from "@/components/dnd/SortableItem";
|
||||||
import { BaseDraggable } from "@/components/dnd/types/types";
|
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||||
@ -59,67 +59,63 @@ const FunnelDnd = <
|
|||||||
const sensors = useDndSensors();
|
const sensors = useDndSensors();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea
|
<DndContext
|
||||||
offsetScrollbars="x"
|
sensors={sensors}
|
||||||
scrollbarSize="0.5rem">
|
collisionDetection={closestCorners}
|
||||||
<DndContext
|
onDragStart={onDragStart}
|
||||||
sensors={sensors}
|
onDragOver={onDragOver}
|
||||||
collisionDetection={closestCorners}
|
onDragEnd={onDragEnd}>
|
||||||
onDragStart={onDragStart}
|
<SortableContext
|
||||||
onDragOver={onDragOver}
|
items={containers.map(getContainerId)}
|
||||||
onDragEnd={onDragEnd}>
|
strategy={horizontalListSortingStrategy}>
|
||||||
<SortableContext
|
<Group
|
||||||
items={containers.map(getContainerId)}
|
gap="xs"
|
||||||
strategy={horizontalListSortingStrategy}>
|
wrap="nowrap"
|
||||||
<Group
|
align="start">
|
||||||
gap="xs"
|
{containers.map(container => {
|
||||||
wrap="nowrap"
|
const containerItems = getItemsByContainer(
|
||||||
align="start">
|
container,
|
||||||
{containers.map(container => {
|
items
|
||||||
const containerItems = getItemsByContainer(
|
);
|
||||||
container,
|
const containerId = getContainerId(container);
|
||||||
items
|
return (
|
||||||
);
|
<SortableItem
|
||||||
const containerId = getContainerId(container);
|
key={containerId}
|
||||||
return (
|
id={containerId}>
|
||||||
<SortableItem
|
{renderContainer(
|
||||||
key={containerId}
|
|
||||||
id={containerId}>
|
|
||||||
{renderContainer(
|
|
||||||
container,
|
|
||||||
<FunnelColumn
|
|
||||||
id={containerId}
|
|
||||||
items={containerItems}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SortableItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<FunnelOverlay
|
|
||||||
activeContainer={activeContainer}
|
|
||||||
activeItem={activeItem}
|
|
||||||
renderContainer={container => {
|
|
||||||
const containerItems = getItemsByContainer(
|
|
||||||
container,
|
|
||||||
items
|
|
||||||
);
|
|
||||||
const containerId = getContainerId(container);
|
|
||||||
return renderContainerOverlay(
|
|
||||||
container,
|
container,
|
||||||
<FunnelColumn
|
<FunnelColumn
|
||||||
id={containerId}
|
id={containerId}
|
||||||
items={containerItems}
|
items={containerItems}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
}}
|
</SortableItem>
|
||||||
renderItem={renderItemOverlay}
|
);
|
||||||
/>
|
})}
|
||||||
</Group>
|
<FunnelOverlay
|
||||||
</SortableContext>
|
activeContainer={activeContainer}
|
||||||
</DndContext>
|
activeItem={activeItem}
|
||||||
</ScrollArea>
|
renderContainer={container => {
|
||||||
|
const containerItems = getItemsByContainer(
|
||||||
|
container,
|
||||||
|
items
|
||||||
|
);
|
||||||
|
const containerId = getContainerId(container);
|
||||||
|
return renderContainerOverlay(
|
||||||
|
container,
|
||||||
|
<FunnelColumn
|
||||||
|
id={containerId}
|
||||||
|
items={containerItems}
|
||||||
|
renderItem={renderItem}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
renderItem={renderItemOverlay}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
85
src/components/ui/InPlaceInput/InPlaceInput.tsx
Normal file
85
src/components/ui/InPlaceInput/InPlaceInput.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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 InPlaceInput: FC<Props> = ({
|
||||||
|
onComplete,
|
||||||
|
placeholder,
|
||||||
|
inputStyles,
|
||||||
|
getChildren,
|
||||||
|
defaultValue = "",
|
||||||
|
}) => {
|
||||||
|
const [isWriting, setIsWriting] = useState<boolean>(false);
|
||||||
|
const [value, setValue] = useState<string>(defaultValue);
|
||||||
|
console.log(value);
|
||||||
|
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 => {
|
||||||
|
if (e.key === "Enter") onCompleteCreating();
|
||||||
|
if (e.key === "Escape") onCancelCreating();
|
||||||
|
}}
|
||||||
|
styles={inputStyles}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getChildren(onStartCreating);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InPlaceInput;
|
||||||
Reference in New Issue
Block a user