Enhance ticket handling and UI interactions

Backend: Added lineId to ticket creation, improved ticket update logic, and switched ticket update route to POST. Added 'update_ticket' event to socket.io provider. Frontend: Integrated 'motion' for animated bottom toolbar, added expand/collapse functionality, improved ticket creation and update flows, and ensured terminal focus on CLI open. Adjusted delays in ButtonDPELP, improved ticket info copy, and enhanced input handling in ModalTerminal. Updated dependencies to include 'motion'.
This commit is contained in:
nguyentrungthat 2025-11-19 13:57:52 +07:00
parent d908cf204c
commit c36b9f69df
11 changed files with 501 additions and 320 deletions

View File

@ -85,6 +85,7 @@ export default class TicketsController {
model: payload.model.trim(), model: payload.model.trim(),
sn: payload.sn.trim(), sn: payload.sn.trim(),
stationId: payload.station_id, stationId: payload.station_id,
lineId: payload.line_id,
status: 'open', status: 'open',
history: JSON.stringify(history), history: JSON.stringify(history),
}, },
@ -149,11 +150,14 @@ export default class TicketsController {
const listHistory = ticket.history ? JSON.parse(ticket.history) : [] const listHistory = ticket.history ? JSON.parse(ticket.history) : []
listHistory.unshift(history) listHistory.unshift(history)
payload.history = JSON.stringify(listHistory) payload.history = JSON.stringify(listHistory)
delete payload.userName
delete payload.userId
ticket.merge(payload) ticket.merge(payload)
await ticket.save() await ticket.save()
return response.ok({ status: true, message: 'Ticket updated successfully', data: ticket }) return response.ok({ status: true, message: 'Ticket updated successfully', data: ticket })
} catch (error) { } catch (error) {
console.log(error)
return response.internalServerError({ return response.internalServerError({
status: false, status: false,
message: 'Failed to update ticket', message: 'Failed to update ticket',

View File

@ -448,6 +448,10 @@ export class WebSocketIo {
} }
} }
}) })
socket.on('update_ticket', async (data) => {
io.emit('update_ticket', data)
})
}) })
socketServer.listen(SOCKET_IO_PORT, () => { socketServer.listen(SOCKET_IO_PORT, () => {

View File

@ -77,7 +77,7 @@ router
router.post('/all', '#controllers/tickets_controller.getAll') router.post('/all', '#controllers/tickets_controller.getAll')
router.post('create', '#controllers/tickets_controller.create') router.post('create', '#controllers/tickets_controller.create')
router.put('update/:id', '#controllers/tickets_controller.update') router.post('update/:id', '#controllers/tickets_controller.update')
router.delete('delete/:id', '#controllers/tickets_controller.delete') router.delete('delete/:id', '#controllers/tickets_controller.delete')
}) })
.prefix('api/ticket') .prefix('api/ticket')

View File

@ -20,6 +20,7 @@
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"axios": "^1.12.2", "axios": "^1.12.2",
"moment": "^2.30.1", "moment": "^2.30.1",
"motion": "^12.23.24",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.9.4", "react-router-dom": "^7.9.4",
@ -2868,6 +2869,33 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/framer-motion": {
"version": "12.23.24",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -3339,6 +3367,47 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/motion": {
"version": "12.23.24",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz",
"integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==",
"license": "MIT",
"dependencies": {
"framer-motion": "^12.23.24",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@ -22,6 +22,7 @@
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"axios": "^1.12.2", "axios": "^1.12.2",
"moment": "^2.30.1", "moment": "^2.30.1",
"motion": "^12.23.24",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.9.4", "react-router-dom": "^7.9.4",

View File

@ -77,6 +77,7 @@ function App() {
const [usersConnecting, setUsersConnecting] = useState<TUser[]>([]); const [usersConnecting, setUsersConnecting] = useState<TUser[]>([]);
const [testLogContent, setTestLogContent] = useState(""); const [testLogContent, setTestLogContent] = useState("");
const [isLogModalOpen, setIsLogModalOpen] = useState(false); const [isLogModalOpen, setIsLogModalOpen] = useState(false);
const [expandedBottomBar, setExpandedBottomBar] = useState(true);
const connectApcSwitch = (station: TStation) => { const connectApcSwitch = (station: TStation) => {
if (station?.apc_1_ip && station?.apc_1_port) { if (station?.apc_1_ip && station?.apc_1_port) {
@ -281,6 +282,18 @@ function App() {
}, 100); }, 100);
}); });
socket?.on("update_ticket", (data) => {
setTimeout(() => {
updateValueLineStation(
data.lineId,
{
tickets: data.data,
},
data?.stationId
);
}, 100);
});
// ✅ cleanup on unmount or when socket changes // ✅ cleanup on unmount or when socket changes
return () => { return () => {
socket.off("init"); socket.off("init");
@ -293,6 +306,7 @@ function App() {
socket.off("user_close_cli"); socket.off("user_close_cli");
socket.off("response_content_log"); socket.off("response_content_log");
socket.off("data_textfsm"); socket.off("data_textfsm");
socket.off("update_ticket");
}; };
}, [socket, stations, selectedLine]); }, [socket, stations, selectedLine]);
@ -396,7 +410,7 @@ function App() {
borderRadius: 8, borderRadius: 8,
}} }}
> >
<ScrollArea h={"73vh"}> <ScrollArea h={expandedBottomBar ? "73vh" : "85vh"}>
{station.lines.length > 8 ? ( {station.lines.length > 8 ? (
<Grid <Grid
style={{ style={{
@ -492,6 +506,7 @@ function App() {
setIsLogModalOpen={setIsLogModalOpen} setIsLogModalOpen={setIsLogModalOpen}
setTestLogContent={setTestLogContent} setTestLogContent={setTestLogContent}
scenarios={scenarios} scenarios={scenarios}
setExpanded={setExpandedBottomBar}
/> />
</Flex> </Flex>
</Tabs.Panel> </Tabs.Panel>

View File

@ -1,4 +1,5 @@
import { import {
ActionIcon,
Box, Box,
Button, Button,
CloseButton, CloseButton,
@ -18,6 +19,8 @@ import { ButtonDPELP, ButtonScenario, ButtonSelect } from "./ButtonAction";
import DrawerLogs from "./DrawerLogs"; import DrawerLogs from "./DrawerLogs";
import { DrawerAPCControl, DrawerSwitchControl } from "./DrawerControl"; import { DrawerAPCControl, DrawerSwitchControl } from "./DrawerControl";
import { isJsonString } from "../untils/helper"; import { isJsonString } from "../untils/helper";
import { motion } from "motion/react";
import { IconCaretDown, IconCaretUp } from "@tabler/icons-react";
interface TabsProps { interface TabsProps {
selectedLines: TLine[]; selectedLines: TLine[];
@ -31,6 +34,7 @@ interface TabsProps {
setIsLogModalOpen: (value: React.SetStateAction<boolean>) => void; setIsLogModalOpen: (value: React.SetStateAction<boolean>) => void;
setTestLogContent: (value: React.SetStateAction<string>) => void; setTestLogContent: (value: React.SetStateAction<string>) => void;
scenarios: IScenario[]; scenarios: IScenario[];
setExpanded: (value: React.SetStateAction<boolean>) => void;
} }
const BottomToolBar = ({ const BottomToolBar = ({
@ -45,6 +49,7 @@ const BottomToolBar = ({
setIsLogModalOpen, setIsLogModalOpen,
setTestLogContent, setTestLogContent,
scenarios, scenarios,
setExpanded,
}: TabsProps) => { }: TabsProps) => {
const user = useMemo(() => { const user = useMemo(() => {
return localStorage.getItem("user") && return localStorage.getItem("user") &&
@ -54,8 +59,46 @@ const BottomToolBar = ({
}, []); }, []);
const [valueInput, setValueInput] = useState<string>(""); const [valueInput, setValueInput] = useState<string>("");
const [activeTabBottom, setActiveBottom] = useState<string>("command"); const [activeTabBottom, setActiveBottom] = useState<string>("command");
const [isExpand, setIsExpand] = useState<boolean>(true);
return ( return (
<motion.div
initial={false}
animate={{
height: isExpand ? "15vh" : 0,
y: 0, // đẩy xuống khi thu nhỏ
}}
transition={{ type: "spring", stiffness: 180, damping: 20 }}
style={{
width: "100%",
position: "fixed",
bottom: 0,
left: 0,
zIndex: 1,
}}
>
<Box style={{ position: "relative" }}>
<ActionIcon
style={{
position: "absolute",
top: isExpand ? -4 : -24,
left: "50%",
translate: "-19px 0",
backgroundColor: "#e3e0e0",
width: "55px",
}}
variant="light"
onClick={() => {
setIsExpand((prev) => !prev);
setExpanded((prev) => !prev);
}}
>
{isExpand ? (
<IconCaretDown color="green" />
) : (
<IconCaretUp color="green" />
)}
</ActionIcon>
<Grid> <Grid>
<Grid.Col span={1}></Grid.Col> <Grid.Col span={1}></Grid.Col>
<Grid.Col span={10}> <Grid.Col span={10}>
@ -72,7 +115,8 @@ const BottomToolBar = ({
<Tabs.List> <Tabs.List>
<Tabs.Tab <Tabs.Tab
style={{ style={{
backgroundColor: activeTabBottom === "command" ? "#c8d9fd" : "", backgroundColor:
activeTabBottom === "command" ? "#c8d9fd" : "",
fontSize: "13px", fontSize: "13px",
paddingTop: "8px", paddingTop: "8px",
paddingBottom: "8px", paddingBottom: "8px",
@ -94,7 +138,8 @@ const BottomToolBar = ({
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab <Tabs.Tab
style={{ style={{
backgroundColor: activeTabBottom === "switch" ? "#c8d9fd" : "", backgroundColor:
activeTabBottom === "switch" ? "#c8d9fd" : "",
fontSize: "13px", fontSize: "13px",
paddingTop: "8px", paddingTop: "8px",
paddingBottom: "8px", paddingBottom: "8px",
@ -127,7 +172,9 @@ const BottomToolBar = ({
aria-label="Clear input" aria-label="Clear input"
onClick={() => { onClick={() => {
setSelectedLines( setSelectedLines(
selectedLines.filter((line) => line.id !== el.id) selectedLines.filter(
(line) => line.id !== el.id
)
); );
socket?.emit("close_cli", { socket?.emit("close_cli", {
lineId: el?.id, lineId: el?.id,
@ -337,6 +384,8 @@ const BottomToolBar = ({
</Grid.Col> </Grid.Col>
<Grid.Col span={1}></Grid.Col> <Grid.Col span={1}></Grid.Col>
</Grid> </Grid>
</Box>
</motion.div>
); );
}; };

View File

@ -68,42 +68,42 @@ export const ButtonDPELP = ({
{ {
expect: "", expect: "",
send: "show diag", send: "show diag",
delay: "2000", delay: "1500",
repeat: "1", repeat: "1",
note: "", note: "",
}, },
{ {
expect: "", expect: "",
send: "show post", send: "show post",
delay: "3000", delay: "1500",
repeat: "1", repeat: "1",
note: "", note: "",
}, },
{ {
expect: "", expect: "",
send: "show env all", send: "show env all",
delay: "3000", delay: "1500",
repeat: "1", repeat: "1",
note: "", note: "",
}, },
{ {
expect: "", expect: "",
send: "show license", send: "show license",
delay: "3000", delay: "1500",
repeat: "1", repeat: "1",
note: "", note: "",
}, },
{ {
expect: "", expect: "",
send: "show log", send: "show log",
delay: "3000", delay: "1500",
repeat: "1", repeat: "1",
note: "", note: "",
}, },
{ {
expect: "", expect: "",
send: "show platform", send: "show platform",
delay: "3000", delay: "1500",
repeat: "1", repeat: "1",
note: "", note: "",
}, },

View File

@ -37,6 +37,7 @@ const CardLine = ({
}, []); }, []);
const [isDisabled, setIsDisabled] = useState<boolean>(false); const [isDisabled, setIsDisabled] = useState<boolean>(false);
const [valueBaud, setValueBaud] = useState<string>(""); const [valueBaud, setValueBaud] = useState<string>("");
const [focusTerminal, setFocusTerminal] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if ( if (
@ -109,6 +110,7 @@ const CardLine = ({
} }
} else { } else {
setSelectedLines((pre) => [...pre, line]); setSelectedLines((pre) => [...pre, line]);
setFocusTerminal(true);
socket?.emit("open_cli", { socket?.emit("open_cli", {
lineId: line.id, lineId: line.id,
stationId: line.stationId || line.station_id, stationId: line.stationId || line.station_id,
@ -177,6 +179,10 @@ const CardLine = ({
navigator.clipboard.writeText( navigator.clipboard.writeText(
`PID: ${line?.inventory?.pid || ""} | SN: ${ `PID: ${line?.inventory?.pid || ""} | SN: ${
line?.inventory?.sn || "" line?.inventory?.sn || ""
} | Ticket: ${
line?.tickets && line?.tickets?.length > 0
? line?.tickets[0].description
: ""
}` }`
); );
}} }}
@ -541,6 +547,7 @@ const CardLine = ({
handleClick(); handleClick();
}} }}
onBlur={() => { onBlur={() => {
setFocusTerminal(false);
if ( if (
!selectedLines.find((value) => value.id === line?.id) && !selectedLines.find((value) => value.id === line?.id) &&
line?.userOpenCLI === user?.userName line?.userOpenCLI === user?.userName
@ -550,6 +557,7 @@ const CardLine = ({
stationId: line.stationId || line.station_id, stationId: line.stationId || line.station_id,
}); });
}} }}
focusTerminal={focusTerminal}
/> />
</Box> </Box>
<Box> <Box>

View File

@ -55,7 +55,6 @@ const ModalTerminal = ({
? JSON.parse(localStorage.getItem("user") || "") ? JSON.parse(localStorage.getItem("user") || "")
: null; : null;
}, []); }, []);
const [inputTicket, setInputTicket] = useState<string>("");
const [isDisable, setIsDisable] = useState<boolean>(false); const [isDisable, setIsDisable] = useState<boolean>(false);
const [isDisableTicket, setIsDisableTicket] = useState<boolean>(false); const [isDisableTicket, setIsDisableTicket] = useState<boolean>(false);
const [latestTicket, setLatestTicket] = useState<TDataTicket>({ const [latestTicket, setLatestTicket] = useState<TDataTicket>({
@ -116,7 +115,7 @@ const ModalTerminal = ({
}} }}
> >
<Text style={{ fontSize: "14px", fontWeight: "bold" }} fw={600}> <Text style={{ fontSize: "14px", fontWeight: "bold" }} fw={600}>
@{item?.userName}: {item?.status === "closed" ? "*" : ""}@{item?.userName}:
</Text> </Text>
<Text <Text
@ -168,14 +167,18 @@ const ModalTerminal = ({
description: dataTicket.description.trim(), description: dataTicket.description.trim(),
model: dataTicket.model.trim(), model: dataTicket.model.trim(),
sn: dataTicket.sn.trim(), sn: dataTicket.sn.trim(),
station_id: Number(dataTicket.station_id), station_id: Number(stationItem?.id),
line_id: Number(line?.id),
status: "open", status: "open",
userName: user?.userName,
userId: user?.id,
}; };
try { try {
const res = await axios.post(apiUrl + "api/ticket/create", payload); const res = await axios.post(apiUrl + "api/ticket/create", payload);
if (res.status) { if (res.status) {
// setDataTicket(res.data) // setLatestTicket(res.data);
// setDataTicket({ ...res.data, description: "" });
// notifications.show({ // notifications.show({
// title: 'Success', // title: 'Success',
@ -183,10 +186,11 @@ const ModalTerminal = ({
// color: 'green', // color: 'green',
// }) // })
// socket?.emit( socket?.emit("update_ticket", {
// "create_ticket", lineId: Number(line?.id),
// payload, data: [res.data.data, ...(line?.tickets || [])],
// ) stationId: Number(stationItem?.id),
});
return; return;
} }
} catch (error) { } catch (error) {
@ -218,38 +222,44 @@ const ModalTerminal = ({
: "", : "",
model: dataTicket.model.trim(), model: dataTicket.model.trim(),
sn: dataTicket.sn.trim(), sn: dataTicket.sn.trim(),
station_id: Number(dataTicket.station_id), station_id: Number(stationItem?.id),
line_id: Number(line?.id),
status: status, status: status,
userName: user?.userName,
userId: user?.id,
}; };
try { try {
const res = await axios.put( const res = await axios.post(
`${apiUrl + "api/ticket/create" + "/" + dataTicket.id}`, `${apiUrl + "api/ticket/update" + "/" + dataTicket.id}`,
payload payload
); );
if (res.status) { if (res.status) {
if (res?.data?.status !== "closed") // if (res?.data?.status !== "closed")
setDataTicket({ ...res.data, description: "" }); // setDataTicket({ ...res.data, description: "" });
else // else
setDataTicket({ // setDataTicket({
id: 0, // id: 0,
description: "", // description: "",
model: latestTicket.model.trim(), // model: latestTicket.model.trim(),
sn: latestTicket.sn.trim(), // sn: latestTicket.sn.trim(),
station_id: latestTicket.station_id, // station_id: latestTicket.station_id,
history: "", // history: "",
status: "open", // status: "open",
}); // });
// notifications.show({ // notifications.show({
// title: 'Success', // title: 'Success',
// message: res.message, // message: res.message,
// color: 'green', // color: 'green',
// }) // })
// socket?.emit( socket?.emit("update_ticket", {
// SOCKET_EVENTS.RELOAD_TICKET.RELOAD_TICKET_FROM_WEB, lineId: Number(line?.id),
// payload, data: line?.tickets?.map((el) =>
// ) el.id === dataTicket.id ? res.data.data : el
),
stationId: Number(stationItem?.id),
});
return; return;
} }
} catch (error) { } catch (error) {
@ -308,7 +318,7 @@ const ModalTerminal = ({
> >
<Grid> <Grid>
<Grid.Col span={2}> <Grid.Col span={2}>
<Flex justify={"space-between"} direction={"column"} h={"100%"}> <Flex justify={"space-between"} direction={"column"} h={"95%"}>
<Box> <Box>
<Flex gap={"sm"} justify={"center"} align={"center"}> <Flex gap={"sm"} justify={"center"} align={"center"}>
<Text size="xl"> <Text size="xl">
@ -324,7 +334,7 @@ const ModalTerminal = ({
)} )}
</Flex> </Flex>
<Flex mt="4px"> <Flex mt="4px">
<Text size="md" w={"50px"}> <Text size="md" mr="6px">
BAUD: BAUD:
</Text> </Text>
<Text size="md"> <Text size="md">
@ -332,7 +342,7 @@ const ModalTerminal = ({
</Text> </Text>
</Flex> </Flex>
<Flex mt="4px"> <Flex mt="4px">
<Text size="md" w={"50px"}> <Text size="md" mr="6px">
PID: PID:
</Text> </Text>
<Text size="md">{line?.inventory?.pid || ""}</Text> <Text size="md">{line?.inventory?.pid || ""}</Text>
@ -345,13 +355,13 @@ const ModalTerminal = ({
)} )}
</Flex> </Flex>
<Flex mt="4px"> <Flex mt="4px">
<Text size="md" w={"50px"}> <Text size="md" mr="6px">
SN: SN:
</Text> </Text>
<Text size="md">{line?.inventory?.sn || ""}</Text> <Text size="md">{line?.inventory?.sn || ""}</Text>
</Flex> </Flex>
<Flex mt="4px"> <Flex mt="4px">
<Text size="md" mr={"sm"} fw={"bold"} w={"50px"}> <Text size="md" mr={"6px"} fw={"bold"}>
IOS: IOS:
</Text> </Text>
<Text size="md">{""}</Text> <Text size="md">{""}</Text>
@ -543,7 +553,6 @@ const ModalTerminal = ({
variant="filled" variant="filled"
color="orange" color="orange"
size="xs" size="xs"
radius="md"
onClick={() => { onClick={() => {
socket?.emit("write_command_line_from_web", { socket?.emit("write_command_line_from_web", {
lineIds: [line?.id], lineIds: [line?.id],
@ -648,7 +657,11 @@ const ModalTerminal = ({
</Flex> </Flex>
</Tooltip> </Tooltip>
</Box> </Box>
<ScrollArea h={600} style={{ border: "1px solid #ccc" }} p={"4px"}> <ScrollArea
h={"65vh"}
style={{ border: "1px solid #ccc" }}
p={"4px"}
>
{renderHistory(latestTicket)} {renderHistory(latestTicket)}
</ScrollArea> </ScrollArea>
<Box mt={"8px"}> <Box mt={"8px"}>
@ -658,14 +671,16 @@ const ModalTerminal = ({
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)", boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
}} }}
placeholder={"Input description ticket"} placeholder={"Input description ticket"}
value={inputTicket} value={dataTicket.description || ""}
onChange={(event) => { onChange={(event) => {
const newValue = event.currentTarget.value; setDataTicket((pre) => ({
setInputTicket(newValue); ...pre,
description: event.target.value,
}));
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === "Enter") { if (event.key === "Enter" && dataTicket.description) {
setInputTicket(""); setDataTicket((pre) => ({ ...pre, description: "" }));
if (dataTicket?.status === "closed") { if (dataTicket?.status === "closed") {
handleCreate(); handleCreate();
} else handleUpdate("open"); } else handleUpdate("open");
@ -679,9 +694,11 @@ const ModalTerminal = ({
rightSection={ rightSection={
<CloseButton <CloseButton
aria-label="Clear input" aria-label="Clear input"
onClick={() => setInputTicket("")} onClick={() =>
setDataTicket((pre) => ({ ...pre, description: "" }))
}
style={{ style={{
display: inputTicket ? undefined : "none", display: dataTicket?.description ? undefined : "none",
}} }}
/> />
} }
@ -691,12 +708,13 @@ const ModalTerminal = ({
<Flex justify={"end"} mt={"4px"}> <Flex justify={"end"} mt={"4px"}>
{dataTicket?.status === "closed" ? ( {dataTicket?.status === "closed" ? (
<Button <Button
disabled={isDisableTicket} disabled={isDisableTicket || !dataTicket.description}
mr={"8px"} mr={"8px"}
fw={400} fw={400}
variant="outline" variant="outline"
size="xs" size="xs"
onClick={() => { onClick={() => {
setDataTicket((pre) => ({ ...pre, description: "" }));
handleCreate(); handleCreate();
setIsDisableTicket(true); setIsDisableTicket(true);
setTimeout(() => { setTimeout(() => {
@ -708,7 +726,7 @@ const ModalTerminal = ({
</Button> </Button>
) : ( ) : (
<Button <Button
disabled={isDisableTicket} disabled={isDisableTicket || !dataTicket.description}
mr={"8px"} mr={"8px"}
fw={400} fw={400}
variant="outline" variant="outline"
@ -725,13 +743,18 @@ const ModalTerminal = ({
</Button> </Button>
)} )}
<Button <Button
disabled={isDisableTicket || dataTicket?.status === "closed"} disabled={
isDisableTicket ||
dataTicket?.status === "closed" ||
!dataTicket.description
}
mr={"8px"} mr={"8px"}
fw={400} fw={400}
variant="outline" variant="outline"
color="orange" color="orange"
size="xs" size="xs"
onClick={() => { onClick={() => {
setDataTicket((pre) => ({ ...pre, description: "" }));
handleUpdate("issue"); handleUpdate("issue");
setIsDisableTicket(true); setIsDisableTicket(true);
setTimeout(() => { setTimeout(() => {

View File

@ -29,6 +29,7 @@ interface TerminalCLIProps {
fontSize?: number; fontSize?: number;
miniSize?: boolean; miniSize?: boolean;
loadingContent?: boolean; loadingContent?: boolean;
focusTerminal?: boolean;
} }
const TerminalCLI: React.FC<TerminalCLIProps> = ({ const TerminalCLI: React.FC<TerminalCLIProps> = ({
@ -46,6 +47,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
loadingContent = false, loadingContent = false,
onFocus, onFocus,
onBlur, onBlur,
focusTerminal,
}) => { }) => {
const xtermRef = useRef<HTMLDivElement>(null); const xtermRef = useRef<HTMLDivElement>(null);
const terminal = useRef<Terminal>(null); const terminal = useRef<Terminal>(null);
@ -177,6 +179,12 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
}; };
}, []); }, []);
useEffect(() => {
if (focusTerminal && terminal.current) {
terminal.current?.focus();
}
}, [focusTerminal]);
return ( return (
<> <>
<div <div
@ -185,7 +193,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
height: "100%", height: "100%",
backgroundColor: "black", backgroundColor: "black",
paddingBottom: customStyle.paddingBottom ?? "10px", paddingBottom: customStyle.paddingBottom ?? "10px",
maxHeight: customStyle.maxHeight ?? "73vh", maxHeight: customStyle.maxHeight ?? "70vh",
}} }}
> >
<div <div
@ -206,8 +214,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
paddingLeft: customStyle.paddingLeft ?? "10px", paddingLeft: customStyle.paddingLeft ?? "10px",
paddingBottom: customStyle.paddingBottom ?? "10px", paddingBottom: customStyle.paddingBottom ?? "10px",
fontSize: customStyle.fontSize ?? "9px", fontSize: customStyle.fontSize ?? "9px",
maxHeight: customStyle.maxHeight ?? "73vh", maxHeight: customStyle.maxHeight ?? "70vh",
height: customStyle.height ?? "73vh", height: customStyle.height ?? "70vh",
padding: customStyle.padding ?? "4px", padding: customStyle.padding ?? "4px",
}} }}
onDoubleClick={(event) => { onDoubleClick={(event) => {