diff --git a/README.md b/README.md index 1a3ec4d..571687a 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,229 @@ # MobX RESTful Shadcn -A **Pagination Table** & **Scroll List** component suite for [CRUD operation][1], which is based on [MobX RESTful][2] & [React][3]. +A **Pagination Table** & **Scroll List** component suite for [CRUD operation][1], which is based on [MobX RESTful][2], [React][3] & [Shadcn UI][4]. -You can use the `shadcn` CLI to run your own component registry. Running your own -component registry allows you to distribute your custom components, hooks, pages, and -other files to any React project. +[![MobX compatibility](https://img.shields.io/badge/Compatible-1?logo=mobx&label=MobX%206%2F7)][5] +[![NPM Dependency](https://img.shields.io/librariesio/github/idea2app/MobX-RESTful-Shadcn.svg)][6] +[![CI & CD](https://github.com/idea2app/MobX-RESTful-Shadcn/actions/workflows/main.yml/badge.svg)][7] -> [!IMPORTANT] -> This template uses Tailwind v4. For Tailwind v3, see [registry-template-v3](https://github.com/shadcn-ui/registry-template-v3). +## Components -## Getting Started +1. [Badge Bar](https://mobx-restful-shadcn.idea2.app/) +2. [Badge Input](https://mobx-restful-shadcn.idea2.app/) +3. [Image Preview](https://mobx-restful-shadcn.idea2.app/) +4. [File Preview](https://mobx-restful-shadcn.idea2.app/) +5. [File Picker](https://mobx-restful-shadcn.idea2.app/) +6. [File Uploader](https://mobx-restful-shadcn.idea2.app/) +7. [Form Field](https://mobx-restful-shadcn.idea2.app/) +8. [Range Input](https://mobx-restful-shadcn.idea2.app/) +9. [Array Field](https://mobx-restful-shadcn.idea2.app/) +10. [REST Form](https://mobx-restful-shadcn.idea2.app/) +11. [REST Form Modal](https://mobx-restful-shadcn.idea2.app/) +12. [Pager](https://mobx-restful-shadcn.idea2.app/) +13. [REST Table](https://mobx-restful-shadcn.idea2.app/) +14. [Scroll Boundary](https://mobx-restful-shadcn.idea2.app/) +15. [Scroll List](https://mobx-restful-shadcn.idea2.app/) +16. [Searchable Input](https://mobx-restful-shadcn.idea2.app/) -This is a template for creating a custom registry using Next.js. +## Installation -- The template uses a `registry.json` file to define components and their files. -- The `shadcn build` command is used to build the registry. -- The registry items are served as static files under `public/r/[name].json`. -- The template also includes a route handler for serving registry items. -- Every registry item are compatible with the `shadcn` CLI. -- We have also added v0 integration using the `Open in v0` api. +```shell +npx shadcn-helper add https://mobx-restful-shadcn.idea2.app/r/rest-table.json +``` + +Replace `rest-table` with any component name from the list above. + +## Configuration + +### Internationalization + +Set up i18n translation model for UI text: + +```typescript +import { TranslationModel } from "mobx-i18n"; +import { IDType } from "mobx-restful"; + +export const i18n = new TranslationModel({ + en_US: { + load_more: "Load more", + no_more: "No more", + create: "Create", + view: "View", + submit: "Submit", + cancel: "Cancel", + edit: "Edit", + delete: "Delete", + total_x_rows: ({ totalCount }: { totalCount: number }) => + `Total ${totalCount} rows`, + sure_to_delete_x: ({ keys }: { keys: IDType[] }) => + `Are you sure to delete ${keys.join(", ")}?`, + }, +}); +``` + +### Data Source + +Set up HTTP client and implement Model class: + +```typescript +import { githubClient, RepositoryModel } from "mobx-github"; + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; + +githubClient.use(({ request }, next) => { + if (GITHUB_TOKEN) + request.headers = { + ...request.headers, + Authorization: `Bearer ${GITHUB_TOKEN}`, + }; + return next(); +}); + +export const repositoryStore = new RepositoryModel("idea2app"); +``` + +## Usage + +### Pagination Table + +```tsx +import { computed } from "mobx"; +import { observer } from "mobx-react"; +import { Component } from "react"; + +import { BadgeBar } from "@/components/ui/badge-bar"; +import { RestTable, Column } from "@/components/ui/rest-table"; +import repositoryStore, { Repository } from "@/models/Repository"; +import { i18n } from "@/models/Translation"; + +@observer +export class RepositoryTable extends Component { + @computed + get columns() { + return [ + { + key: "full_name", + renderHead: "Repository Name", + renderBody: ({ html_url, full_name }) => ( + + {full_name} + + ), + required: true, + minLength: 3, + invalidMessage: "Input 3 characters at least", + }, + { key: "homepage", type: "url", renderHead: "Home Page" }, + { key: "language", renderHead: "Programming Language" }, + { + key: "topics", + renderHead: "Topic", + renderBody: ({ topics }) => ( + ({ + text, + link: `https://github.com/topics/${text}`, + }))} + /> + ), + }, + { key: "stargazers_count", type: "number", renderHead: "Star Count" }, + { key: "description", renderHead: "Description", rows: 3 }, + ]; + } + + render() { + return ( + + ); + } +} +``` + +### Scroll List + +```tsx +import { observer } from "mobx-react"; + +import { ScrollList } from "@/components/ui/scroll-list"; +import repositoryStore from "@/models/Repository"; +import { i18n } from "@/models/Translation"; + +export const ScrollListExample = () => ( + ( +
    + {allItems.map(({ id, name, description }) => ( +
  • +

    {name}

    +

    {description}

    +
  • + ))} +
+ )} + /> +); +``` + +### File Uploader + +```tsx +import { FileModel, FileUploader } from "@/components/ui/file-uploader"; + +class MyFileModel extends FileModel {} + +const store = new MyFileModel(); + +export const EditorPage = () => ( + +); +``` + +## Development + +This is a custom component registry built with Next.js and compatible with the `shadcn` CLI. + +### Getting Started + +1. Clone the repository +2. Install dependencies: `pnpm install` +3. Run development server: `pnpm dev` +4. Build registry: `pnpm registry:build` +5. Build project: `pnpm build` + +### Registry Structure + +- The `registry.json` file defines all components and their files +- Components are located in `registry/new-york/blocks/` +- Each component has its implementation and example files +- The `shadcn build` command generates registry items in `public/r/` ## Documentation -Visit the [shadcn documentation](https://ui.shadcn.com/docs/registry) to view the full documentation. +- [Shadcn UI Documentation](https://ui.shadcn.com/docs) +- [Component Registry Documentation](https://ui.shadcn.com/docs/registry) +- [MobX RESTful Documentation][2] [1]: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete [2]: https://github.com/idea2app/MobX-RESTful [3]: https://reactjs.org/ +[4]: https://ui.shadcn.com/ +[5]: https://mobx.js.org/ +[6]: https://libraries.io/npm/mobx-restful-shadcn +[7]: https://github.com/idea2app/MobX-RESTful-Shadcn/actions/workflows/main.yml diff --git a/app/globals.css b/app/globals.css index 1b09b35..2a53fcb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,6 +1,10 @@ @import "tailwindcss"; @import "tw-animate-css"; +.overflow-x-auto { + overflow-x: auto; +} + @custom-variant dark (&:is(.dark *)); @theme inline { @@ -44,8 +48,9 @@ } :root { - --font-geist-sans: 'Geist Sans', ui-sans-serif, system-ui, sans-serif; - --font-geist-mono: 'Geist Mono', ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; + --font-geist-sans: "Geist Sans", ui-sans-serif, system-ui, sans-serif; + --font-geist-mono: "Geist Mono", ui-monospace, "Cascadia Code", + "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace; --radius: 0.625rem; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); diff --git a/app/page.tsx b/app/page.tsx index b17f03d..a6e9175 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -14,6 +14,7 @@ import { BadgeInputExample } from "@/registry/new-york/blocks/badge-input/exampl import { RangeInputExample } from "@/registry/new-york/blocks/range-input/example"; import { FilePickerExample } from "@/registry/new-york/blocks/file-picker/example"; import { FormFieldExample } from "@/registry/new-york/blocks/form-field/example"; +import { RestTableExample } from "@/registry/new-york/blocks/rest-table/example"; export default function Home() { return ( @@ -130,6 +131,14 @@ export default function Home() { > + + + + ); diff --git a/components/example/form.tsx b/components/example/form.tsx index 38cd046..4bf2cc4 100644 --- a/components/example/form.tsx +++ b/components/example/form.tsx @@ -1,22 +1,37 @@ import { GitRepository } from "mobx-github"; import { i18n, topicStore } from "@/models/example"; +import { BadgeBar } from "@/registry/new-york/blocks/badge-bar/badge-bar"; import { Field } from "@/registry/new-york/blocks/rest-form/rest-form"; +import { Column } from "@/registry/new-york/blocks/rest-table/rest-table"; import { SearchableInput } from "@/registry/new-york/blocks/searchable-input/searchable-input"; -export const fields: Field[] = [ +export const columns: Column[] = [ { key: "full_name", - renderLabel: "Repository Name", + renderHead: "Repository Name", + renderBody: ({ html_url, full_name }) => ( + + {full_name} + + ), required: true, minLength: 3, invalidMessage: "Input 3 characters at least", }, - { key: "homepage", type: "url", renderLabel: "Home Page" }, - { key: "language", renderLabel: "Programming Language" }, + { key: "homepage", type: "url", renderHead: "Home Page" }, + { key: "language", renderHead: "Programming Language" }, { key: "topics", - renderLabel: "Topic", + renderHead: "Topic", + renderBody: ({ topics }) => ( + ({ + text, + link: `https://github.com/topics/${text}`, + }))} + /> + ), renderInput: ({ topics }) => ( [] = [ /> ), }, - { key: "stargazers_count", type: "number", renderLabel: "Star Count" }, - { key: "description", renderLabel: "Description", rows: 3 }, + { key: "stargazers_count", type: "number", renderHead: "Star Count" }, + { key: "description", renderHead: "Description", rows: 3 }, ]; + +export const fields: Field[] = columns.map( + ({ renderHead, renderBody, ...meta }) => ({ + ...meta, + renderLabel: renderHead, + }) +); diff --git a/components/example/open-in-v0-button.tsx b/components/example/open-in-v0-button.tsx index 96f9eaa..0e1772c 100644 --- a/components/example/open-in-v0-button.tsx +++ b/components/example/open-in-v0-button.tsx @@ -1,42 +1,41 @@ -import { Button } from "@/registry/new-york/ui/button" -import { cn } from "@/lib/utils" +import { ComponentProps, FC } from "react"; -export function OpenInV0Button({ - name, - className, -}: { name: string } & React.ComponentProps) { - return ( - - ) -} + + + + + +); diff --git a/components/index.ini b/components/index.ini index e3be92f..ffb497d 100755 --- a/components/index.ini +++ b/components/index.ini @@ -2,4 +2,6 @@ badge button dialog input -label \ No newline at end of file +label +checkbox +table \ No newline at end of file diff --git a/models/example.ts b/models/example.ts index 2f4e437..6558a93 100644 --- a/models/example.ts +++ b/models/example.ts @@ -1,15 +1,23 @@ import { components, operations } from "@octokit/openapi-types"; import { githubClient, RepositoryModel } from "mobx-github"; import { TranslationModel } from "mobx-i18n"; -import { ListModel, Filter } from "mobx-restful"; +import { ListModel, Filter, IDType } from "mobx-restful"; import { buildURLData } from "web-utility"; export const i18n = new TranslationModel({ en_US: { load_more: "Load more", no_more: "No more", + create: "Create", + view: "View", submit: "Submit", cancel: "Cancel", + edit: "Edit", + delete: "Delete", + total_x_rows: ({ totalCount }: { totalCount: number }) => + `Total ${totalCount} rows`, + sure_to_delete_x: ({ keys }: { keys: IDType[] }) => + `Are you sure to delete ${keys.join(", ")}?`, }, }); diff --git a/package.json b/package.json index 098ae13..a5f2fc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-restful-shadcn", - "version": "0.6.0", + "version": "1.5.0", "private": true, "scripts": { "postinstall": "shadcn-helper install", @@ -10,6 +10,7 @@ "start": "next start" }, "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-slot": "^1.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39fbf5a..7a69a97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ importers: .: dependencies: + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -695,6 +698,19 @@ packages: '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': ^19.1.2 + '@types/react-dom': ^19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -898,6 +914,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': ^19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': ^19.1.2 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -4054,6 +4088,22 @@ snapshots: '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + '@types/react-dom': 19.2.3(@types/react@19.2.7) + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 @@ -4220,6 +4270,19 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.7)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.7 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.7)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.7 + '@rtsao/scc@1.1.0': {} '@sec-ant/readable-stream@0.4.1': {} diff --git a/registry.json b/registry.json index bd9e155..4cb3e20 100644 --- a/registry.json +++ b/registry.json @@ -1,7 +1,7 @@ { "$schema": "https://ui.shadcn.com/schema/registry.json", - "name": "acme", - "homepage": "https://acme.com", + "name": "MobX RESTful Shadcn", + "homepage": "https://mobx-restful-shadcn.idea2.app", "items": [ { "name": "complex-component", @@ -311,6 +311,37 @@ "type": "registry:component" } ] + }, + { + "name": "rest-table", + "type": "registry:component", + "title": "REST Table", + "description": "A comprehensive pagination table component for CRUD operations with MobX RESTful integration, supporting sorting, filtering, and inline editing.", + "registryDependencies": [ + "button", + "checkbox", + "table", + "badge-bar", + "file-preview", + "pager", + "rest-form", + "rest-form-modal" + ], + "dependencies": [ + "lodash.debounce", + "mobx", + "mobx-i18n", + "mobx-react", + "mobx-react-helper", + "mobx-restful", + "web-utility" + ], + "files": [ + { + "path": "registry/new-york/blocks/rest-table/rest-table.tsx", + "type": "registry:component" + } + ] } ] } diff --git a/registry/new-york/blocks/rest-table/example.tsx b/registry/new-york/blocks/rest-table/example.tsx new file mode 100644 index 0000000..1f7eb42 --- /dev/null +++ b/registry/new-york/blocks/rest-table/example.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { configure } from "mobx"; + +import { columns } from "@/components/example/form"; +import { i18n, repositoryStore } from "@/models/example"; +import { RestTable } from "./rest-table"; + +configure({ enforceActions: "never" }); + +export const RestTableExample = () => ( +
+ console.log("Checked keys:", keys)} + /> +
+); diff --git a/registry/new-york/blocks/rest-table/rest-table.tsx b/registry/new-york/blocks/rest-table/rest-table.tsx new file mode 100644 index 0000000..f11476d --- /dev/null +++ b/registry/new-york/blocks/rest-table/rest-table.tsx @@ -0,0 +1,427 @@ +"use client"; + +import debounce from "lodash.debounce"; +import { computed, observable } from "mobx"; +import { TranslationModel } from "mobx-i18n"; +import { observer } from "mobx-react"; +import { ObservedComponent } from "mobx-react-helper"; +import { DataObject, Filter, IDType } from "mobx-restful"; +import { HTMLAttributes, ReactNode } from "react"; +import { isEmpty } from "web-utility"; + +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { BadgeBar } from "../badge-bar/badge-bar"; +import { FilePreview } from "../file-preview/file-preview"; +import { Pager } from "../pager/pager"; +import { Field, RestForm, RestFormProps } from "../rest-form/rest-form"; +import { RestFormModal } from "../rest-form-modal/rest-form-modal"; + +export interface Column + extends Omit, "renderLabel"> { + renderHead?: Field["renderLabel"]; + renderBody?: (data: T) => ReactNode; + renderFoot?: ReactNode | ((data: keyof T) => ReactNode); +} + +type Translator = RestFormProps["translator"] & + TranslationModel< + string, + "create" | "view" | "edit" | "delete" | "total_x_rows" | "sure_to_delete_x" + >; + +export interface RestTableProps< + D extends DataObject, + F extends Filter = Filter +> extends Omit, "onSubmit" | "onReset">, + Pick, "size" | "store" | "onSubmit" | "onReset"> { + filter?: F; + filterFields?: Field[]; + editable?: boolean; + deletable?: boolean; + columns: Column[]; + translator: Translator; + onCheck?: (keys: IDType[]) => any; +} + +@observer +export class RestTable< + D extends DataObject, + F extends Filter = Filter +> extends ObservedComponent> { + static readonly displayName = "RestTable"; + + componentDidMount() { + const { store, filter } = this.props; + + store?.clear(); + store?.getList(filter); + } + + @computed + get fieldSize() { + const { size } = this.observedProps; + + return !size || size === "default" ? "default" : size; + } + + @observable + accessor checkedKeys: IDType[] = []; + + toggleCheck(key: IDType) { + const { checkedKeys } = this; + const index = checkedKeys.indexOf(key); + + this.checkedKeys = + index < 0 + ? [...checkedKeys, key] + : [...checkedKeys.slice(0, index), ...checkedKeys.slice(index + 1)]; + + this.props.onCheck?.(this.checkedKeys); + } + + toggleCheckAll = () => { + const { store, onCheck } = this.props; + + if (!store) return; + + const { indexKey, currentPage } = store; + + this.checkedKeys = this.checkedKeys.length + ? [] + : currentPage.map(({ [indexKey]: ID }) => ID); + + onCheck?.(this.checkedKeys); + }; + + @computed + get checkColumn(): Column { + const { checkedKeys, toggleCheckAll } = this; + const { store } = this.observedProps; + + if (!store) return {} as Column; + + const { indexKey, currentPage } = store; + + return { + renderHead: () => ( + checkedKeys.includes(ID)) + } + onCheckedChange={toggleCheckAll} + aria-label="Select all" + /> + ), + renderBody: ({ [indexKey]: ID }) => ( + this.toggleCheck(ID)} + aria-label={`Select row ${ID}`} + /> + ), + }; + } + + @computed + get operateColumn(): Column { + const { editable, deletable, columns, store, translator } = + this.observedProps; + + if (!store) return {} as Column; + + const { fieldSize } = this, + { t } = translator, + readOnly = columns.every(({ readOnly }) => readOnly), + disabled = columns.every(({ disabled }) => disabled); + + return { + renderHead: () => <>, + renderBody: (data) => ( +
+ {!disabled && editable && ( + + )} + {deletable && ( + + )} +
+ ), + }; + } + + @computed + get columns() { + const { editable, deletable, columns, onCheck } = this.observedProps; + + return [ + onCheck && this.checkColumn, + ...columns.map( + ({ renderBody, ...column }) => + ({ + ...column, + renderBody: renderBody ?? this.renderCustomBody(column), + } as Column) + ), + (editable || deletable) && this.operateColumn, + ].filter(Boolean) as Column[]; + } + + @computed + get hasHeader() { + return this.columns.some(({ renderHead }) => renderHead); + } + + @computed + get hasFooter() { + return this.columns.some(({ renderFoot }) => renderFoot); + } + + @computed + get editing() { + return !isEmpty(this.observedProps.store?.currentOne); + } + + renderCustomBody = ({ + key, + type, + multiple, + options, + accept, + rows, + }: Column): Column["renderBody"] => + type === "url" + ? ({ [key!]: value }) => + value && ( + + {value as string} + + ) + : type === "email" + ? ({ [key!]: value }) => + value && ( + + {value as string} + + ) + : type === "tel" + ? ({ [key!]: value }) => + value && ( + + {value as string} + + ) + : type === "file" + ? ({ [key!]: value }) => + ((Array.isArray(value) ? value : [value]) as string[]).map( + (path) => + path && + ) + : options || multiple + ? ({ [key!]: value }) => + value && ( + ({ text }))} /> + ) + : !options && rows + ? ({ [key!]: value }) => ( +

+ {value as string} +

+ ) + : undefined; + + renderTable() { + const { store } = this.props; + + if (!store) return null; + + const { hasHeader, hasFooter, columns, editing } = this; + const { indexKey, downloading, currentPage } = store; + + return ( + + {hasHeader && ( + + + {columns.map( + ({ key, renderHead }, index) => + (key || renderHead) && ( + + {typeof renderHead === "function" + ? renderHead(key!) + : renderHead || (key as string)} + + ) + )} + + + )} + + {!editing && downloading > 0 ? ( + + +
+
+
+ + + ) : ( + currentPage.map((data) => ( + + {columns.map( + ({ key, renderBody }, index) => + (key || renderBody) && ( + + {renderBody?.(data) || (key && data[key])} + + ) + )} + + )) + )} + + + {hasFooter && ( + + + {columns.map( + ({ key, renderFoot }, index) => + (key || renderFoot) && ( + + {typeof renderFoot === "function" + ? renderFoot(key!) + : renderFoot || (key as string)} + + ) + )} + + + )} +
+ ); + } + + getList = debounce(({ pageIndex, pageSize }) => { + const { store, filter } = this.props; + + if (store && store.downloading < 1) + store.getList(filter, pageIndex, pageSize); + }); + + async deleteList(keys: IDType[]) { + const { translator, store } = this.props; + + if (confirm(translator.t("sure_to_delete_x", { keys }))) + for (const key of keys) await store?.deleteOne(key); + } + + render() { + const { + className = "overflow-auto flex flex-col gap-3", + editable, + deletable, + filterFields, + store, + translator, + onSubmit, + onReset, + ...props + } = this.props; + + if (!store) return null; + + const { fieldSize } = this; + const { t } = translator; + const { indexKey, pageSize, pageIndex, pageCount, totalCount } = store; + + return ( +
+
+ {filterFields && ( + store.getList(filter, 1)} + onReset={() => store.getList({}, 1)} + /> + )} + {deletable && ( + + )} + {editable && ( + + )} +
+ + {this.renderTable()} + +
+ {!!totalCount && ( + + {t("total_x_rows", { totalCount })} + + )} + +
+ + {editable && ( + ({ + ...field, + renderLabel: renderHead, + }))} + {...{ store, translator, onSubmit }} + /> + )} +
+ ); + } +}