Files
labelimage/components/label/components/RightObjectTools.tsx
2026-04-15 17:13:28 +08:00

1078 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import {
ActionIcon,
Badge,
Box,
Checkbox,
Collapse,
Flex,
Group,
Paper,
Popover,
ScrollArea,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core"
import dayjs from "dayjs"
import {
ChevronDown,
ChevronUp,
CircleDot,
Eye,
EyeOff,
Info,
RectangleHorizontal,
Settings,
Spline,
} from "lucide-react"
import paper from "paper"
import { useCallback, useMemo, useState } from "react"
import { Project } from "../api/project/typing"
import { Task } from "../api/task/typing"
import { LabelState } from "../LabelNossr"
import { useKeyEventStore, useLabelStore, useObjectStore } from "../store"
import { useAllUserStore } from "../store/auth"
import { useBottomToolsStore } from "../useBottomToolsStore"
import { useOtherToolsStore } from "../useOtherToolsStore"
import { getShowContextMenuPosition, usePaperStore } from "../usePaperStore"
import { usePaperSupportStore } from "../usePaperSupportStore"
import { useTopToolsStore } from "../useTopToolsStore"
import { getSubAttrStatus } from "../util"
import { labelTypeMap } from "../utils/constants"
import { toggleObjectHideByShortcut } from "../utils/objectVisibility"
interface RightObjectToolsComponentProps {
labelState: LabelState
projectDetail?: Project.DetailResponse
taskDetail?: Task.DataProps
}
export const renderOperationIcon = (
operationSchema: Project.LabelSchemaList | null
) => {
if (operationSchema) {
let color = `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
const style = { color }
switch (operationSchema && labelTypeMap.get(operationSchema.label_type)) {
case "多边形": {
return <Settings size={20} style={style} />
}
case "关键点": {
return <CircleDot size={20} style={style} />
}
case "多线段": {
return <Spline size={20} style={style} />
}
case "2D框": {
return <RectangleHorizontal size={20} style={style} />
}
default:
return <Settings size={20} />
}
} else return <Settings size={20} />
}
const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
const { labelState, projectDetail, taskDetail } = props
const storeLabel = useLabelStore((state) => state.label)
const { activeImage } = useBottomToolsStore()
const {
operationStatus,
pathStatus,
selectedPath,
imageStatus,
updateImageStatus,
updateOperationStatus,
updatePathStatus,
updateSelectedPath,
} = useObjectStore()
const { userOpts } = useAllUserStore()
const [searchId, setSearchId] = useState("")
const [searchSubAttrs, setSearchSubAttrs] = useState<{
[x: string]: string
}>({})
const [checkBoxsChecked, setCheckBoxsChecked] = useState([
true,
true,
true,
true,
])
const getOperationSchema = useCallback(
(operationId: string) => {
return (
projectDetail?.label_schema_list?.find(
(item) => item.category_id === +operationId
) || null
)
},
[projectDetail?.label_schema_list]
)
const renderShortcutKey = (operationId: string) => {
if (+operationId > 10) {
return "ctrl+" + (+operationId % 10).toString()
} else {
return (+operationId % 10).toString()
}
}
const getCheckBoxColor = useCallback((type: number) => {
switch (type) {
case 3:
return "yellow"
case 2:
return "red"
case 1:
return "green"
case 0:
return "gray"
default:
return "blue"
}
}, [])
const { shift: pressShift } = useKeyEventStore()
const continuePolygonTarget = usePaperStore(
(state) => state.continuePolygonTarget
)
const { getItemById, getItemsById } = usePaperStore()
const continueObjectId =
continuePolygonTarget?.imageId === activeImage
? continuePolygonTarget.objectId
: null
const objectOperations = useTopToolsStore((state) => state.objectOperations)
const getOperationChildren = useCallback(
(imageId: string, operationId: string) => {
const operationChildren = storeLabel.get(imageId)?.get(operationId) || []
if (continueObjectId === null || imageId !== activeImage) {
return operationChildren
}
return operationChildren.filter(
([objectId]) => objectId !== continueObjectId
)
},
[activeImage, continueObjectId, storeLabel]
)
// 切换选中对象时,清除所有其他对象的当前选中状态
const clearSelectedItems = () => {
usePaperStore
.getState()
.paperScope?.project.selectedItems.forEach((item) => {
item.data.selected = false
})
// const group = usePaperStore.getState().group!;
// const needClear = [
// ...group.getItems({ data: { type: "mask" } }),
// ...group.getItems({ data: { type: "text" } }),
// ];
// needClear.forEach((item) => {
// item.remove();
// });
usePaperSupportStore.getState().clearAll()
}
const handleSelectedPath = (path: paper.Path) => {
console.log("handleSelectedPath", path)
if (path.data.type === "rectangle") {
path.data.selected = true
usePaperSupportStore.getState().handleBufferPaths(path)
usePaperSupportStore.getState().handleCircles(path)
path.fillColor = path.data.fillColor
} else if (path.data.type === "brush") {
path.data.selected = true
usePaperSupportStore.getState().handleMask(path)
usePaperSupportStore.getState().handleBufferPaths(path)
usePaperSupportStore.getState().handleCircles(path)
} else if (path.data.type === "point") {
path.data.selected = true
usePaperSupportStore.getState().handleMask(path)
usePaperSupportStore.getState().handleText(path)
usePaperSupportStore.getState().handleBufferPaths(path)
usePaperSupportStore.getState().handleCircles(path)
} else if (path.data.type === "polygon") {
path.fillColor = path.data.fillColor
}
path.bringToFront()
}
const cancelSelectedPathColor = (path: paper.Path | paper.CompoundPath) => {
if (path.data.type === "rectangle" || path.data.type === "polygon")
path.fillColor = path.data.blankColor
}
const handleSelect = useCallback(
(id: number, operationId: any) => {
if (selectedPath[activeImage]?.length) {
if (!selectedPath[activeImage]?.includes(id)) {
if (pressShift) {
if (getItemsById(id)?.length) {
getItemsById(id).forEach((item) => {
if (item instanceof paper.Path) {
handleSelectedPath(item)
} else if (item instanceof paper.CompoundPath) {
if (item.data.type === "polygon")
item.fillColor = item.data.fillColor
else item.data.selected = true
}
})
let category = useTopToolsStore
.getState()
.objectOperations.find(
(item) => item.category_id.toString() === operationId
)
const globalPosition = getItemById(id)!.globalMatrix.transform(
getItemById(id)!.position
)
const { top, left } = getShowContextMenuPosition(
globalPosition,
category!
)
useOtherToolsStore.getState().setShowContextMenu({
showContextMenu: true,
position: { top, left },
imageId: activeImage,
operationId: operationId,
id,
})
}
} else {
clearSelectedItems()
selectedPath[activeImage].forEach((selectedId) => {
if (getItemsById(selectedId)?.length) {
getItemsById(selectedId).forEach((item) => {
cancelSelectedPathColor(item)
})
useOtherToolsStore.getState().setShowContextMenu({
showContextMenu: false,
position: {
top: 0,
left: 0,
},
imageId: activeImage,
operationId: operationId,
id,
})
}
})
if (getItemsById(id)?.length) {
getItemsById(id).forEach((item) => {
if (item instanceof paper.Path) {
handleSelectedPath(item)
} else if (item instanceof paper.CompoundPath) {
if (item.data.type === "polygon")
item.fillColor = item.data.fillColor
else item.data.selected = true
}
})
let category = useTopToolsStore
.getState()
.objectOperations.find(
(item) => item.category_id.toString() === operationId
)
const globalPosition = getItemById(id)!.globalMatrix.transform(
getItemById(id)!.position
)
const { top, left } = getShowContextMenuPosition(
globalPosition,
category!
)
useOtherToolsStore.getState().setShowContextMenu({
showContextMenu: true,
position: { top, left },
imageId: activeImage,
operationId: operationId,
id,
})
}
}
updateSelectedPath(activeImage, "ADD", id)
} else {
clearSelectedItems()
updateSelectedPath(activeImage, "DELETE", id)
if (getItemsById(id)?.length) {
getItemsById(id).forEach((item) => {
cancelSelectedPathColor(item)
})
useOtherToolsStore.getState().setShowContextMenu({
showContextMenu: false,
position: {
top: 0,
left: 0,
},
imageId: activeImage,
operationId: operationId,
id,
})
}
}
} else {
clearSelectedItems()
updateSelectedPath(activeImage, "ADD", id)
if (getItemsById(id)?.length) {
getItemsById(id).forEach((item) => {
if (item instanceof paper.Path) {
handleSelectedPath(item)
} else if (item instanceof paper.CompoundPath) {
if (item.data.type === "polygon")
item.fillColor = item.data.fillColor
else item.data.selected = true
}
// if (item.data.type === "rectangle") {
// item.data.selected = true;
// usePaperSupportStore.getState().handleCircles(item);
// }
// item.fillColor = item.data?.fillColor;
// item.bringToFront();
})
let category = useTopToolsStore
.getState()
.objectOperations.find(
(item) => item.category_id.toString() === operationId
)
const globalPosition = getItemById(id)!.globalMatrix.transform(
getItemById(id)!.position
)
const { top, left } = getShowContextMenuPosition(
globalPosition,
category!
)
useOtherToolsStore.getState().setShowContextMenu({
showContextMenu: true,
position: { top, left },
imageId: activeImage,
operationId: operationId,
id,
})
}
}
},
[
activeImage,
getItemById,
getItemsById,
pressShift,
selectedPath,
updateSelectedPath,
]
)
// 根据img operationId 更新操作栏operation状态
const checkIfOperationNeedsUpdate = (imgId: string, operationId: any) => {
const operationMap = storeLabel.get(imgId)
if (operationMap) {
const objList = operationMap.get(operationId)
if (objList && objList.length) {
const hideFlag = objList.every(([id]) => {
return useObjectStore.getState().pathStatus[`${imgId}${id}`]?.["HIDE"]
? true
: false
})
updateOperationStatus(imgId + operationId, "HIDE", hideFlag)
}
}
}
// 隐藏时清空当前选中的item选中状态
const setSelectedItemFalse = (id: number) => {
if (!id) return
const sameIdItems = usePaperStore.getState().getItemsById(id)
sameIdItems.forEach((i) => {
if (i.data.type === "mask" || i.data.type === "text") {
i.remove()
}
if (i.data.selected) i.data.selected = false
})
}
const operationHide = useCallback(
(operationId: string, hide: boolean) => {
updateOperationStatus(activeImage + operationId, "HIDE", hide)
getOperationChildren(activeImage, operationId).map((child) => {
toggleObjectHideByShortcut(activeImage, operationId, child[0], hide)
})
},
[activeImage, getOperationChildren, updateOperationStatus]
)
const objectHide = useCallback(
(operationId: string, objectId: number, hide: boolean) => {
toggleObjectHideByShortcut(activeImage, operationId, objectId, hide)
},
[activeImage]
)
const activeImageOperations = useMemo(() => {
const schemaOperationIds = objectOperations.map((item) =>
item.category_id.toString()
)
const imageOperationIds = labelState.get(activeImage) || []
return Array.from(new Set([...schemaOperationIds, ...imageOperationIds]))
}, [activeImage, labelState, objectOperations])
const isObjectHidden = (imageId: string, objectId: number) => {
return pathStatus[imageId + objectId]?.["HIDE"] ?? false
}
const isOperationHidden = (imageId: string, operationId: string) => {
const operationChildren = getOperationChildren(imageId, operationId)
return (
operationChildren.length > 0 &&
operationChildren.every(([objectId]) => isObjectHidden(imageId, objectId))
)
}
const totalObjectCount = activeImageOperations.reduce(
(count, operationId) => {
return count + getOperationChildren(activeImage, operationId).length
},
0
)
const areAllObjectsHidden =
totalObjectCount > 0 &&
activeImageOperations.every((operationId) => {
const operationChildren = getOperationChildren(activeImage, operationId)
return operationChildren.every(([objectId]) =>
isObjectHidden(activeImage, objectId)
)
})
// const renderObjectName = useCallback((name: string, input: string) => {
// const index = name?.indexOf(input);
// const beforeStr = name?.substring(0, index);
// const afterStr = name?.slice(index + input.length);
// return (
// <span>
// {beforeStr}
// <span className="text-primary">{input}</span>
// {afterStr}
// </span>
// );
// }, []);
// child 是否存在不符合要求的子属性
const getOperationSubAttrStatus = useCallback(
(operationId: string) => {
let bool = storeLabel
.get(activeImage)
?.get(operationId)
?.some((child) => {
if (continueObjectId !== null && child[0] === continueObjectId) {
return false
}
return getSubAttrStatus(
getOperationSchema(operationId),
child[2]?.sub_attributes
)
})
return bool
},
[activeImage, continueObjectId, getOperationSchema, storeLabel]
)
// 搜索标注对象id
const handleSelectedBySearchId = (e: any) => {
if (e.key === "Enter") {
if (searchId && !isNaN(Number(searchId))) {
const id = Number(searchId)
const labelObjMap = storeLabel.get(activeImage)
Array.from(labelObjMap!.entries()).forEach(([operationId, objList]) => {
false && objList
getOperationChildren(activeImage, operationId).forEach((obj) => {
if (id === obj[0]) {
handleSelect(id, operationId)
}
})
})
}
}
}
// 搜索标注对象子属性
const handleSelectedBySearchSubAttr = (e: any, currentOperationId: any) => {
if (e.key === "Enter") {
Array.from(storeLabel.entries()).forEach(([imgId, objectMap]) => {
if (objectMap.get(currentOperationId)) {
const list = getOperationChildren(imgId, currentOperationId)
list!.forEach((item) => {
const { sub_attributes } = item[2]
// 如果没有子属性搜索值则把对象HIDE设为false
if (!searchSubAttrs[currentOperationId]) {
updatePathStatus(imgId + item[0], "HIDE", false)
updateOperationStatus(imgId + currentOperationId, "HIDE", false)
if (getItemsById(item[0]))
getItemsById(item[0]).forEach((i) => {
i.visible = true
})
return
}
if (sub_attributes && Object.entries(sub_attributes).length) {
let flag = true
Object.entries(sub_attributes).forEach(([_k, value]) => {
if (searchSubAttrs[currentOperationId] === value) {
flag = false
}
})
if (flag) {
updatePathStatus(imgId + item[0], "HIDE", true)
if (getItemsById(item[0])) {
getItemsById(item[0]).forEach((i) => {
i.visible = false
})
setSelectedItemFalse(item[0])
}
} else {
updatePathStatus(imgId + item[0], "HIDE", false)
if (getItemsById(item[0]))
getItemsById(item[0]).forEach((i) => {
i.visible = true
})
}
} else {
// 如果有子属性搜索值但对象无子属性则把对象HIDE设为true
updatePathStatus(imgId + item[0], "HIDE", true)
if (getItemsById(item[0])) {
getItemsById(item[0]).forEach((i) => {
i.visible = false
})
setSelectedItemFalse(item[0])
}
}
})
checkIfOperationNeedsUpdate(imgId, currentOperationId)
}
})
}
}
return (
<Box h="100%" w="100%">
<Flex direction="column" h="100%" gap="xs">
<Flex
justify="space-between"
align="center"
wrap="nowrap"
gap={6}
px="md"
py="xs"
style={{ minWidth: 0 }}>
<Group gap={6} wrap="nowrap" style={{ minWidth: 0, flexShrink: 0 }}>
<ActionIcon
size="sm"
variant="subtle"
aria-label={areAllObjectsHidden ? "显示全部对象" : "隐藏全部对象"}
onClick={() => {
if (!totalObjectCount) return
activeImageOperations.forEach((operationId) => {
operationHide(operationId, !areAllObjectsHidden)
})
}}>
{areAllObjectsHidden ? <EyeOff size={16} /> : <Eye size={16} />}
</ActionIcon>
<Title
order={5}
size="h5"
style={{ whiteSpace: "nowrap", lineHeight: 1.1 }}>
</Title>
</Group>
<Group gap={6} wrap="nowrap" style={{ minWidth: 0, flexShrink: 0 }}>
<TextInput
w={72}
size="xs"
placeholder="输入ID"
value={searchId}
onChange={(e) => setSearchId(e.target.value)}
onKeyDown={handleSelectedBySearchId}
/>
<Group gap={2} wrap="nowrap">
{[3, 2, 1, 0].map((type, index) => (
<Checkbox
key={type}
id={type + ""}
color={getCheckBoxColor(type)}
checked={checkBoxsChecked[index]}
onChange={(e) => {
setCheckBoxsChecked((pre) => {
const arr = [...pre]
arr[index] = e.target.checked
return arr
})
}}
size="xs"
/>
))}
</Group>
<ActionIcon
size="sm"
variant="subtle"
onClick={() => {
updateImageStatus(
activeImage,
"COLLAPSE",
!imageStatus[activeImage]?.["COLLAPSE"]
)
}}>
{!imageStatus[activeImage]?.["COLLAPSE"] ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</ActionIcon>
</Group>
</Flex>
<Collapse
in={!imageStatus[activeImage]?.["COLLAPSE"]}
style={{
flex: 1,
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}>
<ScrollArea style={{ flex: 1 }} px="md">
<Stack gap="xs" pb="md">
{activeImageOperations.map((operationId) => {
const operationSchema = getOperationSchema(operationId)
const isCollapsed =
operationStatus[activeImage + operationId]?.["COLLAPSE"]
const isHide = isOperationHidden(activeImage, operationId)
const hasSubAttrError = getOperationSubAttrStatus(operationId)
const operationLabel = operationSchema?.label_class
const operationChildren = getOperationChildren(
activeImage,
operationId
)
const objectCount = operationChildren.length
const visibleChildren = operationChildren.filter((child) => {
const { comment } = child[2]
let visibleFlag = true
if (comment && comment.length) {
const latest_comment =
(taskDetail && taskDetail.label_status === 2) ||
comment.length === 1
? comment[comment.length - 1]
: comment[comment.length - 2]
let type = latest_comment.comment_type
if (type === 2) {
const { last_modified_timestamp } = child[2]
if (
last_modified_timestamp &&
last_modified_timestamp > latest_comment.date
)
type = 3
}
visibleFlag = checkBoxsChecked[3 - type]
}
if (checkBoxsChecked.every((value) => !value))
visibleFlag = true
return visibleFlag
})
return (
<Box key={operationId} w="100%">
<Paper withBorder p="xs" radius="md" shadow="sm">
<Flex align="center" gap="xs" style={{ minWidth: 0 }}>
<Flex
align="center"
gap="xs"
style={{ flex: 1, minWidth: 0 }}>
<ActionIcon
variant="subtle"
size="sm"
onClick={() => operationHide(operationId, !isHide)}>
{!isHide ? <Eye size={16} /> : <EyeOff size={16} />}
</ActionIcon>
<Group
gap={8}
wrap="nowrap"
style={{ flex: 1, minWidth: 0 }}>
<Group
gap={6}
wrap="nowrap"
px="xs"
py={4}
style={{
flexShrink: 0,
borderRadius: 999,
border: `1px solid ${
hasSubAttrError
? "var(--mantine-color-red-2)"
: "var(--mantine-color-gray-2)"
}`,
background: hasSubAttrError
? "var(--mantine-color-red-0)"
: "var(--mantine-color-gray-0)",
}}>
{renderOperationIcon(operationSchema)}
<Text
size="xs"
fw={700}
c={hasSubAttrError ? "red" : "dimmed"}
style={{
whiteSpace: "nowrap",
lineHeight: 1,
}}>
{renderShortcutKey(operationId)}
</Text>
</Group>
<Box style={{ flex: 1, minWidth: 0 }}>
<Text
size="sm"
fw={500}
c={hasSubAttrError ? "red" : undefined}
title={operationLabel || undefined}
style={{
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}>
{operationLabel}
</Text>
</Box>
</Group>
</Flex>
<Group
gap={6}
wrap="nowrap"
style={{ flexShrink: 0, marginLeft: "auto" }}>
<TextInput
w={88}
size="xs"
placeholder="搜索子属性"
value={searchSubAttrs[operationId] || ""}
onChange={(e) => {
setSearchSubAttrs((searchValues) => ({
...searchValues,
[operationId]: e.target.value,
}))
}}
onKeyDown={(e) =>
handleSelectedBySearchSubAttr(e, operationId)
}
/>
<Badge
size="sm"
variant="light"
radius="xl"
color={hasSubAttrError ? "red" : "gray"}>
{objectCount}
</Badge>
<ActionIcon
variant="subtle"
size="sm"
onClick={() => {
updateOperationStatus(
activeImage + operationId,
"COLLAPSE",
!isCollapsed
)
}}>
{!isCollapsed ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</ActionIcon>
</Group>
</Flex>
</Paper>
<Collapse in={!isCollapsed}>
<Stack gap="xs" mt="xs" ml="xs">
{visibleChildren.map((child, childIndex) => {
const { comment } = child[2]
const isSelected = selectedPath[
activeImage
]?.includes(child[0])
const isChildHide =
pathStatus[activeImage + child[0]]?.["HIDE"]
const hasChildError = getSubAttrStatus(
getOperationSchema(operationId),
child[2]?.sub_attributes
)
const isLastVisibleChild =
childIndex === visibleChildren.length - 1
let textColor = undefined
let connectorColor = "var(--mantine-color-gray-4)"
if (hasChildError) {
textColor = "red"
connectorColor = "var(--mantine-color-red-3)"
} else if (isSelected) {
textColor = "white"
connectorColor =
"var(--mantine-primary-color-filled)"
}
return (
<Box
key={child[0]}
pl="lg"
style={{ position: "relative" }}>
<Box
style={{
position: "absolute",
left: 7,
top: 0,
bottom: isLastVisibleChild ? "75%" : 0,
width: 1.5,
borderRadius: 999,
background: connectorColor,
opacity: isSelected ? 0.9 : 0.35,
}}
/>
{isLastVisibleChild ? (
<Box
style={{
position: "absolute",
left: 7,
top: "calc(50% - 10px)",
width: 14,
height: 10,
borderLeft: `1.5px solid ${connectorColor}`,
borderBottom: `1.5px solid ${connectorColor}`,
borderBottomLeftRadius: 10,
opacity: isSelected ? 0.9 : 0.55,
}}
/>
) : (
<>
<Box
style={{
position: "absolute",
left: 7,
top: "50%",
width: 14,
borderTop: `1.5px solid ${connectorColor}`,
opacity: isSelected ? 0.9 : 0.45,
}}
/>
<Box
style={{
position: "absolute",
left: 4,
top: "calc(50% - 3px)",
width: 6,
height: 6,
borderRadius: 999,
background: connectorColor,
boxShadow:
"0 0 0 2px var(--mantine-color-body)",
opacity: isSelected ? 1 : 0.85,
}}
/>
</>
)}
<Paper
withBorder
p="xs"
radius="md"
shadow="sm"
bg={
isSelected
? "var(--mantine-primary-color-filled)"
: "var(--mantine-color-gray-0)"
}
c={isSelected ? "white" : undefined}
onClick={() =>
handleSelect(child[0], operationId)
}
style={{ cursor: "pointer" }}>
<Flex justify="space-between" align="center">
<Group gap="xs">
<ActionIcon
variant="subtle"
size="sm"
c={isSelected ? "white" : undefined}
onClick={(e) => {
e.stopPropagation()
objectHide(
operationId,
child[0],
!isChildHide
)
}}>
{!isChildHide ? (
<Eye size={16} />
) : (
<EyeOff size={16} />
)}
</ActionIcon>
<Text size="sm" c={textColor}>
{child[0]?.toString()}
</Text>
<Tooltip
label={
<Stack gap={0}>
<Group gap={4}>
<Text size="xs"></Text>
<Text size="xs">
{
userOpts.find(
(item) =>
item.value === child[2]?.uid
)?.label
}
</Text>
<Text size="xs">
{dayjs(
(child[2]?.create_timestamp ||
0) * 1000
).format("YYYY-MM-DD HH:mm:ss")}
</Text>
</Group>
{child[2]?.first_modified_uid && (
<Group gap={4}>
<Text size="xs"></Text>
<Text size="xs">
{
userOpts.find(
(item) =>
item.value ===
child[2]
?.first_modified_uid
)?.label
}
</Text>
<Text size="xs">
{dayjs(
(child[2]
?.first_modified_timestamp ||
0) * 1000
).format("YYYY-MM-DD HH:mm:ss")}
</Text>
</Group>
)}
{child[2]?.last_modified_uid && (
<Group gap={4}>
<Text size="xs"></Text>
<Text size="xs">
{
userOpts.find(
(item) =>
item.value ===
child[2]
?.last_modified_uid
)?.label
}
</Text>
<Text size="xs">
{dayjs(
(child[2]
?.last_modified_timestamp ||
0) * 1000
).format("YYYY-MM-DD HH:mm:ss")}
</Text>
</Group>
)}
</Stack>
}
multiline
bg="white"
c="black"
withArrow>
<Info
size={16}
style={{ cursor: "pointer" }}
color={isSelected ? "white" : "black"}
/>
</Tooltip>
</Group>
{comment ? (
<Group gap={4}>
{comment.map((c: any) => {
const count =
c.check_box_comments.length +
c.text_comments.length
const { last_modified_timestamp } =
child[2]
let flag = false
if (
count > 0 &&
last_modified_timestamp &&
last_modified_timestamp > c.date
)
flag = true
let badgeColor = "gray"
if (c.comment_type === 1)
badgeColor = "green"
if (c.comment_type === 2)
badgeColor = "red"
if (flag) badgeColor = "yellow"
return (
<Popover
key={`${child[0]}${c.turn}${c.times}`}
position="left"
withArrow>
<Popover.Target>
<Badge
size="xs"
circle
color={badgeColor}
style={{
cursor: "pointer",
}}>
{count}
</Badge>
</Popover.Target>
<Popover.Dropdown>
<Text size="xs" c={badgeColor}>
{`P${child[2].image_id}${
child[2].uid
}${c.check_box_comments.join(
","
)},${c.text_comments.join(
","
)} ${
c.date
? dayjs(
c.date * 1000
).format(
"YYYY-MM-DD HH:mm:ss"
)
: ""
}`}
</Text>
</Popover.Dropdown>
</Popover>
)
})}
</Group>
) : null}
</Flex>
</Paper>
</Box>
)
})}
</Stack>
</Collapse>
</Box>
)
})}
</Stack>
</ScrollArea>
</Collapse>
</Flex>
</Box>
)
}
export default RightObjectTools