From 033f78d29aed1b31c691a8e07ecfebb2eb8efa84 Mon Sep 17 00:00:00 2001
From: duchuy-wins
Date: Thu, 13 Jun 2024 09:42:48 +0700
Subject: [PATCH 1/3] Feat: permission group
---
package-lock.json | 107 +---
package.json | 3 +-
.../dashboard/chihois/_components/columns.tsx | 6 +-
.../dashboard/roles/_components/columns.tsx | 24 +-
.../dashboard/roles/_components/role-form.tsx | 26 +-
.../roles/_components/roles-table.tsx | 2 +-
src/components/form/antd-image-upload.tsx | 38 +-
src/components/ui/command.tsx | 155 ++++++
src/components/ui/dialog.tsx | 122 +++++
src/components/ui/multi-selector.tsx | 498 ++++++++++++++++++
src/lib/axios.ts | 22 +-
src/lib/constants.tsx | 10 +-
src/lib/hooks/use-sorting.ts | 2 +-
src/lib/utils.ts | 11 +
src/types/default-type.ts | 8 +
src/types/employee.ts | 29 +-
src/types/permissionGroup.ts | 26 +
src/types/request-params.ts | 2 +-
src/types/role.ts | 49 +-
19 files changed, 982 insertions(+), 158 deletions(-)
create mode 100644 src/components/ui/command.tsx
create mode 100644 src/components/ui/dialog.tsx
create mode 100644 src/components/ui/multi-selector.tsx
create mode 100644 src/types/default-type.ts
create mode 100644 src/types/permissionGroup.ts
diff --git a/package-lock.json b/package-lock.json
index 9299d34..7d1d81a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,8 +19,6 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
- "@radix-ui/react-progress": "^1.0.3",
- "@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
@@ -33,6 +31,7 @@
"axios": "^1.7.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"dayjs": "^1.11.11",
"focus-trap-react": "^10.2.3",
@@ -45,7 +44,6 @@
"react-aria": "^3.33.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18",
- "react-dropzone": "^14.2.3",
"react-hook-form": "^7.51.5",
"react-stately": "^3.31.1",
"sonner": "^1.4.41",
@@ -1318,30 +1316,6 @@
}
}
},
- "node_modules/@radix-ui/react-progress": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.0.3.tgz",
- "integrity": "sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==",
- "dependencies": {
- "@babel/runtime": "^7.13.10",
- "@radix-ui/react-context": "1.0.1",
- "@radix-ui/react-primitive": "1.0.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0",
- "react-dom": "^16.8 || ^17.0 || ^18.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",
@@ -1373,37 +1347,6 @@
}
}
},
- "node_modules/@radix-ui/react-scroll-area": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz",
- "integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==",
- "dependencies": {
- "@babel/runtime": "^7.13.10",
- "@radix-ui/number": "1.0.1",
- "@radix-ui/primitive": "1.0.1",
- "@radix-ui/react-compose-refs": "1.0.1",
- "@radix-ui/react-context": "1.0.1",
- "@radix-ui/react-direction": "1.0.1",
- "@radix-ui/react-presence": "1.0.1",
- "@radix-ui/react-primitive": "1.0.3",
- "@radix-ui/react-use-callback-ref": "1.0.1",
- "@radix-ui/react-use-layout-effect": "1.0.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0",
- "react-dom": "^16.8 || ^17.0 || ^18.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
"node_modules/@radix-ui/react-select": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
@@ -4125,14 +4068,6 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
- "node_modules/attr-accept": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
- "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -4447,6 +4382,19 @@
"node": ">=6"
}
},
+ "node_modules/cmdk": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz",
+ "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==",
+ "dependencies": {
+ "@radix-ui/react-dialog": "1.0.5",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -5610,17 +5558,6 @@
"node": "^10.12.0 || >=12.0.0"
}
},
- "node_modules/file-selector": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
- "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
- "dependencies": {
- "tslib": "^2.4.0"
- },
- "engines": {
- "node": ">= 12"
- }
- },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -8736,22 +8673,6 @@
"react": "^18.2.0"
}
},
- "node_modules/react-dropzone": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
- "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
- "dependencies": {
- "attr-accept": "^2.2.2",
- "file-selector": "^0.6.0",
- "prop-types": "^15.8.1"
- },
- "engines": {
- "node": ">= 10.13"
- },
- "peerDependencies": {
- "react": ">= 16.8 || 18.0.0"
- }
- },
"node_modules/react-hook-form": {
"version": "7.51.5",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.5.tgz",
diff --git a/package.json b/package.json
index e3a1e4a..e66b553 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"axios": "^1.7.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"dayjs": "^1.11.11",
"focus-trap-react": "^10.2.3",
@@ -81,4 +82,4 @@
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
-}
\ No newline at end of file
+}
diff --git a/src/app/dashboard/chihois/_components/columns.tsx b/src/app/dashboard/chihois/_components/columns.tsx
index 6ef851e..8495416 100644
--- a/src/app/dashboard/chihois/_components/columns.tsx
+++ b/src/app/dashboard/chihois/_components/columns.tsx
@@ -64,12 +64,12 @@ const defaultColumns: ColumnDef[] = [
-
-
-
+
+
+
);
diff --git a/src/app/dashboard/roles/_components/columns.tsx b/src/app/dashboard/roles/_components/columns.tsx
index 629ef23..83a066f 100644
--- a/src/app/dashboard/roles/_components/columns.tsx
+++ b/src/app/dashboard/roles/_components/columns.tsx
@@ -13,11 +13,13 @@ import {
import { MoreHorizontal } from "lucide-react";
import { Role, roleSchema } from "@/types/role";
import { Checkbox } from "@/components/ui/checkbox";
-import { mergeArraysById } from "@/lib/utils";
+import { mergeArraysById, snakeToHumanReadable } from "@/lib/utils";
import DeleteRoleForm from "@/app/dashboard/roles/_components/delete-role-form";
import DropDownModalWrapper from "@/components/dropdown/dropdown-modal-wrapper";
import RoleForm from "@/app/dashboard/roles/_components/role-form";
import { dynamicColumns } from "@/components/table/dynamic-column";
+import { DataTableColumnHeader } from "@/components/table/data-table-column-header";
+import { PermissionGroup } from "@/types/permissionGroup";
const defaultColumns: ColumnDef[] = [
{
@@ -72,6 +74,24 @@ const defaultColumns: ColumnDef[] = [
];
// Add, or override stuff here by specify id
-const updates: ColumnDef[] = [];
+const updates: ColumnDef[] = [
+ {
+ id: "permission_groups",
+ accessorKey: "permission_groups",
+ header: ({ column }) => ,
+ cell: ({ row }) => {
+ const role = row.original;
+ const permissionGroups = role.permission_groups;
+ return (
+ permissionGroups &&
+ permissionGroups.he_thong &&
+ permissionGroups.he_thong.length > 0 &&
+ permissionGroups?.he_thong?.map((permission) => {
+ return {permission.display_name}
;
+ })
+ );
+ },
+ },
+];
export const columns = mergeArraysById(defaultColumns, updates);
diff --git a/src/app/dashboard/roles/_components/role-form.tsx b/src/app/dashboard/roles/_components/role-form.tsx
index 0b11079..9cc4f94 100644
--- a/src/app/dashboard/roles/_components/role-form.tsx
+++ b/src/app/dashboard/roles/_components/role-form.tsx
@@ -1,14 +1,14 @@
"use client";
-import { Role, roleSchema } from "@/types/role";
+import { PermissionID, Role, roleSchema } from "@/types/role";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import ModalCard from "@/components/form/modal-card";
import ReactHookForm from "@/components/form/react-hook-form";
import { useCreateRole, useEditRole } from "@/services/role-service";
import DynamicFormFields from "@/components/form/dynamic-form-fields";
-import { PasswordInput } from "@/components/ui/toggle-able-password";
+import MultipleSelector, { Option } from "@/components/ui/multi-selector";
-export default function RoleForm({ type, initialData = {} }: { type: "Create" | "Edit"; initialData?: any }) {
+export default function RoleForm({ type, initialData = {} }: { type: "Create" | "Edit"; initialData?: Role }) {
const { mutate: mutateCreate, isPending: isPendingCreate, isSuccess: isSuccessCreate } = useCreateRole();
const { mutate: mutateEdit, isPending: isPendingEdit, isSuccess: isSuccessEdit } = useEditRole();
@@ -16,12 +16,20 @@ export default function RoleForm({ type, initialData = {} }: { type: "Create" |
resolver: zodResolver(roleSchema),
defaultValues: initialData,
});
-
const handleSubmit = (values: Role) => {
+ // Handle permissions_id format: from [{label: 'test', value: '1'}] to ['1', '2']
+ const permissionIds = values.permission_ids as Option[];
+ values.permission_ids = permissionIds?.length > 0 && permissionIds.map(({ value }) => value);
+
if (type === "Create") mutateCreate(values);
if (type === "Edit") mutateEdit(values);
};
+ const defaultOptions = Object.entries(PermissionID).map(([key, value]) => ({
+ value: value as string,
+ label: key as string,
+ }));
+
return (
,
+ permission_ids: (
+ no results found.
+ }
+ />
+ ),
}}
/>
diff --git a/src/app/dashboard/roles/_components/roles-table.tsx b/src/app/dashboard/roles/_components/roles-table.tsx
index 655cb01..d0d610e 100644
--- a/src/app/dashboard/roles/_components/roles-table.tsx
+++ b/src/app/dashboard/roles/_components/roles-table.tsx
@@ -47,5 +47,5 @@ export function RolesTable({ columns, initialData, total }: DataT
debugTable: process.env.NODE_ENV === "development",
});
- return ;
+ return ;
}
diff --git a/src/components/form/antd-image-upload.tsx b/src/components/form/antd-image-upload.tsx
index 218a67d..c7bc446 100644
--- a/src/components/form/antd-image-upload.tsx
+++ b/src/components/form/antd-image-upload.tsx
@@ -1,15 +1,16 @@
-import { FC } from "react";
import { InboxOutlined } from "@ant-design/icons";
-import type { UploadProps } from "antd";
import { message, Upload } from "antd";
+import { AxiosRequestHeaders } from "axios";
+import { useSession } from "next-auth/react";
const { Dragger } = Upload;
-const props: UploadProps = {
- name: "file",
- multiple: true,
- action: "https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload",
- onChange(info) {
+export default function ImageUpload({ onUpload }: { onUpload: any }) {
+ const session = useSession();
+
+ const handleSubmit = () => {};
+
+ const handleOnChange = (info) => {
const { status } = info.file;
if (status !== "uploading") {
console.log(info.file, info.fileList);
@@ -19,15 +20,26 @@ const props: UploadProps = {
} else if (status === "error") {
message.error(`${info.file.name} file upload failed.`);
}
- },
- onDrop(e) {
+ };
+
+ const handleOnDrop = (e) => {
console.log("Dropped files", e.dataTransfer.files);
- },
-};
+ };
-export default function ImageUpload({ onUpload }: { onUpload: any }) {
return (
-
+ false} // return false so that antd doesn't upload the picture right away
+ >
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
new file mode 100644
index 0000000..007798a
--- /dev/null
+++ b/src/components/ui/command.tsx
@@ -0,0 +1,155 @@
+"use client"
+
+import * as React from "react"
+import { type DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Dialog, DialogContent } from "@/components/ui/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+interface CommandDialogProps extends DialogProps {}
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+
+ )
+}
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+CommandShortcut.displayName = "CommandShortcut"
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..01ff19c
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/src/components/ui/multi-selector.tsx b/src/components/ui/multi-selector.tsx
new file mode 100644
index 0000000..b138da1
--- /dev/null
+++ b/src/components/ui/multi-selector.tsx
@@ -0,0 +1,498 @@
+"use client";
+
+import * as React from "react";
+import { forwardRef, useEffect } from "react";
+import { Command as CommandPrimitive, useCommandState } from "cmdk";
+import { X } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { Badge } from "@/components/ui/badge";
+import { Command, CommandGroup, CommandItem, CommandList } from "@/components/ui/command";
+
+export interface Option {
+ value: string;
+ label: string;
+ disable?: boolean;
+ /** fixed option that can't be removed. */
+ fixed?: boolean;
+ /** Group the options by providing key. */
+ [key: string]: string | boolean | undefined;
+}
+interface GroupOption {
+ [key: string]: Option[];
+}
+
+interface MultipleSelectorProps {
+ value?: Option[];
+ defaultOptions?: Option[];
+ /** manually controlled options */
+ options?: Option[];
+ placeholder?: string;
+ /** Loading component. */
+ loadingIndicator?: React.ReactNode;
+ /** Empty component. */
+ emptyIndicator?: React.ReactNode;
+ /** Debounce time for async search. Only work with `onSearch`. */
+ delay?: number;
+ /**
+ * Only work with `onSearch` prop. Trigger search when `onFocus`.
+ * For example, when user click on the input, it will trigger the search to get initial options.
+ **/
+ triggerSearchOnFocus?: boolean;
+ /** async search */
+ onSearch?: (value: string) => Promise