Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled

Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
chuan
2025-11-07 00:00:52 +08:00
commit 8ebde8aa05
4886 changed files with 462270 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
"use client";
import * as React from "react";
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
Table as TanstackTable,
} from "@tanstack/react-table";
import {
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Search } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { CloseIcon } from "@plane/propel/icons";
// plane package imports
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table";
import { cn } from "@plane/utils";
// plane web components
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
searchPlaceholder: string;
actions?: (table: TanstackTable<TData>) => React.ReactNode;
}
export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, actions }: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const { t } = useTranslation();
const inputRef = React.useRef<HTMLInputElement>(null);
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
const table = useReactTable({
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
},
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
});
return (
<div className="space-y-4">
<div className="flex w-full items-center justify-between">
<div className="relative flex max-w-[300px] items-center gap-4 ">
{table.getHeaderGroups()?.[0]?.headers?.[0]?.id && (
<div className="flex items-center gap-2 whitespace-nowrap text-sm text-custom-text-400">
{searchPlaceholder}
</div>
)}
{!isSearchOpen && (
<button
type="button"
className="-mr-5 grid place-items-center rounded p-2 text-custom-text-400 hover:bg-custom-background-80"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"mr-auto flex w-0 items-center justify-start gap-1 overflow-hidden rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 opacity-0 transition-[width] ease-linear",
{
"w-64 border-custom-border-200 px-2.5 py-1.5 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={table.getColumn(table.getHeaderGroups()?.[0]?.headers?.[0]?.id)?.getFilterValue() as string}
onChange={(e) => {
const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id;
if (columnId) table.getColumn(columnId)?.setFilterValue(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setIsSearchOpen(true);
}
}}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
const columnId = table.getHeaderGroups()?.[0]?.headers?.[0]?.id;
if (columnId) {
table.getColumn(columnId)?.setFilterValue("");
}
setIsSearchOpen(false);
}}
>
<CloseIcon className="h-3 w-3" />
</button>
)}
</div>
</div>
{actions && <div>{actions(table)}</div>}
</div>
<div className="rounded-md">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id} colSpan={header.colSpan} className="whitespace-nowrap">
{header.isPlaceholder
? null
: (flexRender(header.column.columnDef.header, header.getContext()) as any)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext()) as any}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="p-0">
<EmptyStateCompact
assetKey="unknown"
assetClassName="size-20"
rootClassName="border border-custom-border-100 px-5 py-10 md:py-20 md:px-20"
title={t("workspace_empty_state.analytics_work_items.title")}
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export * from "./root";

View File

@@ -0,0 +1,34 @@
import * as React from "react";
import type { ColumnDef } from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@plane/propel/table";
import { Loader } from "@plane/ui";
interface TableSkeletonProps {
columns: ColumnDef<any>[];
rows: number;
}
export const TableLoader: React.FC<TableSkeletonProps> = ({ columns, rows }) => (
<Table>
<TableHeader>
<TableRow>
{columns.map((column, index) => (
<TableHead key={column.header?.toString() ?? index}>
{typeof column.header === "string" ? column.header : ""}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: rows }).map((_, rowIndex) => (
<TableRow key={rowIndex}>
{columns.map((_, colIndex) => (
<TableCell key={colIndex}>
<Loader.Item height="20px" width="100%" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
);

View File

@@ -0,0 +1,45 @@
import type { ColumnDef, Row, Table } from "@tanstack/react-table";
import { Download } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import type { AnalyticsTableDataMap, TAnalyticsTabsBase } from "@plane/types";
import { DataTable } from "./data-table";
import { TableLoader } from "./loader";
interface InsightTableProps<T extends Exclude<TAnalyticsTabsBase, "overview">> {
analyticsType: T;
data?: AnalyticsTableDataMap[T][];
isLoading?: boolean;
columns: ColumnDef<AnalyticsTableDataMap[T]>[];
columnsLabels?: Record<string, string>;
headerText: string;
onExport?: (rows: Row<AnalyticsTableDataMap[T]>[]) => void;
}
export const InsightTable = <T extends Exclude<TAnalyticsTabsBase, "overview">>(
props: InsightTableProps<T>
): React.ReactElement => {
const { data, isLoading, columns, headerText, onExport } = props;
const { t } = useTranslation();
if (isLoading) {
return <TableLoader columns={columns} rows={5} />;
}
return (
<div className="">
<DataTable
columns={columns}
data={data || []}
searchPlaceholder={`${data?.length || 0} ${headerText}`}
actions={(table: Table<AnalyticsTableDataMap[T]>) => (
<Button
variant="accent-primary"
prependIcon={<Download className="h-3.5 w-3.5" />}
onClick={() => onExport?.(table.getFilteredRowModel().rows)}
>
<div>{t("exporter.csv.short_description")}</div>
</Button>
)}
/>
</div>
);
};