diff --git a/.gitignore b/.gitignore index bc5557fd..613a4cc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules dist .DS_Store -bun.lockb +tester/ +bun.lockb \ No newline at end of file diff --git a/README.md b/README.md index e4225e84..4e6ae72a 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ To run locally: ```sh pnpm i -pnpm run dev +pnpm run build npm install -g . (in a second terminal - this will then make kirimase available across your machine using "kirimase *command*") ``` diff --git a/package.json b/package.json index a894b525..0c09aade 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "npx tsc", + "build": "npx tsc && rm -rf dist/templates && cp -r src/templates dist/templates", + "reinstall": "bun run build && npm i -g .", + "createTesterApp": "rm -rf tester/ && bunx create-next-app tester && cd tester/ && kirimase init", "dev2": "node dist/index.js", "dev": "tsc -w" }, @@ -29,6 +31,7 @@ "chalk": "^5.3.0", "commander": "^11.0.0", "consola": "^3.2.3", + "eta": "^3.2.0", "execa": "^8.0.1", "figlet": "^1.7.0", "ora": "^8.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66c23900..5c35e179 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: consola: specifier: ^3.2.3 version: 3.2.3 + eta: + specifier: ^3.2.0 + version: 3.2.0 execa: specifier: ^8.0.1 version: 8.0.1 @@ -691,6 +694,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /eta@3.2.0: + resolution: {integrity: sha512-Qzc3it7nLn49dbOb9+oHV9rwtt9qN8oShRztqkZ3gXPqQflF0VLin5qhWk0g/2ioibBwT4DU6OIMVft7tg/rVg==} + engines: {node: '>=6.0.0'} + dev: false + /execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} diff --git a/src/commands/add/componentLib/shadcn-ui/index.ts b/src/commands/add/componentLib/shadcn-ui/index.ts index 4d705f5c..686bf753 100644 --- a/src/commands/add/componentLib/shadcn-ui/index.ts +++ b/src/commands/add/componentLib/shadcn-ui/index.ts @@ -1,6 +1,7 @@ import { consola } from "consola"; // import { execa } from "execa"; import { existsSync } from "fs"; +import { eta } from "../../../../eta.js"; import { addPackageToConfig, createFile, @@ -47,6 +48,7 @@ const manualInstallShadCn = async ( addToInstallList({ regular: [ + "@tanstack/react-table", "tailwindcss-animate", "class-variance-authority", "clsx", @@ -85,6 +87,16 @@ const manualInstallShadCn = async ( rootPath.concat("components/ui/ThemeToggle.tsx"), generateThemeToggler() ); + + // generate base Data Table + createFile( + rootPath.concat("components/ui/DataTable/index.tsx"), + eta.render("DataTable/index.eta", {}) + ); + createFile( + rootPath.concat("components/ui/DataTable/pagination.tsx"), + eta.render("DataTable/pagination.eta", {}) + ); // add context provider to layout addContextProviderToRootLayout("ThemeProvider"); }; @@ -131,6 +143,8 @@ export const installShadcnUI = async ( // "label", // ]); addToShadcnComponentList([ + "table", + "pagination", "button", "sonner", "avatar", diff --git a/src/commands/add/orm/drizzle/generators.ts b/src/commands/add/orm/drizzle/generators.ts index cabf2014..02737727 100644 --- a/src/commands/add/orm/drizzle/generators.ts +++ b/src/commands/add/orm/drizzle/generators.ts @@ -540,7 +540,7 @@ export const addScriptsToPackageJson = ( "db:migrate": `tsx ${libPath}/db/migrate.ts`, "db:drop": "drizzle-kit drop", "db:pull": `drizzle-kit introspect:${driver}`, - ...(driver !== "pg" ? { "db:push": `drizzle-kit push:${driver}` } : {}), + "db:push": `drizzle-kit push:${driver}`, "db:studio": "drizzle-kit studio", "db:check": `drizzle-kit check:${driver}`, }; diff --git a/src/commands/add/orm/drizzle/utils.ts b/src/commands/add/orm/drizzle/utils.ts index ed9e3611..0209cf80 100644 --- a/src/commands/add/orm/drizzle/utils.ts +++ b/src/commands/add/orm/drizzle/utils.ts @@ -69,7 +69,6 @@ ${nanoidContent.split("\n")[1].trim()} replaceFile(utilsPath, newContent); } }; - export const checkTimestampsInUtils = () => { const timestampsContent = `export const timestamps: { createdAt: true; updatedAt: true } = { createdAt: true, diff --git a/src/commands/generate/generators/views-with-server-actions.ts b/src/commands/generate/generators/views-with-server-actions.ts index bacf0c15..e5e56c46 100644 --- a/src/commands/generate/generators/views-with-server-actions.ts +++ b/src/commands/generate/generators/views-with-server-actions.ts @@ -36,6 +36,7 @@ import { formatTableName, toCamelCase } from "../utils.js"; import { existsSync, readFileSync } from "fs"; import { consola } from "consola"; import { addToShadcnComponentList } from "../../add/utils.js"; +import eta from "../../../eta.js"; export const scaffoldViewsAndComponentsWithServerActions = async ( schema: ExtendedSchema @@ -80,6 +81,15 @@ export const scaffoldViewsAndComponentsWithServerActions = async ( createListComponent(schema) ); + // create columns + createFile( + formatFilePath( + `app/(app)/${tableNameKebabCase}/columns.tsx`, + { prefix: "rootPath", removeExtension: false } + ), + eta.render('DataTable/columns.eta', { fields: schema.fields, tableNameKebabCase }) + ); + // create components/tableName/TableNameForm.tsx createFile( formatFilePath( @@ -251,7 +261,8 @@ const generateView = (schema: Schema) => { const relationsFormatted = formatRelations(relations); return `import { Suspense } from "react"; - +import { DataTable } from "${formatFilePath(`components/ui/DataTable`, {prefix: "alias", removeExtension: false})}"; +import { columns } from "./columns"; import Loading from "${formatFilePath("app/loading", { prefix: "alias", removeExtension: false, @@ -313,6 +324,7 @@ const ${tableNameCapitalised} = async () => { .join(" ") : "" } /> + ); }; @@ -469,19 +481,6 @@ export default function ${tableNameSingularCapitalised}List({ + - {optimistic${tableNamePluralCapitalised}.length === 0 ? ( - - ) : ( - - )} ); } diff --git a/src/commands/generate/generators/views.ts b/src/commands/generate/generators/views.ts index 926cbd51..83007f62 100644 --- a/src/commands/generate/generators/views.ts +++ b/src/commands/generate/generators/views.ts @@ -15,13 +15,16 @@ import { toCamelCase, toNormalEnglish, } from "../utils.js"; +import fs from 'fs'; import { addToShadcnComponentList } from "../../add/utils.js"; +import eta from "../../../eta.js"; export const scaffoldViewsAndComponents = async (schema: Schema) => { const { hasSrc, packages } = readConfigFile(); const { tableNameCamelCase, tableNameSingularCapitalised, + tableNameSingular, tableNameKebabCase, } = formatTableName(schema.tableName); // require trpc for these views @@ -32,13 +35,22 @@ export const scaffoldViewsAndComponents = async (schema: Schema) => { rootPath.concat(`app/(app)/${tableNameKebabCase}/page.tsx`), generateView(schema) ); + + // create tableName/[id]/page.tsx + createFile( + formatFilePath( + `app/(app)/${tableNameKebabCase}/[${tableNameSingular}Id]/page.tsx`, + { removeExtension: false, prefix: "rootPath" } + ), + createShowPage(schema) + ); // create components/tableName/TableNameList.tsx createFile( rootPath.concat( `components/${tableNameCamelCase}/${tableNameSingularCapitalised}List.tsx` - ), + ), createListComponent(schema) - ); + ); // create components/tableName/TableNameForm.tsx createFile( rootPath.concat( @@ -46,6 +58,11 @@ export const scaffoldViewsAndComponents = async (schema: Schema) => { ), createFormComponent(schema) ); + // create components/tableName/columns.tsx + createFile( + rootPath.concat(`components/${tableNameCamelCase}/columns.tsx`), + eta.render('DataTable/columns.eta', { fields: schema.fields, tableNameKebabCase }) + ); // create components/tableName/TableNameModal.tsx createFile( rootPath.concat( @@ -101,6 +118,8 @@ import { api } from "${formatFilePath(trpc.trpcApiTs, { })}";` : "" } +import { DataTable } from "${alias}/components/ui/DataTable"; +import { columns } from "${alias}/components/${tableNameCamelCase}/columns"; export default async function ${tableNameCapitalised}() { ${ @@ -113,13 +132,49 @@ export default async function ${tableNameCapitalised}() {

${tableNameNormalEnglishCapitalised}

- <${tableNameSingularCapitalised}List ${tableNameCamelCase}={${tableNameCamelCase}} /> + ); } `; }; +const createShowPage = (schema: Schema) => { + const { + tableNameCamelCase, + tableNameSingularCapitalised, + tableNameKebabCase, + tableNameSingular, + tableNameNormalEnglishCapitalised, + } = formatTableName(schema.tableName); + const { trpc } = getFilePaths(); + const { alias } = readConfigFile(); + const trpcRoute = formatFilePath(trpc.trpcApiTs, { + prefix: "alias", + removeExtension: true, + }) + const { fields } = schema; + if (!fs.existsSync(`src/compononents/ui/breadcrumbs.tsx`)) { + createFile( + `src/components/ui/breadcrumbs.tsx`, + eta.render('components/ui/breadcrumbs.eta', {}) + ) + } + + return eta.render( + 'resources/show.eta', { + fields, + tableNameSingular, + tableNameSingularCapitalised, + tableNameCamelCase, + tableNameKebabCase, + tableNameNormalEnglishCapitalised, + alias, + trpcRoute + } + ) +} + const queryHasJoins = (tableName: string) => { // const { hasSrc } = readConfigFile(); const { shared } = getFilePaths(); diff --git a/src/commands/init/utils.ts b/src/commands/init/utils.ts index 14706e4b..24f14734 100644 --- a/src/commands/init/utils.ts +++ b/src/commands/init/utils.ts @@ -268,4 +268,4 @@ export const toggleAnalytics = (input: { toggle?: boolean }) => { `Anonymous analytics are currently ${analytics ? "on" : "off"}` ); } -}; +}; \ No newline at end of file diff --git a/src/eta.ts b/src/eta.ts new file mode 100644 index 00000000..7addb4a4 --- /dev/null +++ b/src/eta.ts @@ -0,0 +1,9 @@ +import { Eta } from "eta" +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +export const eta = new Eta({ views: path.join(__dirname, "templates") }); +export default eta; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 65535cbd..aa085989 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ addCommonOptions(program.command("init")) program .command("generate") + .alias("g") .description("Generate a new resource") .action(buildSchema); diff --git a/src/templates/DataTable/columns.eta b/src/templates/DataTable/columns.eta new file mode 100644 index 00000000..d16df207 --- /dev/null +++ b/src/templates/DataTable/columns.eta @@ -0,0 +1,54 @@ +"use client" +import { MoreHorizontal } from "lucide-react" +import { + ColumnDef, +} from "@tanstack/react-table" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Button } from "@/components/ui/button" +import Link from 'next/link' + +export const columns: ColumnDef[] = [ + <% it.fields.map(function (field) { %> + { + accessorKey: "<%_ = field.name _%>", + header: "<%_ = field.name _%>", + enableHiding: false, + cell: ({ row }) => { + const resource = row.original + return ( + /${resource.id}`}> + {resource.<%_ = field.name _%>} + + ) + }, + }, + <% }) %> + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + const resource = row.original + return ( + + + + + + Actions + Edit + Delete + + + ) + }, + }, +] \ No newline at end of file diff --git a/src/templates/DataTable/index.eta b/src/templates/DataTable/index.eta new file mode 100644 index 00000000..fcd71213 --- /dev/null +++ b/src/templates/DataTable/index.eta @@ -0,0 +1,133 @@ +"use client" +import * as React from "react" +import { + VisibilityState, + flexRender, + getCoreRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table" +import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { DataTablePagination } from './pagination' + +export interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const [columnVisibility, setColumnVisibility] = + React.useState({}) + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onColumnVisibilityChange: setColumnVisibility, + state: { + columnVisibility, + }, + }) + + const hideableColumns = table.getAllColumns().filter((column) => column.getCanHide()) + + return ( +
+
+ {hideableColumns.length > 0 && + + + + + {hideableColumns + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ) + })} + + } +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ +
+ ) +} diff --git a/src/templates/DataTable/pagination.eta b/src/templates/DataTable/pagination.eta new file mode 100644 index 00000000..0c4a1939 --- /dev/null +++ b/src/templates/DataTable/pagination.eta @@ -0,0 +1,110 @@ +import { Table } from "@tanstack/react-table"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { ChevronsLeft, ChevronsRight } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const PaginationFirst = ({ + className, + ...props +}: React.ComponentProps) => ( + + + +); +PaginationFirst.displayName = "PaginationFirst"; + + +const PaginationLast = ({ + className, + ...props +}: React.ComponentProps) => ( + + + +); +PaginationLast.displayName = "PaginationLast"; + +interface DataTablePaginationProps { + table: Table; +} + +export function DataTablePagination({ + table, +}: DataTablePaginationProps) { + const currentPageIndex = table.getState().pagination.pageIndex; + const lastPageIndex = table.getPageCount() - 1; + return ( + + + + table.setPageIndex(0)} + isActive={table.getCanPreviousPage()} + /> + + + table.previousPage()} + isActive={table.getCanPreviousPage()} + /> + + {table.getCanPreviousPage() && ( + + table.previousPage()} + isActive={table.getCanPreviousPage()} + > + {currentPageIndex} + + + )} + + + {currentPageIndex + 1} + + + {table.getCanNextPage() && ( + + table.nextPage()} + isActive={table.getCanNextPage()} + > + {currentPageIndex + 2} + + + )} + + { + if (table.getCanNextPage()) table.nextPage(); + }} + isActive={table.getCanNextPage()} + /> + + + table.setPageIndex(lastPageIndex)} + isActive={table.getCanNextPage()} + /> + + + + ); +} diff --git a/src/templates/components/ui/breadcrumbs.eta b/src/templates/components/ui/breadcrumbs.eta new file mode 100644 index 00000000..15283481 --- /dev/null +++ b/src/templates/components/ui/breadcrumbs.eta @@ -0,0 +1,153 @@ +import * as React from "react" +import { ChevronRight } from "lucide-react" + +import { cn } from "@/lib/utils" + +function getValidChildren(children: React.ReactNode) { + return React.Children.toArray(children).filter((child) => + React.isValidElement(child) + ) as React.ReactElement[] +} + +export interface BreadcrumbProps extends React.ComponentPropsWithoutRef<"nav"> { + /* The visual separator between each breadcrumb item */ + separator?: React.ReactNode + /** + * If `true`, adds a separator between each breadcrumb item. + * @default true + */ + addSeparator?: boolean +} + +export const Breadcrumb = React.forwardRef( + ( + { + children, + className, + separator = , + addSeparator = true, + ...props + }, + forwardedRef + ) => { + const validChildren = getValidChildren(children) + const clones = validChildren.map((child, index) => { + return React.cloneElement(child, { + addSeparator, + separator, + isLastChild: validChildren.length === index + 1, + }) + }) + + return ( + + ) + } +) +Breadcrumb.displayName = "Breadcrumb" + +export interface BreadcrumbItemProps extends BreadcrumbProps { + /** + * If `true`, indicates that the breadcrumb item is active, adds + * `aria-current=page` and renders a `span` + */ + isCurrentPage?: boolean + isLastChild?: boolean +} + +export const BreadcrumbItem = React.forwardRef< + HTMLLIElement, + BreadcrumbItemProps +>( + ( + { + children, + className, + isCurrentPage, + isLastChild, + separator, + addSeparator, + ...props + }, + forwardedRef + ) => { + const validChildren = getValidChildren(children) + const clones = validChildren.map((child) => { + if (child.type === BreadcrumbLink) { + return React.cloneElement(child, { isCurrentPage }) + } + + if (child.type === BreadcrumbSeparator) { + return React.cloneElement(child, { + children: separator || child.props.children, + }) + } + + return child + }) + + return ( +
  • + {clones} + {!isLastChild && addSeparator && ( + {separator} + )} +
  • + ) + } +) +BreadcrumbItem.displayName = "BreadcrumbItem" + +export interface BreadcrumbLinkProps + extends React.ComponentPropsWithoutRef<"a">, + Pick { + as?: React.ElementType +} + +export const BreadcrumbLink = React.forwardRef< + HTMLAnchorElement, + BreadcrumbLinkProps +>(({ className, as: asComp, isCurrentPage, ...props }, forwardedRef) => { + const Comp = (isCurrentPage ? "span" : asComp || "a") as "a" + + return ( + + ) +}) +BreadcrumbLink.displayName = "BreadcrumbLink" + +export type BreadcrumbSeparatorProps = React.ComponentPropsWithoutRef<"span"> + +export const BreadcrumbSeparator = React.forwardRef< + HTMLSpanElement, + BreadcrumbSeparatorProps +>(({ className, ...props }, forwardedRef) => { + return ( + + ) +}) +BreadcrumbSeparator.displayName = "BreadcrumbSeparator" \ No newline at end of file diff --git a/src/templates/resources/show.eta b/src/templates/resources/show.eta new file mode 100644 index 00000000..9811624c --- /dev/null +++ b/src/templates/resources/show.eta @@ -0,0 +1,39 @@ +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from "<%_ = it.alias _%>/components/ui/breadcrumbs" +import { api } from "<%_ = it.trpcRoute _%>" +import <%= it.tableNameSingularCapitalised _%>Modal from "<%_ = it.alias _%>/components/<%_ = it.tableNameCamelCase _%>/<%_ = it.tableNameSingularCapitalised _%>Modal" + +export default async function Page({params} : {params: { <% = it.tableNameSingular _%>Id: string}}) { + const { <% = it.tableNameSingular %> } = await api.<%_ = it.tableNameCamelCase _%>.get<%_ = it.tableNameSingularCapitalised _%>ById.query({ id: params.<%_ = it.tableNameSingular _%>Id }) + return ( +
    +
    + + + Home + + + <%_ = it.tableNameNormalEnglishCapitalised _%> + + + + {params.<% = it.tableNameSingular _%>Id} + + + + +

    <%_ = it.tableNameSingularCapitalised _%>: {params.<% = it.tableNameSingular _%>Id}

    + <<%_ = it.tableNameSingularCapitalised _%>Modal <% = it.tableNameSingular _%>={<%_ = it.tableNameSingular _%>} /> +
    +
    + <% it.fields.map(function (field) { %> +
    +

    <%_ = field.name _%>

    +

    {<%_ = it.tableNameSingular _%>.<%_ = field.name _%>}

    +
    + <% }) %> +
    +
    +
    + ) +} + diff --git a/src/utils.ts b/src/utils.ts index 69acef04..3df5cdf3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -269,3 +269,11 @@ export const sendEvent = async ( return; } }; + +export const humanize = (input: string) => { + return input + .replace(/([A-Z])/g, (match) => ` ${match}`) + .replace(/_|-/g, " ") + .toLowerCase() + .replace(/(?:^|\s)\S/g, (str) => str.toUpperCase()); +};