feat: added tags for cards, aligned status headers
This commit is contained in:
@@ -7,6 +7,7 @@ import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import isModuleInProject, { Modules } from "../../../../modules/utils/isModuleInProject.ts";
|
||||
import CardTags from "../CardTags/CardTags.tsx";
|
||||
|
||||
type Props = {
|
||||
cards: CardSummary[];
|
||||
@@ -75,6 +76,7 @@ export const CardGroupView: FC<Props> = ({ cards, group }) => {
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
<CardTags groupId={group.id} tags={cards[0].tags}/>
|
||||
{isServicesAndProductsIncluded && (
|
||||
<Flex
|
||||
p={rem(10)}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useContextMenu } from "mantine-contextmenu";
|
||||
import useCardSummaryState from "./useCardSummaryState.tsx";
|
||||
import isModuleInProject, { Modules } from "../../../../modules/utils/isModuleInProject.ts";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import CardTags from "../CardTags/CardTags.tsx";
|
||||
|
||||
type Props = {
|
||||
cardSummary: CardSummary;
|
||||
@@ -26,7 +27,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, selectedProject);
|
||||
const isClientIncluded = isModuleInProject(Modules.CLIENTS, selectedProject);
|
||||
|
||||
const onDealSummaryClick = () => {
|
||||
const onCardSummaryClick = () => {
|
||||
CardService.getCardById({ cardId: cardSummary.id }).then(card => {
|
||||
setSelectedCard(card);
|
||||
});
|
||||
@@ -59,7 +60,7 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
icon: <IconTrash />,
|
||||
},
|
||||
])}
|
||||
onClick={() => onDealSummaryClick()}
|
||||
onClick={() => onCardSummaryClick()}
|
||||
className={styles["container"]}
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
@@ -95,6 +96,9 @@ const CardSummaryItem: FC<Props> = ({ cardSummary, color }) => {
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{!cardSummary.group?.id && (
|
||||
<CardTags cardId={cardSummary.id} tags={cardSummary.tags}/>
|
||||
)}
|
||||
<Flex align={"center"} justify={"space-between"}>
|
||||
<Flex align={"center"} gap={rem(5)}>
|
||||
<CopyButton value={cardSummary.id.toString()}>
|
||||
|
||||
21
src/components/Dnd/Cards/CardTags/CardTags.module.css
Normal file
21
src/components/Dnd/Cards/CardTags/CardTags.module.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.add-tag-button {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
border: 1px gray dashed;
|
||||
border-radius: 50%;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-tag-button:hover {
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.add-tag-button-icon {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.add-tag-button-icon:hover {
|
||||
color: white;
|
||||
}
|
||||
98
src/components/Dnd/Cards/CardTags/CardTags.tsx
Normal file
98
src/components/Dnd/Cards/CardTags/CardTags.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Center, Checkbox, Group, Menu, Pill, rem, Stack, Text } from "@mantine/core";
|
||||
import { CardTagSchema, CardTagService } from "../../../../client";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import styles from "./CardTags.module.css";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { useProjectsContext } from "../../../../contexts/ProjectsContext.tsx";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { useCardPageContext } from "../../../../pages/CardsPage/contexts/CardPageContext.tsx";
|
||||
|
||||
type Props = {
|
||||
cardId?: number;
|
||||
groupId?: number;
|
||||
tags: CardTagSchema[];
|
||||
}
|
||||
|
||||
const CardTags = ({ tags, cardId, groupId }: Props) => {
|
||||
const { selectedProject } = useProjectsContext();
|
||||
const { refetchCards } = useCardPageContext();
|
||||
|
||||
if (selectedProject?.tags.length === 0) return;
|
||||
|
||||
const onTagClick = (tag: CardTagSchema, event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
|
||||
event.stopPropagation();
|
||||
|
||||
CardTagService.switchTag({
|
||||
requestBody: {
|
||||
cardId: cardId ?? null,
|
||||
groupId: groupId ?? null,
|
||||
tagId: tag.id,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) notifications.error({ message });
|
||||
refetchCards();
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const addTagButton = (
|
||||
<Menu withArrow>
|
||||
<Menu.Target>
|
||||
<button
|
||||
onClick={e => e.stopPropagation()}
|
||||
className={classNames(styles["add-tag-button"])}
|
||||
>
|
||||
<Center>
|
||||
<IconPlus
|
||||
size={"1.2em"}
|
||||
className={classNames(styles["add-tag-button-icon"])}
|
||||
/>
|
||||
</Center>
|
||||
</button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Stack
|
||||
p={"xs"}
|
||||
gap={"sm"}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{selectedProject?.tags.map(tag => (
|
||||
<Group key={tag.id} wrap={"nowrap"}>
|
||||
<Checkbox
|
||||
checked={!!tags.find(cardTag => cardTag.id === tag.id)}
|
||||
onClick={(event) => onTagClick(tag, event)}
|
||||
/>
|
||||
<Text>{tag.name}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const pills = tags.map(tag => {
|
||||
return (
|
||||
<Pill
|
||||
key={tag.id}
|
||||
style={{
|
||||
color: "gray",
|
||||
backgroundColor: "var(--mantine-color-dark-6)",
|
||||
border: "1px solid gray",
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
</Pill>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Group gap={rem(4)}>
|
||||
{addTagButton}
|
||||
{pills}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardTags;
|
||||
@@ -1,5 +1,5 @@
|
||||
import styles from "../../../../pages/CardsPage/ui/CardsPage.module.css";
|
||||
import { Divider, Text, Title } from "@mantine/core";
|
||||
import { Divider, Stack, Text, Title } from "@mantine/core";
|
||||
import getColumnColor from "../../Cards/CardsDndColumn/utils/getColumnColor.ts";
|
||||
import { BoardSchema, CardSummary, StatusSchema } from "../../../../client";
|
||||
import { getPluralForm } from "../../../../shared/lib/utils.ts";
|
||||
@@ -10,6 +10,7 @@ import { useContextMenu } from "mantine-contextmenu";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import useStatus from "./hooks/useStatus.tsx";
|
||||
import isModuleInProject, { Modules } from "../../../../modules/utils/isModuleInProject.ts";
|
||||
import { useEqualHeightsContext } from "./contexts/EqualHeightContext.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
@@ -25,6 +26,10 @@ const Status = ({ summaries, status, board, dragState, index, refetch }: Props)
|
||||
const isDropDisabled = dragState === DragState.DRAG_CARD;
|
||||
const isServicesAndProductsIncluded = isModuleInProject(Modules.SERVICES_AND_PRODUCTS, board?.project);
|
||||
|
||||
const {
|
||||
divRefs,
|
||||
} = useEqualHeightsContext();
|
||||
|
||||
const {
|
||||
onEditStatusClick,
|
||||
onDeleteStatusClick,
|
||||
@@ -60,50 +65,63 @@ const Status = ({ summaries, status, board, dragState, index, refetch }: Props)
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<Droppable
|
||||
isDropDisabled={isDropDisabled}
|
||||
droppableId={"status-" + status.id.toString()}
|
||||
direction={"horizontal"}
|
||||
>
|
||||
{provided => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<Draggable
|
||||
draggableId={"status-" + status.id.toString()}
|
||||
index={status.ordinalNumber}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
const header = (
|
||||
<Stack h={"100%"} gap={0} justify={"space-between"}>
|
||||
<Stack gap={0}>
|
||||
<Title
|
||||
style={{ textAlign: "center" }}
|
||||
size={"h4"}>
|
||||
{status.name}
|
||||
</Title>
|
||||
<Text style={{ textAlign: "center", textWrap: "nowrap" }}>
|
||||
{getDealsText()}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Divider
|
||||
size={"xl"}
|
||||
color={getColumnColor(index)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
className={styles["header-statuses"]}
|
||||
onContextMenu={contextMenu()}
|
||||
>
|
||||
<Title
|
||||
style={{ textAlign: "center" }}
|
||||
size={"h4"}>
|
||||
{status.name}
|
||||
</Title>
|
||||
<Text style={{ textAlign: "center", textWrap: "nowrap" }}>
|
||||
{getDealsText()}
|
||||
</Text>
|
||||
<Divider
|
||||
size={"xl"}
|
||||
my={10}
|
||||
color={getColumnColor(index)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
ref={el => (divRefs.current[index] = el)}
|
||||
>
|
||||
<Droppable
|
||||
isDropDisabled={isDropDisabled}
|
||||
droppableId={"status-" + status.id.toString()}
|
||||
direction={"horizontal"}
|
||||
>
|
||||
{provided => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Draggable
|
||||
draggableId={"status-" + status.id.toString()}
|
||||
index={status.ordinalNumber}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
|
||||
className={styles["header-statuses"]}
|
||||
onContextMenu={contextMenu()}
|
||||
>
|
||||
{header}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { createContext, FC, useContext, useEffect, useRef, useState } from "react";
|
||||
import { StatusSchema } from "../../../../../client";
|
||||
|
||||
type EqualHeightsContextState = {
|
||||
divRefs: React.MutableRefObject<(HTMLDivElement | null)[]>;
|
||||
};
|
||||
|
||||
const EqualHeightsContext = createContext<EqualHeightsContextState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
type EqualHeightsContextProps = {
|
||||
statuses: StatusSchema[];
|
||||
}
|
||||
|
||||
const useEqualHeightsContextState = ({ statuses }: EqualHeightsContextProps) => {
|
||||
const divRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
const updateHeights = () => {
|
||||
divRefs.current.forEach(div => {
|
||||
if (div) {
|
||||
div.style.minHeight = "auto";
|
||||
div.style.height = "auto";
|
||||
}
|
||||
});
|
||||
|
||||
const heights = divRefs.current.map(div => div?.offsetHeight || 0);
|
||||
const newMaxHeight = Math.max(...heights);
|
||||
|
||||
divRefs.current.forEach(div => {
|
||||
if (div) {
|
||||
div.style.minHeight = newMaxHeight + "px";
|
||||
div.style.height = newMaxHeight + "px";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowWidth(window.innerWidth);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updateHeights();
|
||||
}, [windowWidth, divRefs.current.length, statuses]);
|
||||
|
||||
return {
|
||||
divRefs,
|
||||
};
|
||||
};
|
||||
|
||||
type EqualHeightsContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
} & EqualHeightsContextProps;
|
||||
|
||||
export const EqualHeightsContextProvider: FC<EqualHeightsContextProviderProps> = ({ children, statuses }) => {
|
||||
const state = useEqualHeightsContextState({ statuses });
|
||||
|
||||
return (
|
||||
<EqualHeightsContext.Provider value={state}>
|
||||
{children}
|
||||
</EqualHeightsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useEqualHeightsContext = () => {
|
||||
const context = useContext(EqualHeightsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useEqualHeightsContext must be used within a EqualHeightsContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { Flex, rem, Stack } from "@mantine/core";
|
||||
import { DragDropContext } from "@hello-pangea/dnd";
|
||||
import useDnd from "../../../../pages/CardsPage/hooks/useDnd.tsx";
|
||||
import Status from "../Status/Status.tsx";
|
||||
import { EqualHeightsContextProvider } from "../Status/contexts/EqualHeightContext.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
@@ -67,19 +68,21 @@ const Statuses = ({
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<Flex
|
||||
justify={"space-between"}
|
||||
direction={"column"}
|
||||
>
|
||||
<Flex className={styles["statuses"]}>
|
||||
{selectedBoard &&
|
||||
statuses.map(((status: StatusSchema, index: number) => {
|
||||
return statusDndColumn(status, index);
|
||||
}))
|
||||
}
|
||||
<EqualHeightsContextProvider statuses={statuses}>
|
||||
<Flex
|
||||
justify={"space-between"}
|
||||
direction={"column"}
|
||||
>
|
||||
<Flex className={styles["statuses"]}>
|
||||
{selectedBoard &&
|
||||
statuses.map(((status: StatusSchema, index: number) => {
|
||||
return statusDndColumn(status, index);
|
||||
}))
|
||||
}
|
||||
</Flex>
|
||||
<CardsDndFooter dragState={dragState} />
|
||||
</Flex>
|
||||
<CardsDndFooter dragState={dragState} />
|
||||
</Flex>
|
||||
</EqualHeightsContextProvider>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
26
src/components/Selects/CardTagSelect/CardTagSelect.tsx
Normal file
26
src/components/Selects/CardTagSelect/CardTagSelect.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { FC } from "react";
|
||||
import { CardTagSchema, ProjectSchema } from "../../../client";
|
||||
import ObjectSelect, { ObjectSelectProps } from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
|
||||
type OtherProps = {
|
||||
project: ProjectSchema | null;
|
||||
}
|
||||
|
||||
type SelectProps = Omit<ObjectSelectProps<CardTagSchema | null>, "data" | "getLabelFn" | "getValueFn">;
|
||||
|
||||
type Props = OtherProps & SelectProps;
|
||||
|
||||
const CardTagSelect: FC<Props> = ({ project, ...props }) => {
|
||||
const onClear = () => props.onChange(null);
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={project?.tags ?? []}
|
||||
searchable
|
||||
placeholder={"Выберите тег"}
|
||||
onClear={onClear}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default CardTagSelect;
|
||||
Reference in New Issue
Block a user