added Create User
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { m } from '@/paraglide/messages';
|
||||
import { LOG_ACTION } from '@/types/enum';
|
||||
import { Badge } from '../ui/badge';
|
||||
|
||||
export type UserActionType = {
|
||||
@@ -9,9 +10,10 @@ export type UserActionType = {
|
||||
unban: string;
|
||||
sign_in: string;
|
||||
sign_out: string;
|
||||
change_password: string;
|
||||
};
|
||||
|
||||
const ActionBadge = ({ action }: { action: keyof UserActionType }) => {
|
||||
const ActionBadge = ({ action }: { action: LOG_ACTION }) => {
|
||||
const USER_ACTION = Object.freeze(
|
||||
new Proxy(
|
||||
{
|
||||
@@ -22,6 +24,7 @@ const ActionBadge = ({ action }: { action: keyof UserActionType }) => {
|
||||
sign_out: 'bg-yellow-400',
|
||||
ban: 'bg-rose-400',
|
||||
unban: 'bg-emerald-400',
|
||||
change_password: 'bg-red-600',
|
||||
} as UserActionType,
|
||||
{
|
||||
get: function (target: UserActionType, name: string | symbol) {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { formatters } from '@/utils/formatters';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Badge } from '../ui/badge';
|
||||
|
||||
import ActionBadge, { UserActionType } from './action-badge';
|
||||
import { LOG_ACTION } from '@/types/enum';
|
||||
import ActionBadge from './action-badge';
|
||||
import ViewDetail from './view-detail-dialog';
|
||||
|
||||
export const logColumns: ColumnDef<AuditWithUser>[] = [
|
||||
@@ -35,14 +36,12 @@ export const logColumns: ColumnDef<AuditWithUser>[] = [
|
||||
thClass: 'w-1/6',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<ActionBadge action={row.original.action as keyof UserActionType} />
|
||||
);
|
||||
return <ActionBadge action={row.original.action as LOG_ACTION} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: m.logs_page_ui_table_header_action(),
|
||||
header: m.logs_page_ui_table_header_create_at(),
|
||||
meta: {
|
||||
thClass: 'w-2/6',
|
||||
},
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard';
|
||||
import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
|
||||
import { m } from '@/paraglide/messages';
|
||||
import { LOG_ACTION } from '@/types/enum';
|
||||
import { formatters } from '@/utils/formatters';
|
||||
import { jsonSupport } from '@/utils/helper';
|
||||
import { EyeIcon } from '@phosphor-icons/react';
|
||||
import { CheckIcon, CopyIcon, EyeIcon } from '@phosphor-icons/react';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
@@ -15,7 +17,7 @@ import {
|
||||
} from '../ui/dialog';
|
||||
import { Label } from '../ui/label';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
import ActionBadge, { UserActionType } from './action-badge';
|
||||
import ActionBadge from './action-badge';
|
||||
|
||||
type ViewDetailProps = {
|
||||
data: AuditWithUser;
|
||||
@@ -23,6 +25,7 @@ type ViewDetailProps = {
|
||||
|
||||
const ViewDetail = ({ data }: ViewDetailProps) => {
|
||||
const prevent = usePreventAutoFocus();
|
||||
const { isCopied, copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
@@ -80,7 +83,7 @@ const ViewDetail = ({ data }: ViewDetailProps) => {
|
||||
<span className="font-bold">
|
||||
{m.logs_page_ui_table_header_action()}:
|
||||
</span>
|
||||
<ActionBadge action={data.action as keyof UserActionType} />
|
||||
<ActionBadge action={data.action as LOG_ACTION} />
|
||||
</div>
|
||||
{data.oldValue && (
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -92,13 +95,32 @@ const ViewDetail = ({ data }: ViewDetailProps) => {
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{data.newValue && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="font-bold">
|
||||
{m.logs_page_ui_table_header_new_value()}:
|
||||
</span>
|
||||
<pre className="whitespace-pre-wrap wrap-break-word">
|
||||
{data.newValue ? jsonSupport(data.newValue) : ''}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold">
|
||||
{m.logs_page_ui_table_header_new_value()}:
|
||||
{m.logs_page_ui_table_header_record_id()}:
|
||||
</span>
|
||||
<pre className="whitespace-pre-wrap wrap-break-word">
|
||||
{data.newValue ? jsonSupport(data.newValue) : ''}
|
||||
</pre>
|
||||
<div className="flex gap-1.5 items-center">
|
||||
{data.recordId}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-xs"
|
||||
className="rounded-full cursor-pointer"
|
||||
onClick={() => copyToClipboard(data.recordId)}
|
||||
>
|
||||
{isCopied ? <CheckIcon /> : <CopyIcon />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold">
|
||||
|
||||
198
src/components/form/admin-create-user-form.tsx
Normal file
198
src/components/form/admin-create-user-form.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { m } from '@/paraglide/messages';
|
||||
import { usersQueries } from '@/service/queries';
|
||||
import { createUser } from '@/service/user.api';
|
||||
import { RoleEnum, userCreateSchema } from '@/service/user.schema';
|
||||
import { ReturnError } from '@/types/common';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '../ui/button';
|
||||
import { DialogClose, DialogFooter } from '../ui/dialog';
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field';
|
||||
import { Input } from '../ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../ui/select';
|
||||
|
||||
type FormProps = {
|
||||
onSubmit: (open: boolean) => void;
|
||||
};
|
||||
|
||||
const AdminCreateUserForm = ({ onSubmit }: FormProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: createUserMutation } = useMutation({
|
||||
mutationFn: createUser,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...usersQueries.all, 'list'],
|
||||
});
|
||||
onSubmit(false);
|
||||
toast.success(m.users_page_message_created_user_success(), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
onError: (error: ReturnError) => {
|
||||
console.error(error);
|
||||
toast.error(m.backend_message({ code: error.code }), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
name: '',
|
||||
role: '',
|
||||
},
|
||||
validators: {
|
||||
onChange: userCreateSchema,
|
||||
onSubmit: userCreateSchema,
|
||||
},
|
||||
onSubmit: ({ value }) => {
|
||||
createUserMutation({ data: userCreateSchema.parse(value) });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
id="admin-create-user-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<FieldGroup>
|
||||
<form.Field
|
||||
name="email"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid} className="col-span-2">
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{m.login_page_form_email()}:
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
type="email"
|
||||
/>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="password"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{m.login_page_form_password()}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
type="password"
|
||||
/>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="name"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{m.profile_form_name()}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
aria-invalid={isInvalid}
|
||||
type="text"
|
||||
/>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="role"
|
||||
children={(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched && !field.state.meta.isValid;
|
||||
return (
|
||||
<Field data-invalid={isInvalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
{m.profile_form_role()}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={(value) =>
|
||||
field.handleChange(RoleEnum.parse(value))
|
||||
}
|
||||
>
|
||||
<SelectTrigger aria-invalid={isInvalid}>
|
||||
<SelectValue
|
||||
placeholder={m.users_page_ui_select_placeholder_role()}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="admin">
|
||||
{m.role_tags({ role: 'admin' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="user">
|
||||
{m.role_tags({ role: 'user' })}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Field>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline" type="button">
|
||||
{m.ui_cancel_btn()}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit" variant="destructive">
|
||||
{m.ui_signup_btn()}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCreateUserForm;
|
||||
@@ -5,7 +5,7 @@ import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 disabled:data-[active=true]:opacity-100 disabled:data-[active=true]:bg-teal-400 disabled:data-[active=true]:border-teal-400 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 disabled:data-[active=true]:opacity-100 disabled:data-[active=true]:bg-primary disabled:data-[active=true]:border-primary disabled:data-[active=true]:text-white [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
48
src/components/user/add-new-user-dialog.tsx
Normal file
48
src/components/user/add-new-user-dialog.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus';
|
||||
import { m } from '@/paraglide/messages';
|
||||
import { PlusIcon } from '@phosphor-icons/react';
|
||||
import { useState } from 'react';
|
||||
import AdminCreateUserForm from '../form/admin-create-user-form';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '../ui/dialog';
|
||||
|
||||
const AddNewUserButton = () => {
|
||||
const [_open, _setOpen] = useState(false);
|
||||
const prevent = usePreventAutoFocus();
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button type="button" variant="default">
|
||||
<PlusIcon />
|
||||
{m.nav_add_new()}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="max-w-80 xl:max-w-xl"
|
||||
{...prevent}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-3 text-lg font-bold text-red-600">
|
||||
<PlusIcon size={16} />
|
||||
{m.nav_add_new()}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{m.nav_add_new()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<AdminCreateUserForm />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddNewUserButton;
|
||||
Reference in New Issue
Block a user