next-fast-table
Version:
A super-fast form building solution for full-stack developers, build your data forms in one minute.
1 lines • 362 kB
Source Map (JSON)
{"version":3,"sources":["../src/DataTable.tsx","../src/helper.tsx","../src/TableBody.tsx","../src/index.tsx","#style-inject:#style-inject","../src/tailwind.css"],"sourcesContent":["import React, { useEffect, useMemo, useState } from \"react\";\nimport type {\n ColumnFiltersState,\n ColumnOrderState,\n ColumnPinningState,\n PaginationState,\n RowSelectionState,\n SortingState,\n VisibilityState,\n} from \"@tanstack/react-table\";\nimport {\n getCoreRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable,\n} from \"@tanstack/react-table\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { toast } from \"sonner\";\nimport { Icon } from \"@iconify/react\";\nimport { useMedia } from \"react-use\";\nimport { Controller, useForm } from \"react-hook-form\";\nimport { getLocalTimeZone, fromDate } from \"@internationalized/date\";\nimport { typedIcon } from \"./helper\";\nimport { MyTableBody } from \"./TableBody\";\nimport {\n useDisclosure,\n Modal,\n ModalBody,\n ModalContent,\n ModalFooter,\n ModalHeader,\n Button,\n Input,\n Textarea,\n Select,\n SelectItem,\n DatePicker,\n Pagination,\n Dropdown,\n DropdownItem,\n DropdownMenu,\n DropdownTrigger,\n} from \"@nextui-org/react\";\n\ntype DataWithID<T = Record<string, any>> = {\n id: number | string;\n} & Partial<T>;\n\ntype DataOnlyId<T = number | string> = {\n id: T;\n};\n\nexport type FetchParams = {\n pagination: PaginationState;\n columnFilters: ColumnFiltersState;\n sorting: SortingState;\n};\n\nexport type DeleteParams<T> = DataOnlyId<T> | DataOnlyId<T>[];\n\nexport type UpdateParams<T = Record<string, any>> = DataWithID<T>;\n\nexport type CreateParams<T = Record<string, any>> = DataWithID<T>;\n\n/**\n * Table configuration options.\n */\nexport interface TableConfig {\n /**\n * The name of the table, used to generate the key for tanstack-query. Defaults to 'next-table'.\n */\n name?: string;\n\n /**\n * The columns configuration for the table.\n */\n columns: any;\n\n /**\n * Function to fetch data for the table.\n * @param args - The fetch arguments including pagination, column filters, and sorting state.\n * @returns A promise that resolves with the total number of items and the list of data with ID.\n * @example\n * async function fetchData({ pagination, sorting, columnFilters }) {\n * const data = await fetchDataFromAPI({\n * pagination,\n * sorting,\n * columnFilters,\n * });\n * const total = await fetchTotalCount();\n * return {\n * list: data,\n * total,\n * };\n * }\n */\n onFetch: (\n args: FetchParams\n ) => Promise<{ total: number; list: DataWithID[] }>;\n\n /**\n * Function to delete data.\n * @param data - The data to be deleted, either a single ID or an array of IDs.\n * @returns A promise that resolves when the deletion is complete.\n * @example\n * async function deleteData(data) {\n * await deleteDataFromAPI(data);\n * }\n */\n onDelete?: (data: any) => Promise<void>;\n\n /**\n * Function to create new data.\n * @param data - The data to be created.\n * @returns A promise that resolves when the creation is complete.\n * @example\n * async function createData(data) {\n * const newData = await createDataInAPI(data);\n * return newData;\n * }\n */\n onCreate?: (data: any) => Promise<void>;\n\n /**\n * Function to update existing data.\n * @param data - The data to be updated. Only the ID and fields to be updated will be sent.\n * @returns A promise that resolves when the update is complete.\n * @example\n * async function updateData(data) {\n * const updatedData = await updateDataInAPI(data);\n * return updatedData;\n * }\n */\n onUpdate?: (data: any) => Promise<void>;\n}\nexport function DataTable({\n name = \"next-table\",\n columns,\n onFetch,\n onDelete,\n onCreate,\n onUpdate,\n}: TableConfig) {\n const initalHiddenColumns = columns\n .filter((col) => col?.meta?.list?.hidden)\n .map((item) => item.accessorKey)\n .reduce((acc, cur) => ({ ...acc, [cur]: false }), {});\n const [sorting, setSorting] = useState<SortingState>([]);\n const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);\n const [columnVisibility, setColumnVisibility] =\n useState<VisibilityState>(initalHiddenColumns);\n const [rowSelection, setRowSelection] = useState<RowSelectionState>({});\n const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([]);\n const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({});\n const [pagination, setPagination] = useState<PaginationState>({\n pageSize: 20,\n pageIndex: 0,\n });\n const [data, setData] = useState<DataWithID[]>([]);\n const [pageCount, setPageCount] = useState(1);\n const [total, setTotal] = useState(0);\n\n const table = useReactTable({\n pageCount,\n data,\n columns,\n manualPagination: true,\n manualSorting: true,\n manualFiltering: true,\n getCoreRowModel: getCoreRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n onColumnFiltersChange: setColumnFilters,\n onSortingChange: setSorting,\n onColumnVisibilityChange: setColumnVisibility,\n onRowSelectionChange: setRowSelection,\n onPaginationChange: setPagination,\n onColumnOrderChange: setColumnOrder,\n onColumnPinningChange: setColumnPinning,\n state: {\n sorting,\n columnFilters,\n columnVisibility,\n rowSelection,\n pagination,\n columnOrder,\n columnPinning,\n },\n });\n\n const isMobile = useMedia(\"(max-width: 768px)\", true);\n\n const getQuery = useQuery({\n queryKey: [name, { sorting, columnFilters, pagination }],\n queryFn: () => onFetch({ pagination, columnFilters, sorting }),\n });\n\n const deleteMutation = useMutation({\n mutationFn: onDelete,\n onSuccess: (data, variables) => {\n toast.success(\"Row deleted successfully\");\n getQuery.refetch();\n\n onClose();\n },\n onError: (err, variables) => {\n toast.error(\"Failed to delete rows\", { description: err.message });\n },\n });\n\n const updateMutation = useMutation({\n mutationFn: onUpdate,\n onSuccess: () => {\n toast.success(\"Row updated successfully\");\n getQuery.refetch();\n onClose();\n },\n onError: (e) => {\n toast.error(\"Failed to update row\", { description: e.message });\n },\n });\n\n const createMutation = useMutation({\n mutationFn: onCreate,\n onSuccess: (data, variables) => {\n toast.success(\"Row created successfully\");\n getQuery.refetch();\n onClose();\n },\n onError: (e) => {\n toast.error(\"Failed to create row\", { description: e.message });\n },\n });\n\n useEffect(() => {\n if (getQuery.isSuccess) {\n setPageCount(Math.ceil(getQuery.data.total / pagination.pageSize) ?? 1);\n setData((getQuery.data?.list as any) ?? []);\n setTotal(getQuery.data?.total ?? 0);\n }\n }, [getQuery.isSuccess, getQuery.data, pagination.pageSize]);\n\n const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure({\n onClose() {\n setMode(undefined);\n reset({});\n },\n });\n // const [targetRow, setTargetRow] = useState<any>({});\n const [mode, setMode] = useState<\n \"create\" | \"edit\" | \"filter\" | \"view\" | \"delete\" | undefined\n >(undefined);\n const {\n control,\n handleSubmit,\n formState: { dirtyFields, isDirty, defaultValues },\n register,\n reset,\n getValues,\n setValue,\n watch,\n } = useForm({});\n // it will slow down the input\n const [formData, setFormData] = useState<any>({});\n useEffect(() => {\n if (process.env.NODE_ENV === \"development\") {\n const formData = watch();\n setFormData(formData);\n }\n }, []);\n\n const onSubmit = (data: any) => {\n const dirtyData = Object.keys(dirtyFields).reduce((acc, cur) => {\n return { ...acc, [cur]: data[cur] };\n }, {});\n\n const dirtyDataWithPrimaryKey = {\n ...dirtyData,\n [primaryKey]: data[primaryKey],\n };\n\n console.log({ dirtyDataWithPrimaryKey, dirtyData, data, primaryKey });\n\n if (mode === \"create\") {\n createMutation.mutate(dirtyData as any);\n } else if (mode === \"edit\") {\n updateMutation.mutate(dirtyDataWithPrimaryKey as any);\n } else if (mode === \"filter\") {\n const arr = Object.entries(dirtyData)\n .filter(([key, value]) => value !== undefined && value !== \"\")\n .map(([key, value]) => ({ id: key, value }));\n\n setColumnFilters(arr);\n onClose();\n } else if (mode === \"view\") {\n navigator.clipboard.writeText(JSON.stringify(data));\n toast.success(\"Copied to clipboard\");\n onClose();\n } else if (mode === \"delete\") {\n deleteMutation.mutate({ [primaryKey]: data[primaryKey] });\n } else {\n console.log(\"No mode selected\");\n toast.error(\"No mode selected\");\n }\n };\n\n const visibleColumnIds = table\n .getVisibleFlatColumns()\n .map((column) => column.id);\n const isFilterDirty =\n table\n .getState()\n .columnFilters.filter(\n (item) => item.id && item.value !== undefined && item.value !== \"\"\n ).length > 0;\n\n const onCreateButtonClick = () => {\n setMode(\"create\");\n reset({});\n onOpen();\n };\n\n const onResetButtonClick = () => {\n table.resetColumnFilters();\n onClose();\n };\n\n const onDeleteButtonClick = () => {\n const rows = table.getSelectedRowModel().rows;\n const items = rows.map((row) => ({\n id: row.original.id,\n }));\n table.resetRowSelection();\n deleteMutation.mutate(items as any);\n };\n\n // const onTableSelectionChange = (value) => {\n // if (value === \"all\") return table.toggleAllRowsSelected();\n\n // table.setRowSelection(\n // Array.from(value).reduce((acc, cur) => ({ ...acc, [+cur]: true }), {})\n // );\n // };\n\n const isCreateOrEditMode = mode === \"create\" || mode === \"edit\";\n\n const primaryKey = \"id\";\n\n const inputDefaultValue = (key: string): any | undefined => {\n if (mode === \"edit\" || mode === \"view\") {\n // return targetRow[key];\n return getValues(key);\n } else if (mode === \"create\") {\n return undefined;\n } else if (mode === \"filter\") {\n const filter = table\n .getState()\n .columnFilters.find((item) => item.id === key);\n if (filter !== undefined) {\n return filter.value;\n }\n }\n return undefined;\n };\n\n const iconClasses =\n \"text-xl text-default-500 pointer-events-none flex-shrink-0\";\n\n const memoizedTable = useMemo(\n () => (\n <MyTableBody\n table={table}\n getQuery={getQuery}\n onOpen={onOpen}\n reset={reset}\n setMode={setMode}\n hideEdit={onUpdate === undefined}\n hideDelete={onDelete === undefined}\n />\n ),\n [table, isMobile, getQuery]\n );\n\n return (\n <div id=\"container\" className=\"space-y-2 p-2 flex flex-col h-full gap-2\">\n {/* <div\n id=\"debugger\"\n className=\" w-1/5 p-2 rounded-xl bg-black/50 pointer-events-none top-20 absolute z-[999999]\"\n >\n {Object.entries({\n defaultValues,\n formData,\n isDirty,\n dirtyFields,\n mode,\n columnFilters,\n }).map(([key, value]) => (\n <div key={key} className=\"flex gap-2\">\n <span>{key}</span>\n <pre>{JSON.stringify(value, null, 2)}</pre>\n </div>\n ))}\n </div> */}\n <Modal\n id=\"modal\"\n isOpen={isOpen}\n placement=\"bottom-center\"\n size=\"3xl\"\n scrollBehavior=\"inside\"\n onOpenChange={onOpenChange}\n onClose={onClose}\n backdrop=\"blur\"\n isDismissable={false}\n >\n <ModalContent>\n <ModalHeader>\n <span>{mode}</span>\n </ModalHeader>\n <ModalBody>\n <form id=\"addDataForm\" onSubmit={handleSubmit(onSubmit)}>\n {columns.map((column) => (\n <div key={column.accessorKey} className=\"mb-2\">\n <div>\n {[\"string\", \"number\", \"longtext\"].includes(\n column.meta?.type\n ) && (\n <Input\n {...register(column.accessorKey, {\n setValueAs(value) {\n const type = column.meta?.type;\n if (\n typeof value === \"string\" &&\n value?.trim() === \"\"\n ) {\n return undefined;\n }\n if (type === \"number\") return Number(value);\n if (type === \"string\" || type === \"longtext\")\n return String(value);\n },\n })}\n defaultValue={inputDefaultValue(column.accessorKey)}\n type={\n column.meta?.type === \"longtext\" ? \"textarea\" : \"text\"\n }\n onClick={() => {\n if (mode !== \"view\") return;\n navigator.clipboard.writeText(\n getValues(column.accessorKey)\n );\n toast.success(\"Copied to clipboard\");\n }}\n className={column.enableColumnFilter ? \"\" : \"hidden\"}\n endContent={typedIcon(column.meta?.type)}\n label={column.header}\n isReadOnly={mode === \"view\"}\n isDisabled={\n column.meta?.input?.disabled && isCreateOrEditMode\n }\n isRequired={\n column.meta?.input?.required && mode !== \"filter\"\n }\n />\n )}\n\n {column.meta?.type === \"boolean\" && (\n <Controller\n name={column.accessorKey}\n control={control}\n render={({ field }) => (\n <Select\n label={column.header}\n isDisabled={mode === \"view\" || mode === \"delete\"}\n defaultSelectedKeys={[\n inputDefaultValue(column.accessorKey)?.toString(),\n ]}\n onChange={(\n e: React.ChangeEvent<HTMLSelectElement>\n ) => {\n const select = e.target.value;\n if (select === \"true\") {\n return field.onChange(true);\n }\n if (select === \"false\") {\n return field.onChange(false);\n }\n return field.onChange(undefined);\n }}\n >\n {[\"false\", \"true\"].map((str) => (\n <SelectItem key={str} value={str}>\n {str}\n </SelectItem>\n ))}\n </Select>\n )}\n />\n )}\n {column.meta?.type === \"date\" && (\n <Controller\n control={control}\n name={column.accessorKey}\n render={({ field }) => (\n <DatePicker\n granularity=\"second\"\n label={column.header}\n value={\n field.value\n ? fromDate(field.value, getLocalTimeZone())\n : undefined\n }\n onChange={(date) => {\n field.onChange(date.toDate());\n }}\n isDisabled={column.meta?.edit?.disabled}\n isRequired={\n column.meta?.edit?.required && isCreateOrEditMode\n }\n />\n )}\n />\n )}\n {column.meta?.type === \"enum\" && (\n <Controller\n name={column.accessorKey}\n control={control}\n render={({ field }) => (\n <Select\n label={column.header}\n isDisabled={mode === \"view\" || mode === \"delete\"}\n defaultSelectedKeys={[field.value]}\n onChange={(\n e: React.ChangeEvent<HTMLSelectElement>\n ) => field.onChange(e.target.value)}\n >\n {column.meta?.enum?.map((value) => (\n <SelectItem key={value} value={value}>\n {value}\n </SelectItem>\n ))}\n </Select>\n )}\n />\n )}\n {[\"array\", \"json\"].includes(column.meta?.type) && (\n <Controller\n name={column.accessorKey}\n control={control}\n render={({ field }) => (\n <Textarea\n isInvalid={\n typeof field.value !== \"object\" &&\n !Array.isArray(field.value) &&\n field.value !== undefined\n }\n errorMessage=\"Invalid JSON or ARRAY\"\n endContent={typedIcon(column.meta?.type)}\n defaultValue={JSON.stringify(field.value, null, 2)}\n type=\"text\"\n onChange={(e) => {\n try {\n field.onChange(JSON.parse(e.target.value));\n } catch (error) {\n field.onChange(e.target.value);\n }\n }}\n onClick={() => {\n if (mode !== \"view\") return;\n navigator.clipboard.writeText(\n JSON.stringify(\n getValues(column.accessorKey),\n null,\n 2\n )\n );\n toast.success(\"Copied to clipboard\");\n }}\n className={\n column.enableColumnFilter ? \"\" : \"hidden\"\n }\n label={column.header}\n isReadOnly={mode === \"view\"}\n isDisabled={\n column.meta?.input?.disabled && isCreateOrEditMode\n }\n isRequired={\n column.meta?.input?.required && mode !== \"filter\"\n }\n />\n )}\n />\n )}\n </div>\n </div>\n ))}\n </form>\n </ModalBody>\n <ModalFooter>\n <Button\n variant=\"ghost\"\n onPress={mode === \"filter\" ? onResetButtonClick : onClose}\n >\n {mode === \"filter\" ? \"Reset\" : \"Cancel\"}\n </Button>\n <Button\n form=\"addDataForm\"\n isDisabled={!isDirty && isCreateOrEditMode}\n type=\"submit\"\n isLoading={\n updateMutation.isPending ||\n createMutation.isPending ||\n deleteMutation.isPending\n }\n color={mode === \"delete\" ? \"danger\" : \"primary\"}\n >\n <p className=\"first-letter:uppercase\">\n {mode && mode === \"view\" ? \"Copy\" : mode}\n </p>\n </Button>\n </ModalFooter>\n </ModalContent>\n </Modal>\n <header\n id=\"controls\"\n className=\"flex gap-2 flex-wrap flex-shrink-0 w-full \"\n >\n <Button\n color=\"primary\"\n variant=\"solid\"\n className=\" flex-shrink-0\"\n size={isMobile ? \"lg\" : undefined}\n isIconOnly={isMobile}\n isDisabled={getQuery.isRefetching}\n onClick={() => getQuery.refetch()}\n startContent={<Icon icon=\"material-symbols:refresh-rounded\" />}\n >\n {isMobile ? undefined : \"Refresh\"}\n </Button>\n\n <Dropdown backdrop=\"blur\">\n <DropdownTrigger>\n <Button\n variant=\"solid\"\n isIconOnly={isMobile}\n color=\"primary\"\n size={isMobile ? \"lg\" : undefined}\n className=\" flex-shrink-0\"\n startContent={<Icon icon=\"material-symbols:view-column\" />}\n >\n {isMobile ? undefined : \"Columns\"}\n </Button>\n </DropdownTrigger>\n <DropdownMenu\n aria-label=\"Multiple selection\"\n variant=\"flat\"\n color=\"primary\"\n disallowEmptySelection\n closeOnSelect={false}\n selectionMode=\"multiple\"\n selectedKeys={visibleColumnIds}\n >\n {table\n .getAllColumns()\n .filter(\n (column) =>\n typeof column.accessorFn !== \"undefined\" &&\n column.getCanHide()\n )\n .map((column: any) => {\n return (\n <DropdownItem\n onClick={() => column.toggleVisibility()}\n key={column.id}\n >\n <div className=\" flex space-x-2 items-center\">\n {typedIcon(column.columnDef.meta?.type)}\n <div className=\" flex items-center gap-2\">\n {column.columnDef.header}\n </div>\n </div>\n </DropdownItem>\n );\n })}\n </DropdownMenu>\n </Dropdown>\n <Button\n onClick={() => {\n setMode(\"filter\");\n // setTargetRow({});\n reset({});\n onOpen();\n }}\n size={isMobile ? \"lg\" : undefined}\n isIconOnly={isMobile}\n className=\" flex-shrink-0 mr-auto\"\n color=\"primary\"\n variant={isFilterDirty ? \"ghost\" : \"solid\"}\n startContent={<Icon icon=\"material-symbols:filter-alt\" />}\n >\n {isMobile ? undefined : \"Filter\"}\n </Button>\n\n <div className=\" flex-grow\"></div>\n\n {onDelete && (\n <Button\n className=\" flex-shrink-0 ml-auto\"\n color=\"danger\"\n size={isMobile ? \"lg\" : undefined}\n isIconOnly={isMobile}\n isLoading={deleteMutation.isPending}\n variant=\"solid\"\n startContent={<Icon icon=\"material-symbols:delete\" />}\n onClick={onDeleteButtonClick}\n isDisabled={table.getSelectedRowModel().rows.length === 0 || false}\n >\n {isMobile ? undefined : \"Delete\"}\n </Button>\n )}\n {onCreate && (\n <Button\n color=\"primary\"\n className=\" flex-shrink-0\"\n isIconOnly={isMobile}\n size={isMobile ? \"lg\" : undefined}\n variant=\"solid\"\n startContent={<Icon icon=\"material-symbols:add\" />}\n onClick={onCreateButtonClick}\n >\n {isMobile ? undefined : \"Create\"}\n </Button>\n )}\n </header>\n\n <main id=\"table\" className=\" overflow-scroll scrollbar-hide j \">\n {memoizedTable}\n </main>\n\n <footer\n id=\"pagination\"\n className=\" flex justify-between w-full items-center\"\n >\n <Pagination\n showControls\n siblings={isMobile ? 0 : 3}\n size={isMobile ? \"md\" : undefined}\n variant=\"flat\"\n total={table.getPageCount()}\n initialPage={1}\n page={table.getState().pagination.pageIndex + 1}\n onChange={(page) => {\n table.setPageIndex(page - 1);\n }}\n />\n {/* <div id=\"tips\" className=\"\">\n <h1>{`${table.getSelectedRowModel().rows.length} of ${\n table.getState().pagination.pageSize\n } selected`}</h1>\n <h1>Total {total} rows</h1>\n </div> */}\n <Dropdown backdrop=\"blur\">\n <DropdownTrigger>\n <Button variant=\"flat\" className=\"\">\n {table.getState().pagination.pageSize} of {total}\n </Button>\n </DropdownTrigger>\n <DropdownMenu\n aria-label=\"pageSize\"\n variant=\"solid\"\n color=\"primary\"\n disallowEmptySelection\n selectionMode=\"single\"\n selectedKeys={[table.getState().pagination.pageSize]}\n >\n {[10, 20, 50, 100, 200].map((pageSize) => {\n return (\n <DropdownItem\n key={pageSize}\n onClick={() => table.setPageSize(pageSize)}\n >\n {pageSize}\n </DropdownItem>\n );\n })}\n </DropdownMenu>\n </Dropdown>\n </footer>\n </div>\n );\n}\n","import { Icon } from \"@iconify/react\";\nimport React from \"react\";\nimport { Button, Chip, Spacer, Tooltip, Image, Link } from \"@nextui-org/react\";\nimport JSONPretty from \"react-json-pretty\";\nimport \"react-json-pretty/themes/monikai.css\";\nimport { UAParser } from \"ua-parser-js\";\n\nexport function parseDateRange(obj: any) {\n try {\n // 解析 JSON 字符串为对象\n const dateRange = obj;\n\n // 提取 start 和 end 日期对象\n const start = dateRange.start;\n const end = dateRange.end;\n\n // 将 JSON 日期对象转换为 JavaScript Date 对象\n const fromDate = new Date(\n start.year,\n start.month - 1, // month is 0-indexed in JavaScript Date\n start.day\n );\n const toDate = new Date(\n end.year,\n end.month - 1, // month is 0-indexed in JavaScript Date\n end.day\n );\n\n // 返回结果对象\n return {\n from: fromDate,\n to: toDate,\n };\n } catch (error) {\n console.error(\"Invalid JSON string:\", error);\n return {\n from: new Date(),\n to: new Date(),\n };\n }\n}\nexport function typedIcon(type: string) {\n const iconType = iconMap[type] || \"default-icon\";\n return <Icon icon={iconType} />;\n}\nconst iconMap = {\n string: \"material-symbols:text-fields-rounded\",\n number: \"f7:number\",\n date: \"material-symbols:date-range\",\n boolean: \"oui:token-boolean\",\n array: \"material-symbols:list-alt\",\n json: \"material-symbols:code\",\n longtext: \"material-symbols:article\",\n enum: \"material-symbols:label\",\n};\n\nfunction typedCell(type: string, cell: any) {\n if (type === \"number\") {\n return cell ? <span>{+cell.toString()}</span> : <span>N/A</span>;\n }\n if (type === \"date\") {\n return cell ? (\n <span className=\"\">{new Date(cell).toLocaleDateString()}</span>\n ) : (\n <span>N/A</span>\n );\n }\n\n if (type === \"boolean\") {\n return (\n <span>\n {cell ? (\n <Icon\n icon=\"material-symbols:check-rounded\"\n className=\" text-xl text-success\"\n />\n ) : (\n <Icon\n icon=\"material-symbols:close-rounded\"\n className=\" text-xl text-danger\"\n />\n )}\n </span>\n );\n }\n\n if (type === \"array\" || type === \"json\") {\n return (\n <Tooltip\n content={\n <code className=\" p-2\">\n <JSONPretty\n mainStyle=\"background:transparent\"\n data={cell}\n ></JSONPretty>\n </code>\n }\n >\n <Button size=\"sm\" variant=\"light\">\n {Array.isArray(cell) ? `Array(${cell.length})` : `Object`}\n </Button>\n </Tooltip>\n );\n }\n\n if (type === \"enum\") {\n const specialStates = {\n success: [\n \"succeeded\",\n \"successed\",\n \"done\",\n \"completed\",\n \"finish\",\n \"finished\",\n \"success\",\n \"pass\",\n \"passed\",\n \"approve\",\n \"approved\",\n \"accept\",\n \"accepted\",\n ],\n warning: [\n \"waiting\",\n \"warning\",\n \"warn\",\n \"pending\",\n \"hold\",\n \"holded\",\n \"holdup\",\n \"holduped\",\n \"delay\",\n \"delayed\",\n \"process\",\n \"processing\",\n ],\n danger: [\n \"fail\",\n \"failed\",\n \"failure\",\n \"canceled\",\n \"cancelled\",\n \"reject\",\n \"rejected\",\n \"deny\",\n \"denied\",\n \"refuse\",\n \"refused\",\n \"block\",\n \"blocked\",\n \"stop\",\n \"stopped\",\n \"halt\",\n \"halted\",\n \"pause\",\n \"paused\",\n \"suspend\",\n \"suspended\",\n \"inactive\",\n \"invalid\",\n \"illegal\",\n \"unauthorized\",\n \"forbidden\",\n \"notallowed\",\n ],\n };\n\n let color = \"default\";\n for (const state in specialStates) {\n if (specialStates[state].includes(cell)) {\n color = state;\n break;\n }\n }\n\n return (\n <Chip variant=\"flat\" color={color as any}>\n {cell}\n </Chip>\n );\n }\n\n return (\n <div className=\" max-w-xl text-wrap line-clamp-3 overflow-hidden\">\n {cell}\n </div>\n );\n}\n\n/**\n * Configuration options for helper functions.\n */\ninterface HelperConfig {\n /**\n * Configuration for the input state.\n */\n input?: {\n /**\n * Whether the input is disabled in edit mode, including both creation and editing.\n */\n disabled?: boolean;\n /**\n * Whether the input is required in edit mode, participating in form validation.\n */\n required?: boolean;\n };\n /**\n * Configuration for the display state.\n */\n list?: {\n /**\n * Whether the column is hidden by default. If true, it will not be displayed by default, but can be shown through column settings.\n */\n hidden?: boolean;\n };\n /**\n * The label or alias for the column.\n */\n label?: string;\n /**\n * Whether hiding is allowed. If false, the hide button will not be displayed.\n */\n enableHiding?: boolean;\n /**\n * Whether sorting is allowed. If false, the sort button will not be displayed.\n */\n enableSorting?: boolean;\n /**\n * Whether the column participates in column filtering. If false, it will not be displayed in the column filter.\n */\n enableColumnFilter?: boolean;\n /**\n * Enumeration values, only effective when using `field.enum`.\n */\n enum?: string[];\n /**\n * Custom render function for custom rendering in display state.\n * @param cell - The cell value.\n * @param row - The row data.\n * @returns A JSX element or string for rendering.\n */\n render?: ({ cell, row }: { cell: any; row: any }) => JSX.Element | string;\n}\n\nfunction helper(metaType: string, hconfig = {}) {\n /**\n * @param key - Column identifier used to access data\n * @example key: \"id\" \"amount\" \"profile.name\" \"a.b.c.d.e\"\n * @param config - Configuration options\n * @example config: { label: \"UID\", input: { required: true }, render: ({ cell, row }) => <div>{cell}</div> }\n */\n return (key: string, config: HelperConfig = hconfig) => {\n return {\n meta: {\n type: metaType,\n input: {\n disabled: config.input?.disabled ?? false,\n required: config.input?.required ?? false,\n },\n list: {\n hidden: config.list?.hidden ?? false,\n },\n enum: config.enum ?? [],\n },\n accessorKey: key,\n header: config.label ?? key,\n enableHiding: config.enableHiding ?? true,\n enableSorting: config.enableSorting ?? true,\n enableColumnFilter: config.enableColumnFilter ?? true,\n cell: ({ cell, row }) => {\n return config.render\n ? config.render({ cell: cell.getValue(), row: row.original })\n : typedCell(metaType, cell.getValue());\n },\n };\n };\n}\nconst uaParser = new UAParser();\n\nexport const Fields = {\n string: helper(\"string\"),\n number: helper(\"number\"),\n boolean: helper(\"boolean\"),\n date: helper(\"date\"),\n array: helper(\"array\"),\n json: helper(\"json\"),\n longtext: helper(\"longtext\"),\n enum: helper(\"enum\"),\n ua: helper(\"string\", {\n render: ({ cell }) => {\n const parser = new UAParser();\n parser.setUA(cell);\n const result = parser.getResult();\n\n const browserIcon =\n {\n Chrome: \"teenyicons:chrome-solid\",\n Firefox: \"ri:firefox-fill\",\n Safari: \"fa6-brands:safari\",\n \"Mobile Safari\": \"fa6-brands:safari\",\n Edge: \"mdi:microsoft-edge\",\n Opera: \"mdi:opera\",\n \"Internet Explorer\": \"mdi:internet-explorer\",\n }[result.browser.name as any] || \"mdi:web\";\n\n const osIcon =\n {\n Windows: \"mdi:microsoft-windows\",\n \"Mac OS\": \"mdi:apple\",\n iOS: \"mdi:apple\",\n Android: \"mdi:android\",\n Linux: \"mdi:linux\",\n Ubuntu: \"cib:ubuntu\",\n }[result.os.name as any] || \"mingcute:device-fill\";\n\n return (\n <div className=\"flex \">\n <Chip\n startContent={<Icon icon={osIcon}></Icon>}\n color=\"primary\"\n variant=\"flat\"\n >\n {result.os.name} {result.os.version ? result.os.version : \"\"}\n </Chip>\n <Spacer x={1}></Spacer>\n <Chip\n startContent={<Icon icon={browserIcon}></Icon>}\n color=\"secondary\"\n variant=\"flat\"\n >\n {result.browser.name}{\" \"}\n {result.browser.version ? result.browser.version : \"\"}\n </Chip>\n </div>\n );\n },\n }),\n image: helper(\"string\", {\n render: ({ cell }) => (\n <div className=\"flex justify-center max-w-[50px] aspect-square rounded-xl overflow-hidden \">\n <Image\n src={cell}\n alt=\"image\"\n width={50}\n isZoomed\n height={50}\n className=\"rounded-xl\"\n onClick={() => {\n // open image in new tab\n window.open(cell, \"_blank\");\n }}\n />\n </div>\n ),\n }),\n email: helper(\"string\", {\n render: ({ cell }) => (\n <Link showAnchorIcon href={`mailto:${cell}`}>\n {cell}\n </Link>\n ),\n }),\n tag: helper(\"string\", {\n render: ({ cell }) => (\n <span className=\"inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-600\">\n {cell}\n </span>\n ),\n }),\n link: helper(\"string\", {\n render: ({ cell }) => (\n <Link\n showAnchorIcon\n className=\" max-w-sm overflow-hidden text-ellipsis line-clamp-2\"\n href={cell}\n >\n {cell}\n </Link>\n ),\n }),\n ip: helper(\"string\", {\n render: ({ cell }) => <Chip variant=\"bordered\">{cell}</Chip>,\n }),\n};\n","import React from \"react\";\nimport {\n cn,\n Table,\n TableBody,\n TableCell,\n TableHeader,\n TableRow,\n TableColumn,\n Spinner,\n Button,\n Dropdown,\n DropdownTrigger,\n DropdownMenu,\n DropdownItem,\n} from \"@nextui-org/react\";\nimport { flexRender } from \"@tanstack/react-table\";\nimport { Icon } from \"@iconify/react\";\n\nexport function MyTableBody({\n table,\n getQuery,\n setMode,\n reset,\n onOpen,\n hideDelete,\n hideEdit,\n}) {\n const iconClasses =\n \"text-xl text-default-500 pointer-events-none flex-shrink-0\";\n\n return (\n <Table\n color=\"primary\"\n selectionMode=\"multiple\"\n isStriped\n isHeaderSticky\n isVirtualized\n onSelectionChange={(value) => {\n if (value === \"all\") return table.toggleAllRowsSelected();\n table.setRowSelection(\n Array.from(value).reduce((acc, cur) => ({ ...acc, [+cur]: true }), {})\n );\n }}\n aria-label=\"data-table\"\n selectedKeys={table.getSelectedRowModel().rows.map((row) => row.id)}\n sortDescriptor={{\n column: table.getState().sorting[0]?.id,\n direction: table.getState().sorting[0]?.desc\n ? \"descending\"\n : \"ascending\",\n }}\n onSortChange={({ column, direction }) => {\n table.getColumn(column as string)?.toggleSorting();\n }}\n >\n <TableHeader>\n {table.getHeaderGroups()[0].headers.map((header) => (\n <TableColumn\n key={header.id}\n allowsSorting={header.column.getCanSort()}\n allowsResizing={header.column.getCanResize()}\n >\n {header.isPlaceholder\n ? null\n : flexRender(header.column.columnDef.header, header.getContext())}\n </TableColumn>\n ))}\n <TableColumn key=\"actions\">\n <div>actions</div>\n </TableColumn>\n </TableHeader>\n <TableBody\n emptyContent={\"No rows to display.\"}\n isLoading={getQuery.isPending}\n loadingContent={<Spinner />}\n items={table.getRowModel().rows}\n >\n {table.getRowModel().rows.map((row) => (\n <TableRow key={row.id}>\n {row.getVisibleCells().map((cell) => (\n <TableCell key={cell.id}>\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n </TableCell>\n ))}\n <TableCell id=\"actions-cell\">\n <Dropdown>\n <DropdownTrigger>\n <Button variant=\"flat\" color=\"primary\">\n Actions\n </Button>\n </DropdownTrigger>\n <DropdownMenu\n variant=\"faded\"\n aria-label=\"Dropdown menu with icons\"\n >\n {!hideEdit &&\n ((\n <DropdownItem\n key=\"edit\"\n color=\"primary\"\n variant=\"flat\"\n startContent={\n <Icon\n icon=\"material-symbols:edit-square\"\n className={iconClasses}\n />\n }\n onPress={() => {\n setMode(\"edit\");\n reset(row.original as any);\n onOpen();\n }}\n >\n Edit\n </DropdownItem>\n ) as any)}\n <DropdownItem\n key=\"view\"\n color=\"primary\"\n variant=\"flat\"\n startContent={\n <Icon icon=\"solar:eye-bold\" className={iconClasses} />\n }\n onClick={() => {\n setMode(\"view\");\n reset(row.original as any);\n onOpen();\n }}\n >\n View\n </DropdownItem>\n {!hideDelete && (\n <DropdownItem\n key=\"delete\"\n className=\"text-danger\"\n variant=\"flat\"\n color=\"danger\"\n startContent={\n <Icon\n icon=\"carbon:delete\"\n className={cn(iconClasses, \"text-danger\")}\n />\n }\n onClick={() => {\n setMode(\"delete\");\n reset(row.original as any);\n onOpen();\n }}\n >\n Delete\n </DropdownItem>\n )}\n </DropdownMenu>\n </Dropdown>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n );\n}\n","import {\n DataTable,\n TableConfig,\n UpdateParams,\n FetchParams,\n CreateParams,\n DeleteParams,\n} from \"./DataTable\";\nimport { QueryClientProvider, QueryClient } from \"@tanstack/react-query\";\nimport React from \"react\";\nimport \"./tailwind.css\";\nimport { Fields } from \"./helper\";\nimport { Toaster } from \"sonner\";\n\nconst queryClient = new QueryClient();\n\nfunction NextFastTable(props: TableConfig) {\n return (\n <QueryClientProvider client={queryClient}>\n <Toaster richColors position=\"top-center\" />\n <DataTable {...props} />\n </QueryClientProvider>\n );\n}\nexport {\n NextFastTable,\n Fields,\n type FetchParams,\n type CreateParams,\n type UpdateParams,\n type DeleteParams,\n};\n","\n export default function styleInject(css, { insertAt } = {}) {\n if (!css || typeof document === 'undefined') return\n \n const head = document.head || document.getElementsByTagName('head')[0]\n const style = document.createElement('style')\n style.type = 'text/css'\n \n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild)\n } else {\n head.appendChild(style)\n }\n } else {\n head.appendChild(style)\n }\n \n if (style.styleSheet) {\n style.styleSheet.cssText = css\n } else {\n style.appendChild(document.createTextNode(css))\n }\n }\n ","import styleInject from '#style-inject';styleInject(\"*,\\n::before,\\n::after {\\n box-sizing: border-box;\\n border-width: 0;\\n border-style: solid;\\n border-color: #e5e7eb;\\n}\\n::before,\\n::after {\\n --tw-content: \\\"\\\";\\n}\\nhtml,\\n:host {\\n line-height: 1.5;\\n -webkit-text-size-adjust: 100%;\\n -moz-tab-size: 4;\\n -o-tab-size: 4;\\n tab-size: 4;\\n font-family:\\n ui-sans-serif,\\n system-ui,\\n sans-serif,\\n \\\"Apple Color Emoji\\\",\\n \\\"Segoe UI Emoji\\\",\\n \\\"Segoe UI Symbol\\\",\\n \\\"Noto Color Emoji\\\";\\n font-feature-settings: normal;\\n font-variation-settings: normal;\\n -webkit-tap-highlight-color: transparent;\\n}\\nbody {\\n margin: 0;\\n line-height: inherit;\\n}\\nhr {\\n height: 0;\\n color: inherit;\\n border-top-width: 1px;\\n}\\nabbr:where([title]) {\\n -webkit-text-decoration: underline dotted;\\n text-decoration: underline dotted;\\n}\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6 {\\n font-size: inherit;\\n font-weight: inherit;\\n}\\na {\\n color: inherit;\\n text-decoration: inherit;\\n}\\nb,\\nstrong {\\n font-weight: bolder;\\n}\\ncode,\\nkbd,\\nsamp,\\npre {\\n font-family:\\n ui-monospace,\\n SFMono-Regular,\\n Menlo,\\n Monaco,\\n Consolas,\\n \\\"Liberation Mono\\\",\\n \\\"Courier New\\\",\\n monospace;\\n font-feature-settings: normal;\\n font-variation-settings: normal;\\n font-size: 1em;\\n}\\nsmall {\\n font-size: 80%;\\n}\\nsub,\\nsup {\\n font-size: 75%;\\n line-height: 0;\\n position: relative;\\n vertical-align: baseline;\\n}\\nsub {\\n bottom: -0.25em;\\n}\\nsup {\\n top: -0.5em;\\n}\\ntable {\\n text-indent: 0;\\n border-color: inherit;\\n border-collapse: collapse;\\n}\\nbutton,\\ninput,\\noptgroup,\\nselect,\\ntextarea {\\n font-family: inherit;\\n font-feature-settings: inherit;\\n font-variation-settings: inherit;\\n font-size: 100%;\\n font-weight: inherit;\\n line-height: inherit;\\n letter-spacing: inherit;\\n color: inherit;\\n margin: 0;\\n padding: 0;\\n}\\nbutton,\\nselect {\\n text-transform: none;\\n}\\nbutton,\\ninput:where([type=\\\"button\\\"]),\\ninput:where([type=\\\"reset\\\"]),\\ninput:where([type=\\\"submit\\\"]) {\\n -webkit-appearance: button;\\n background-color: transparent;\\n background-image: none;\\n}\\n:-moz-focusring {\\n outline: auto;\\n}\\n:-moz-ui-invalid {\\n box-shadow: none;\\n}\\nprogress {\\n vertical-align: baseline;\\n}\\n::-webkit-inner-spin-button,\\n::-webkit-outer-spin-button {\\n height: auto;\\n}\\n[type=search] {\\n -webkit-appearance: textfield;\\n outline-offset: -2px;\\n}\\n::-webkit-search-decoration {\\n -webkit-appearance: none;\\n}\\n::-webkit-file-upload-button {\\n -webkit-appearance: button;\\n font: inherit;\\n}\\nsummary {\\n display: list-item;\\n}\\nblockquote,\\ndl,\\ndd,\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6,\\nhr,\\nfigure,\\np,\\npre {\\n margin: 0;\\n}\\nfieldset {\\n margin: 0;\\n padding: 0;\\n}\\nlegend {\\n padding: 0;\\n}\\nol,\\nul,\\nmenu {\\n list-style: none;\\n margin: 0;\\n padding: 0;\\n}\\ndialog {\\n padding: 0;\\n}\\ntextarea {\\n resize: vertical;\\n}\\ninput::-moz-placeholder,\\ntextarea::-moz-placeholder {\\n opacity: 1;\\n color: #9ca3af;\\n}\\ninput::placeholder,\\ntextarea::placeholder {\\n opacity: 1;\\n color: #9ca3af;\\n}\\nbutton,\\n[role=button] {\\n cursor: pointer;\\n}\\n:disabled {\\n cursor: default;\\n}\\nimg,\\nsvg,\\nvideo,\\ncanvas,\\naudio,\\niframe,\\nembed,\\nobject {\\n display: block;\\n vertical-align: middle;\\n}\\nimg,\\nvideo {\\n max-width: 100%;\\n height: auto;\\n}\\n[hidden] {\\n display: none;\\n}\\n:root,\\n[data-theme] {\\n color: hsl(var(--nextui-foreground));\\n background-color: hsl(var(--nextui-background));\\n}\\n*,\\n::before,\\n::after {\\n --tw-border-spacing-x: 0;\\n --tw-border-spacing-y: 0;\\n --tw-translate-x: 0;\\n --tw-translate-y: 0;\\n --tw-rotate: 0;\\n --tw-skew-x: 0;\\n --tw-skew-y: 0;\\n --tw-scale-x: 1;\\n --tw-scale-y: 1;\\n --tw-pan-x: ;\\n --tw-pan-y: ;\\n --tw-pinch-zoom: ;\\n --tw-scroll-snap-strictness: proximity;\\n --tw-gradient-from-position: ;\\n --tw-gradient-via-position: ;\\n --tw-gradient-to-position: ;\\n --tw-ordinal: ;\\n --tw-slashed-zero: ;\\n --tw-numeric-figure: ;\\n --tw-numeric-spacing: ;\\n --tw-numeric-fraction: ;\\n --tw-ring-inset: ;\\n --tw-ring-offset-width: 0px;\\n --tw-ring-offset-color: #fff;\\n --tw-ring-color: rgb(59 130 246 / 0.5);\\n --tw-ring-offset-shadow: 0 0 #0000;\\n --tw-ring-shadow: 0 0 #0000;\\n --tw-shadow: 0 0 #0000;\\n --tw-shadow-colored: 0 0 #0000;\\n --tw-blur: ;\\n --tw-brightness: ;\\n --tw-contrast: ;\\n --tw-grayscale: ;\\n --tw-hue-rotate: ;\\n --tw-invert: ;\\n --tw-saturate: ;\\n --tw-sepia: ;\\n --tw-drop-shadow: ;\\n --tw-backdrop-blur: ;\\n --tw-backdrop-brightness: ;\\n --tw-backdrop-contrast: ;\\n --tw-backdrop-grayscale: ;\\n --tw-backdrop-hue-rotate: ;\\n --tw-backdrop-invert: ;\\n --tw-backdrop-opacity: ;\\n --tw-backdrop-saturate: ;\\n --tw-backdrop-sepia: ;\\n --tw-contain-size: ;\\n --tw-contain-layout: ;\\n --tw-contain-paint: ;\\n --tw-contain-style: ;\\n}\\n::backdrop {\\n --tw-border-spacing-x: 0;\\n --tw-border-spacing-y: 0;\\n --tw-translate-x: 0;\\n --tw-translate-y: 0;\\n --tw-rotate: 0;\\n --tw-skew-x: 0;\\n --tw-skew-y: 0;\\n --tw-scale-x: 1;\\n --tw-scale-y: 1;\\n --tw-pan-x: ;\\n --tw-pan-y: ;\\n --tw-pinch-zoom: ;\\n --tw-scroll-snap-strictness: proximity;\\n --tw-gradient-from-position: ;\\n --tw-gradient-via-position: ;\\n --tw-gradient-to-position: ;\\n --tw-ordinal: ;\\n --tw-slashed-zero: ;\\n --tw-numeric-figure: ;\\n --tw-numeric-spacing: ;\\n --tw-numeric