373 lines
10 KiB
TypeScript
373 lines
10 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
Avatar,
|
|
Box,
|
|
CloseButton,
|
|
Flex,
|
|
Group,
|
|
Input,
|
|
Menu,
|
|
Tabs,
|
|
Text,
|
|
Tooltip,
|
|
UnstyledButton,
|
|
} from "@mantine/core";
|
|
import {
|
|
DndContext,
|
|
useSensor,
|
|
useSensors,
|
|
PointerSensor,
|
|
closestCenter,
|
|
type DragEndEvent,
|
|
} from "@dnd-kit/core";
|
|
import {
|
|
arrayMove,
|
|
SortableContext,
|
|
useSortable,
|
|
horizontalListSortingStrategy,
|
|
} from "@dnd-kit/sortable";
|
|
import { CSS } from "@dnd-kit/utilities";
|
|
import { useEffect, useMemo, useState, type JSX } from "react";
|
|
import {
|
|
IconChevronRight,
|
|
IconEdit,
|
|
IconLogout,
|
|
IconSettingsPlus,
|
|
IconUsersGroup,
|
|
} from "@tabler/icons-react";
|
|
import classes from "./Component.module.css";
|
|
import type { TStation, TUser } from "../untils/types";
|
|
import type { Socket } from "socket.io-client";
|
|
|
|
interface DraggableTabsProps {
|
|
tabsData: TStation[];
|
|
panels: JSX.Element[];
|
|
storageKey?: string;
|
|
onChange: (activeTabId: string | null) => void;
|
|
w?: string | number;
|
|
isStationSettings?: boolean;
|
|
socket: Socket | null;
|
|
usersConnecting: TUser[];
|
|
setIsEditStation: (value: React.SetStateAction<boolean>) => void;
|
|
setIsOpenAddStation: (value: React.SetStateAction<boolean>) => void;
|
|
setStationEdit: (value: React.SetStateAction<TStation | undefined>) => void;
|
|
active: string;
|
|
setActive: (value: React.SetStateAction<string>) => void;
|
|
onSendCommand: (value: string) => void;
|
|
}
|
|
|
|
function SortableTab({
|
|
tab,
|
|
active,
|
|
onChange,
|
|
}: {
|
|
tab: TStation;
|
|
active: string | null;
|
|
onChange: (id: string) => void;
|
|
isStationSettings?: boolean;
|
|
}) {
|
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
|
useSortable({ id: tab.id.toString() });
|
|
|
|
return (
|
|
<Tabs.Tab
|
|
className={classes.tab}
|
|
ref={setNodeRef}
|
|
{...attributes}
|
|
{...listeners}
|
|
onPointerDown={(e) => {
|
|
listeners?.onPointerDown?.(e);
|
|
onChange(tab.id.toString());
|
|
}}
|
|
value={tab.id.toString()}
|
|
style={{
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
cursor: "grab",
|
|
userSelect: "none",
|
|
}}
|
|
color={active === tab.id.toString() ? "green" : ""}
|
|
fw={600}
|
|
fz="md"
|
|
c="#747474"
|
|
>
|
|
<Box className={classes.stationName}>
|
|
<Text fw={600} fz="md" className={classes.stationText}>
|
|
{tab.name}
|
|
</Text>
|
|
</Box>
|
|
</Tabs.Tab>
|
|
);
|
|
}
|
|
|
|
export default function DraggableTabs({
|
|
tabsData,
|
|
panels,
|
|
storageKey = "draggable-tabs-order",
|
|
onChange,
|
|
w,
|
|
isStationSettings = false,
|
|
socket,
|
|
usersConnecting,
|
|
setIsEditStation,
|
|
setIsOpenAddStation,
|
|
setStationEdit,
|
|
active,
|
|
setActive,
|
|
onSendCommand,
|
|
}: DraggableTabsProps) {
|
|
const user = useMemo(() => {
|
|
return localStorage.getItem("user") &&
|
|
typeof localStorage.getItem("user") === "string"
|
|
? JSON.parse(localStorage.getItem("user") || "")
|
|
: null;
|
|
}, []);
|
|
const [tabs, setTabs] = useState<TStation[]>(tabsData);
|
|
const [isChangeTab, setIsChangeTab] = useState<boolean>(false);
|
|
const [isSetActive, setIsSetActive] = useState<boolean>(false);
|
|
const [valueInput, setValueInput] = useState<string>("");
|
|
// const [active, setActive] = useState<string | null>(
|
|
// tabsData?.length > 0 ? tabsData[0]?.id.toString() : null
|
|
// );
|
|
|
|
const sensors = useSensors(useSensor(PointerSensor));
|
|
|
|
// Load saved order from localStorage
|
|
useEffect(() => {
|
|
if (isChangeTab) {
|
|
setTabs((pre) =>
|
|
pre
|
|
.map((t) => {
|
|
const updatedTab = tabsData.find((td) => td.id === t.id);
|
|
return updatedTab ? updatedTab : t;
|
|
})
|
|
.filter((t) => (tabsData.find((td) => td.id === t.id) ? true : false))
|
|
);
|
|
} else {
|
|
const saved = localStorage.getItem(storageKey);
|
|
let tabSelected =
|
|
tabsData?.length > 0 ? tabsData[0]?.id.toString() : null;
|
|
if (saved) {
|
|
try {
|
|
const order = JSON.parse(saved) as { id: string; index: number }[];
|
|
|
|
// Find the max index in saved order
|
|
const maxIndex = Math.max(...order.map((o) => o.index), 0);
|
|
|
|
const sorted = [...tabsData].sort((a, b) => {
|
|
const aOrder = order.find(
|
|
(o) => o.id.toString() === a.id.toString()
|
|
)?.index;
|
|
const bOrder = order.find(
|
|
(o) => o.id.toString() === b.id.toString()
|
|
)?.index;
|
|
|
|
// If not found, assign index after all existing ones
|
|
const aIndex = aOrder !== undefined ? aOrder : maxIndex + 1;
|
|
const bIndex = bOrder !== undefined ? bOrder : maxIndex + 1;
|
|
|
|
return aIndex - bIndex;
|
|
});
|
|
|
|
tabSelected = sorted?.length > 0 ? sorted[0]?.id.toString() : null;
|
|
setTabs(sorted);
|
|
} catch {
|
|
setTabs(tabsData);
|
|
}
|
|
} else {
|
|
setTabs(tabsData);
|
|
}
|
|
|
|
if (!isSetActive && tabSelected) {
|
|
setActive(tabSelected);
|
|
setTimeout(() => {
|
|
onChange(tabSelected);
|
|
}, 100);
|
|
setIsSetActive(true);
|
|
}
|
|
}
|
|
}, [tabsData, storageKey]);
|
|
|
|
// Handle reorder
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
const { active: dragActive, over } = event;
|
|
if (dragActive.id !== over?.id && over?.id) {
|
|
const oldIndex = tabs.findIndex(
|
|
(t) => t.id.toString() === dragActive.id.toString()
|
|
);
|
|
const newIndex = tabs.findIndex(
|
|
(t) => t.id.toString() === over?.id.toString()
|
|
);
|
|
const newTabs = arrayMove(tabs, oldIndex, newIndex);
|
|
setTabs(newTabs);
|
|
|
|
const order = newTabs.map((t, i) => ({ id: t.id, index: i }));
|
|
localStorage.setItem(storageKey, JSON.stringify(order));
|
|
}
|
|
};
|
|
|
|
// Clean up
|
|
useEffect(() => {
|
|
return () => {
|
|
setIsChangeTab(false);
|
|
setIsSetActive(false);
|
|
setTabs([]);
|
|
setActive("0");
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<Tabs
|
|
value={active}
|
|
onChange={(val) => {
|
|
setIsChangeTab(true);
|
|
onChange(val);
|
|
setActive(val || "0");
|
|
}}
|
|
w={w}
|
|
>
|
|
<Flex justify={"space-between"}>
|
|
<Flex style={{ width: "300px" }} align={"center"}>
|
|
<Input
|
|
style={{
|
|
width: "300px",
|
|
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
|
|
}}
|
|
placeholder={"Chat to Port/All"}
|
|
value={valueInput}
|
|
onChange={(event) => {
|
|
const newValue = event.currentTarget.value;
|
|
setValueInput(newValue);
|
|
}}
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Enter") {
|
|
onSendCommand(valueInput);
|
|
setValueInput("");
|
|
}
|
|
}}
|
|
rightSectionPointerEvents="all"
|
|
rightSection={
|
|
<CloseButton
|
|
aria-label="Clear input"
|
|
onClick={() => setValueInput("")}
|
|
style={{ display: valueInput ? undefined : "none" }}
|
|
/>
|
|
}
|
|
/>
|
|
</Flex>
|
|
<Tabs.List className={classes.list}>
|
|
<SortableContext
|
|
items={tabs}
|
|
strategy={horizontalListSortingStrategy}
|
|
>
|
|
{tabs.map((tab) => (
|
|
<SortableTab
|
|
key={tab.id}
|
|
tab={tab}
|
|
active={active}
|
|
onChange={(id) => {
|
|
setIsChangeTab(true);
|
|
onChange(id);
|
|
setActive(id);
|
|
}}
|
|
isStationSettings={isStationSettings}
|
|
/>
|
|
))}
|
|
</SortableContext>
|
|
|
|
<Flex gap={"md"} ms={"xs"} align={"center"}>
|
|
{Number(active) ? (
|
|
<ActionIcon
|
|
title="Edit Station"
|
|
variant="outline"
|
|
onClick={() => {
|
|
setStationEdit(
|
|
tabsData.find((el) => el.id === Number(active))
|
|
);
|
|
setIsOpenAddStation(true);
|
|
setIsEditStation(true);
|
|
}}
|
|
>
|
|
<IconEdit />
|
|
</ActionIcon>
|
|
) : (
|
|
""
|
|
)}
|
|
<ActionIcon
|
|
title="Add Station"
|
|
variant="outline"
|
|
color="green"
|
|
onClick={() => {
|
|
setIsOpenAddStation(true);
|
|
setIsEditStation(false);
|
|
setStationEdit(undefined);
|
|
}}
|
|
>
|
|
<IconSettingsPlus />
|
|
</ActionIcon>
|
|
</Flex>
|
|
</Tabs.List>
|
|
<Flex align={"center"}>
|
|
<Tooltip
|
|
withArrow
|
|
label={usersConnecting.map((el) => (
|
|
<Text key={el.userId}>{el.userName}</Text>
|
|
))}
|
|
>
|
|
<Avatar radius="xl" me={"sm"}>
|
|
<IconUsersGroup color="green" />
|
|
</Avatar>
|
|
</Tooltip>
|
|
<Menu withArrow>
|
|
<Menu.Target>
|
|
<UnstyledButton
|
|
style={{
|
|
padding: "var(--mantine-spacing-md)",
|
|
color: "var(--mantine-color-text)",
|
|
borderRadius: "var(--mantine-radius-sm)",
|
|
}}
|
|
>
|
|
<Group>
|
|
<div style={{ flex: 1 }}>
|
|
<Text size="sm" fw={500}>
|
|
{user?.userName || user?.user_name || ""}
|
|
</Text>
|
|
|
|
<Text c="dimmed" size="xs">
|
|
{user?.email}
|
|
</Text>
|
|
</div>
|
|
|
|
<IconChevronRight size={16} />
|
|
</Group>
|
|
</UnstyledButton>
|
|
</Menu.Target>
|
|
<Menu.Dropdown>
|
|
<Menu.Item
|
|
style={{ width: "150px" }}
|
|
onClick={() => {
|
|
localStorage.removeItem("user");
|
|
window.location.href = "/";
|
|
socket?.disconnect();
|
|
}}
|
|
color="red"
|
|
leftSection={<IconLogout size={16} stroke={1.5} />}
|
|
>
|
|
Logout
|
|
</Menu.Item>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
</Flex>
|
|
</Flex>
|
|
|
|
{panels}
|
|
</Tabs>
|
|
</DndContext>
|
|
);
|
|
}
|