list of requests
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import Td from "@/components/Td";
|
||||
import { type FC, Fragment } from "react";
|
||||
|
||||
type Props = {
|
||||
tdCount: number;
|
||||
trCount?: number;
|
||||
};
|
||||
|
||||
const DefaultTableSkeleton: FC<Props> = ({ tdCount, trCount = 5 }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
{Array.from({ length: trCount }).map((_, rowIndex) => (
|
||||
<tr className="w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5]" key={rowIndex}>
|
||||
{Array.from({ length: tdCount }).map((_, colIndex) => (
|
||||
<Td key={colIndex}>
|
||||
<div className="h-4 w-20 max-w-full rounded bg-gray-200 animate-pulse" />
|
||||
</Td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default DefaultTableSkeleton;
|
||||
@@ -0,0 +1,114 @@
|
||||
import React from "react";
|
||||
import { Group, Rect, Text } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import type { EditorObject } from "@/pages/editor/store/editorStore";
|
||||
|
||||
type EditorTableProps = {
|
||||
obj: EditorObject;
|
||||
selectedCellId: string | null;
|
||||
onCellClick: (tableId: string, cellId: string, node: Konva.Node, e?: Konva.KonvaEventObject<MouseEvent>) => void;
|
||||
onCellDblClick: (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => void;
|
||||
onDragEnd: (tableId: string, x: number, y: number) => void;
|
||||
draggable: boolean;
|
||||
};
|
||||
|
||||
const EditorTable = ({ obj, selectedCellId, onCellClick, onCellDblClick, onDragEnd, draggable }: EditorTableProps) => {
|
||||
const groupRef = React.useRef<Konva.Group>(null);
|
||||
const tableData = obj.tableData;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (groupRef.current) {
|
||||
groupRef.current.setAttr("id", obj.id);
|
||||
}
|
||||
}, [obj.id]);
|
||||
|
||||
if (!tableData) return null;
|
||||
|
||||
const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } = tableData;
|
||||
|
||||
const handleCellClick = (cellId: string, e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
e.cancelBubble = true;
|
||||
if (groupRef.current) {
|
||||
onCellClick(obj.id, cellId, groupRef.current, e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCellDblClick = (cellId: string, e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
e.cancelBubble = true;
|
||||
const stage = e.target.getStage();
|
||||
if (!stage || !groupRef.current) return;
|
||||
|
||||
const cell = cells[cellId];
|
||||
if (!cell) return;
|
||||
|
||||
const targetNode = e.target;
|
||||
const box = targetNode.getClientRect();
|
||||
const stageBox = stage.container().getBoundingClientRect();
|
||||
const x = stageBox.left + box.x;
|
||||
const y = stageBox.top + box.y;
|
||||
const width = box.width;
|
||||
const height = box.height;
|
||||
|
||||
onCellDblClick(obj.id, cellId, x, y, width, height, cell.text);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
if (groupRef.current) {
|
||||
const pos = groupRef.current.position();
|
||||
onDragEnd(obj.id, pos.x, pos.y);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Group ref={groupRef} x={obj.x} y={obj.y} draggable={draggable} onDragEnd={handleDragEnd}>
|
||||
{Array.from({ length: rows }).map((_, row) =>
|
||||
Array.from({ length: cols }).map((_, col) => {
|
||||
const cellId = `cell-${row}-${col}`;
|
||||
const cell = cells[cellId];
|
||||
if (!cell) return null;
|
||||
|
||||
const x = col * cellWidth;
|
||||
const y = row * cellHeight;
|
||||
const isCellSelected = selectedCellId === cellId;
|
||||
const isTopLeft = row === 0 && col === 0;
|
||||
const isTopRight = row === 0 && col === cols - 1;
|
||||
|
||||
return (
|
||||
<Group key={cellId}>
|
||||
<Rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={cellWidth}
|
||||
height={cellHeight}
|
||||
fill={cell.background}
|
||||
stroke={isCellSelected ? "#3b82f6" : stroke || "#808080"}
|
||||
strokeWidth={isCellSelected ? 3 : strokeWidth || 1}
|
||||
cornerRadius={isTopLeft ? [8, 0, 0, 0] : isTopRight ? [0, 8, 0, 0] : 0}
|
||||
onClick={(e) => handleCellClick(cellId, e)}
|
||||
onDblClick={(e) => handleCellDblClick(cellId, e)}
|
||||
/>
|
||||
{cell.text && (
|
||||
<Text
|
||||
x={x}
|
||||
y={y}
|
||||
width={cellWidth}
|
||||
height={cellHeight}
|
||||
text={cell.text}
|
||||
fontSize={14}
|
||||
fontFamily="Arial"
|
||||
fill="#000000"
|
||||
align="center"
|
||||
verticalAlign="middle"
|
||||
onClick={(e) => handleCellClick(cellId, e)}
|
||||
onDblClick={(e) => handleCellDblClick(cellId, e)}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorTable;
|
||||
+95
-121
@@ -1,134 +1,108 @@
|
||||
import React from "react";
|
||||
import { Group, Rect, Text } from "react-konva";
|
||||
import Konva from "konva";
|
||||
import type { EditorObject } from "@/pages/editor/store/editorStore";
|
||||
import DefaultTableSkeleton from "@/components/DefaultTableSkeleton";
|
||||
import Td from "@/components/Td";
|
||||
import type { ColumnType, RowDataType, TableProps } from "@/components/types/TableTypes";
|
||||
import { type ReactElement } from "react";
|
||||
|
||||
type TableProps = {
|
||||
obj: EditorObject;
|
||||
selectedCellId: string | null;
|
||||
onCellClick: (tableId: string, cellId: string, node: Konva.Node, e?: Konva.KonvaEventObject<MouseEvent>) => void;
|
||||
onCellDblClick: (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => void;
|
||||
onDragEnd: (tableId: string, x: number, y: number) => void;
|
||||
draggable: boolean;
|
||||
};
|
||||
const Table = <T extends RowDataType>({
|
||||
columns,
|
||||
data,
|
||||
isLoading = false,
|
||||
onRowClick,
|
||||
className = "",
|
||||
rowClassName = "",
|
||||
noDataMessage = "هیچ دادهای یافت نشد",
|
||||
headerClassName = "bg-gray-50",
|
||||
emptyRowsCount = 5,
|
||||
showHeader = true,
|
||||
actions = null,
|
||||
actionsPosition = "top",
|
||||
}: TableProps<T>): ReactElement => {
|
||||
const getRowClassName = (item: T, index: number): string => {
|
||||
if (typeof rowClassName === "function") {
|
||||
return rowClassName(item, index);
|
||||
}
|
||||
return rowClassName;
|
||||
};
|
||||
|
||||
const Table = ({
|
||||
obj,
|
||||
selectedCellId,
|
||||
onCellClick,
|
||||
onCellDblClick,
|
||||
onDragEnd,
|
||||
draggable,
|
||||
}: TableProps) => {
|
||||
const groupRef = React.useRef<Konva.Group>(null);
|
||||
const tableData = obj.tableData;
|
||||
const getCellClassName = (column: ColumnType<T>): string => {
|
||||
const alignClass = column.align ? `text-${column.align}` : "";
|
||||
return `px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs ${alignClass} ${column.className || ""}`.trim();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (groupRef.current) {
|
||||
groupRef.current.setAttr("id", obj.id);
|
||||
}
|
||||
}, [obj.id]);
|
||||
const handleRowClick = (item: T) => {
|
||||
if (onRowClick) {
|
||||
onRowClick(item.id, item);
|
||||
}
|
||||
};
|
||||
|
||||
if (!tableData) return null;
|
||||
const renderTableBody = () => {
|
||||
if (isLoading) {
|
||||
return <DefaultTableSkeleton tdCount={columns.length} trCount={emptyRowsCount} />;
|
||||
}
|
||||
|
||||
const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } = tableData;
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-3 md:px-6 py-6 md:py-8 text-center text-gray-500">
|
||||
{noDataMessage}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
const handleCellClick = (cellId: string, e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
e.cancelBubble = true;
|
||||
if (groupRef.current) {
|
||||
onCellClick(obj.id, cellId, groupRef.current, e);
|
||||
}
|
||||
};
|
||||
return data.map((item, rowIndex) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className={`w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5] hover:bg-[#F4F5F9] ${onRowClick ? "cursor-pointer" : ""} ${getRowClassName(item, rowIndex)}`}
|
||||
onClick={() => handleRowClick(item)}
|
||||
>
|
||||
{columns.map((col) => (
|
||||
<td key={`${item.id}-${col.key}`} className={getCellClassName(col)} style={{ width: col.width }}>
|
||||
{col.render ? col.render(item) : (item[col.key] as React.ReactNode)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
));
|
||||
};
|
||||
|
||||
const handleCellDblClick = (cellId: string, e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
e.cancelBubble = true;
|
||||
const stage = e.target.getStage();
|
||||
if (!stage || !groupRef.current) return;
|
||||
|
||||
const cell = cells[cellId];
|
||||
if (!cell) return;
|
||||
|
||||
// Get the target node (Rect or Text)
|
||||
const targetNode = e.target;
|
||||
|
||||
// Get the client rect which includes all transformations (scale, position, etc.)
|
||||
const box = targetNode.getClientRect();
|
||||
const stageBox = stage.container().getBoundingClientRect();
|
||||
|
||||
// Calculate position relative to viewport
|
||||
const x = stageBox.left + box.x;
|
||||
const y = stageBox.top + box.y;
|
||||
const width = box.width;
|
||||
const height = box.height;
|
||||
|
||||
onCellDblClick(obj.id, cellId, x, y, width, height, cell.text);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
if (groupRef.current) {
|
||||
const pos = groupRef.current.position();
|
||||
onDragEnd(obj.id, pos.x, pos.y);
|
||||
}
|
||||
};
|
||||
const renderHeader = () => {
|
||||
if (!showHeader || actionsPosition === "header-replace") return null;
|
||||
|
||||
return (
|
||||
<Group
|
||||
ref={groupRef}
|
||||
x={obj.x}
|
||||
y={obj.y}
|
||||
draggable={draggable}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{Array.from({ length: rows }).map((_, row) =>
|
||||
Array.from({ length: cols }).map((_, col) => {
|
||||
const cellId = `cell-${row}-${col}`;
|
||||
const cell = cells[cellId];
|
||||
if (!cell) return null;
|
||||
|
||||
const x = col * cellWidth;
|
||||
const y = row * cellHeight;
|
||||
const isCellSelected = selectedCellId === cellId;
|
||||
|
||||
const isTopLeft = row === 0 && col === 0;
|
||||
const isTopRight = row === 0 && col === cols - 1;
|
||||
|
||||
return (
|
||||
<Group key={cellId}>
|
||||
<Rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={cellWidth}
|
||||
height={cellHeight}
|
||||
fill={cell.background}
|
||||
stroke={isCellSelected ? "#3b82f6" : stroke || "#808080"}
|
||||
strokeWidth={isCellSelected ? 3 : strokeWidth || 1}
|
||||
cornerRadius={isTopLeft ? [8, 0, 0, 0] : isTopRight ? [0, 8, 0, 0] : 0}
|
||||
onClick={(e) => handleCellClick(cellId, e)}
|
||||
onDblClick={(e) => handleCellDblClick(cellId, e)}
|
||||
/>
|
||||
{cell.text && (
|
||||
<Text
|
||||
x={x}
|
||||
y={y}
|
||||
width={cellWidth}
|
||||
height={cellHeight}
|
||||
text={cell.text}
|
||||
fontSize={14}
|
||||
fontFamily="Arial"
|
||||
fill="#000000"
|
||||
align="center"
|
||||
verticalAlign="middle"
|
||||
onClick={(e) => handleCellClick(cellId, e)}
|
||||
onDblClick={(e) => handleCellDblClick(cellId, e)}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Group>
|
||||
<thead className={`h-[60px] md:h-[69px] bg-white text-sm text-[#8C90A3] ${headerClassName}`}>
|
||||
<tr>
|
||||
{columns.map((col) => (
|
||||
<Td key={col.key} text={col.title} />
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
};
|
||||
|
||||
const renderActions = () => {
|
||||
if (!actions) return null;
|
||||
|
||||
return (
|
||||
<div className="h-[64px] rounded-t-2xl md:rounded-t-3xl md:h-[74px] bg-white flex items-center px-3 md:px-6">
|
||||
<div className="flex items-center gap-2 md:gap-4">{actions}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative w-full ${className}`}>
|
||||
{(actionsPosition === "top" || actionsPosition === "above-header") && renderActions()}
|
||||
|
||||
<div
|
||||
className={`overflow-x-auto overflow-hidden ${showHeader && actionsPosition !== "header-replace" ? "rounded-2xl md:rounded-3xl" : "rounded-b-2xl md:rounded-b-3xl"} bg-white`}
|
||||
>
|
||||
<table className="w-full text-sm border-collapse min-w-[600px]">
|
||||
{renderHeader()}
|
||||
<tbody>{renderTableBody()}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type FC, type ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
text?: string | number | ReactNode;
|
||||
children?: ReactNode;
|
||||
dir?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Td: FC<Props> = ({ text, children, dir, className }) => {
|
||||
return (
|
||||
<td
|
||||
className={`px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs ${className || ""}`}
|
||||
style={{ direction: dir === "ltr" ? "ltr" : "rtl" }}
|
||||
>
|
||||
{text ?? children}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
export default Td;
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type RowDataType = Record<string, unknown> & { id: string | number };
|
||||
|
||||
export type ColumnType<T extends RowDataType = RowDataType> = {
|
||||
title: string;
|
||||
key: string;
|
||||
render?: (item: T) => ReactNode;
|
||||
width?: string | number;
|
||||
align?: "left" | "center" | "right";
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type TableProps<T extends RowDataType = RowDataType> = {
|
||||
columns: ColumnType<T>[];
|
||||
data?: T[];
|
||||
isLoading?: boolean;
|
||||
onRowClick?: (id: T["id"], item: T) => void;
|
||||
className?: string;
|
||||
rowClassName?: string | ((item: T, index: number) => string);
|
||||
noDataMessage?: ReactNode;
|
||||
headerClassName?: string;
|
||||
emptyRowsCount?: number;
|
||||
showHeader?: boolean;
|
||||
actions?: ReactNode;
|
||||
actionsPosition?: "top" | "header-replace" | "above-header";
|
||||
};
|
||||
Reference in New Issue
Block a user