diff --git a/build_log.txt b/build_log.txt deleted file mode 100644 index fff4e02..0000000 Binary files a/build_log.txt and /dev/null differ diff --git a/package-lock.json b/package-lock.json index bdaad8d..6420be7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9895,6 +9895,21 @@ "optional": true } } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.0.10", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz", + "integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/src/app/(modules)/clients/components/ClientColumns.tsx b/src/app/(modules)/clients/components/ClientColumns.tsx index 10ee639..296e0c8 100644 --- a/src/app/(modules)/clients/components/ClientColumns.tsx +++ b/src/app/(modules)/clients/components/ClientColumns.tsx @@ -2,12 +2,8 @@ import { useMemo } from 'react'; import type { ColumnDef } from '@tanstack/react-table'; -import { Edit, RotateCcw } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { PERMISSIONS } from '@/constants/permissions'; -import { usePermissions } from '@/hooks/usePermissions'; import type { Client } from '@/types'; function formatDate(value?: string | null) { @@ -19,23 +15,9 @@ function formatDate(value?: string | null) { }).format(new Date(value)); } -interface UseClientColumnsParams { - onEdit: (client: Client) => void; - onToggleStatus: (client: Client) => void; - pendingClientId?: number; -} - -export function useClientColumns({ - onEdit, - onToggleStatus, - pendingClientId, -}: UseClientColumnsParams): ColumnDef[] { - const { hasPermission } = usePermissions(); - const canEdit = hasPermission(PERMISSIONS.CLIENT.UPDATE); - const canDelete = hasPermission(PERMISSIONS.CLIENT.DELETE); - +export function useClientColumns(): ColumnDef[] { return useMemo(() => { - const columns: ColumnDef[] = [ + return [ { accessorKey: 'name', header: 'Client', @@ -75,38 +57,5 @@ export function useClientColumns({ ), }, ]; - - if (!canEdit && !canDelete) return columns; - - columns.push({ - id: 'actions', - header: () =>
Actions
, - enableSorting: false, - cell: ({ row }) => { - const client = row.original; - return ( -
- {canEdit ? ( - - ) : null} - {canDelete ? ( - - ) : null} -
- ); - }, - }); - - return columns; - }, [canDelete, canEdit, onEdit, onToggleStatus, pendingClientId]); + }, []); } diff --git a/src/app/(modules)/clients/components/ClientTable.tsx b/src/app/(modules)/clients/components/ClientTable.tsx index a3499ab..f4cd660 100644 --- a/src/app/(modules)/clients/components/ClientTable.tsx +++ b/src/app/(modules)/clients/components/ClientTable.tsx @@ -2,8 +2,10 @@ import type { ReactNode } from 'react'; import type { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Edit, RotateCcw } from 'lucide-react'; import { DataTable } from '@/components/data-table'; +import { PERMISSIONS } from '@/constants/permissions'; import type { Client } from '@/types'; interface ClientTableProps { @@ -18,6 +20,9 @@ interface ClientTableProps { onPageChange: (skip: number) => void; onLimitChange: (limit: number) => void; onSortingChange: (sorting: SortingState) => void; + onEdit: (client: Client) => void; + onToggleStatus: (client: Client) => void; + pendingClientId?: number; } export function ClientTable({ @@ -32,6 +37,9 @@ export function ClientTable({ onPageChange, onLimitChange, onSortingChange, + onEdit, + onToggleStatus, + pendingClientId, }: ClientTableProps) { return ( , + permission: PERMISSIONS.CLIENT.UPDATE, + onClick: onEdit, + }, + { + label: (client) => (client.is_active ? 'Deactivate' : 'Activate'), + icon: , + permission: PERMISSIONS.CLIENT.DELETE, + disabled: (client) => pendingClientId === client.id, + onClick: onToggleStatus, + }, + ]} pagination={{ skip, limit, diff --git a/src/app/(modules)/clients/page.tsx b/src/app/(modules)/clients/page.tsx index be29392..355c056 100644 --- a/src/app/(modules)/clients/page.tsx +++ b/src/app/(modules)/clients/page.tsx @@ -69,11 +69,7 @@ export default function ClientsPage() { [updateClientStatus], ); - const columns = useClientColumns({ - onEdit: openEdit, - onToggleStatus: toggleStatus, - pendingClientId, - }); + const columns = useClientColumns(); const total = clientsQuery.data?.total ?? 0; const clients = clientsQuery.data?.items ?? []; @@ -118,6 +114,9 @@ export default function ClientsPage() { onPageChange={setSkip} onLimitChange={setLimit} onSortingChange={setSorting} + onEdit={openEdit} + onToggleStatus={toggleStatus} + pendingClientId={pendingClientId} /> diff --git a/src/app/(modules)/package/components/PackageDialog.tsx b/src/app/(modules)/package/components/PackageDialog.tsx index ff3b77b..64f5519 100644 --- a/src/app/(modules)/package/components/PackageDialog.tsx +++ b/src/app/(modules)/package/components/PackageDialog.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ComponentProps } from 'react'; -import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form'; +import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import { Globe, Loader2, Package as PackageIcon } from 'lucide-react'; import { FormField } from '@/components/form'; @@ -36,7 +36,7 @@ interface PackageDialogProps { onProjectChange: (projectId: string) => void; register: UseFormRegister; errors: FieldErrors; - touchedFields: UseFormReturn['formState']['touchedFields']; + isSubmitted: boolean; onSubmit: ComponentProps<'form'>['onSubmit']; canSubmit: boolean; isSaving: boolean; @@ -52,19 +52,15 @@ export function PackageDialog({ onProjectChange, register, errors, - touchedFields, + isSubmitted, onSubmit, canSubmit, isSaving, }: PackageDialogProps) { - const projectErrorMessage = touchedFields.project_id ? errors.project_id?.message : undefined; - const nameErrorMessage = touchedFields.name ? errors.name?.message : undefined; - const startErrorMessage = touchedFields.chainage_start_km - ? errors.chainage_start_km?.message - : undefined; - const endErrorMessage = touchedFields.chainage_end_km - ? errors.chainage_end_km?.message - : undefined; + const projectErrorMessage = isSubmitted ? errors.project_id?.message : undefined; + const nameErrorMessage = isSubmitted ? errors.name?.message : undefined; + const startErrorMessage = isSubmitted ? errors.chainage_start_km?.message : undefined; + const endErrorMessage = isSubmitted ? errors.chainage_end_km?.message : undefined; return ( @@ -155,7 +151,12 @@ export function PackageDialog({ - - ) : null} - {canDelete ? ( - - ) : null} - - ); - }, - }); - - return columns; - }, [canDelete, canEdit, onEdit, onToggleStatus, pendingPlanId]); + }, []); } diff --git a/src/app/(modules)/plans/components/PlanSheet.tsx b/src/app/(modules)/plans/components/PlanSheet.tsx index 8e9580a..a0506e9 100644 --- a/src/app/(modules)/plans/components/PlanSheet.tsx +++ b/src/app/(modules)/plans/components/PlanSheet.tsx @@ -93,7 +93,7 @@ export function PlanSheet({ placeholder="Basic plan for small teams" {...register('description', { required: true })} required - className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 outline-none focus-visible:border-ring" /> diff --git a/src/app/(modules)/plans/components/PlanTable.tsx b/src/app/(modules)/plans/components/PlanTable.tsx index a71c3c9..9a4dede 100644 --- a/src/app/(modules)/plans/components/PlanTable.tsx +++ b/src/app/(modules)/plans/components/PlanTable.tsx @@ -2,8 +2,10 @@ import type { ReactNode } from 'react'; import type { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Edit, RotateCcw } from 'lucide-react'; import { DataTable } from '@/components/data-table'; +import { PERMISSIONS } from '@/constants/permissions'; import type { Plan } from '@/types'; interface PlanTableProps { @@ -18,6 +20,9 @@ interface PlanTableProps { onPageChange: (skip: number) => void; onLimitChange: (limit: number) => void; onSortingChange: (sorting: SortingState) => void; + onEdit: (plan: Plan) => void; + onToggleStatus: (plan: Plan) => void; + pendingPlanId?: number; } export function PlanTable({ @@ -32,6 +37,9 @@ export function PlanTable({ onPageChange, onLimitChange, onSortingChange, + onEdit, + onToggleStatus, + pendingPlanId, }: PlanTableProps) { return ( , + permission: PERMISSIONS.PLAN.UPDATE, + onClick: onEdit, + }, + { + label: (plan) => (plan.is_active ? 'Deactivate' : 'Activate'), + icon: , + permission: PERMISSIONS.PLAN.DELETE, + disabled: (plan) => pendingPlanId === plan.id, + onClick: onToggleStatus, + }, + ]} pagination={{ skip, limit, diff --git a/src/app/(modules)/plans/page.tsx b/src/app/(modules)/plans/page.tsx index f71ae8c..2e855b1 100644 --- a/src/app/(modules)/plans/page.tsx +++ b/src/app/(modules)/plans/page.tsx @@ -57,15 +57,8 @@ export default function PlansPage() { onSaved: () => handleSheetOpenChange(false), }); const { openCreate: prepareCreatePlan, openEdit: prepareEditPlan } = planForm; - const { - register, - control, - onSubmit, - planId, - permissionIds, - setPermissionIds, - isSaving, - } = planForm; + const { register, control, onSubmit, planId, permissionIds, setPermissionIds, isSaving } = + planForm; const statusMutation = usePlanStatusMutation(); const openCreate = useCallback(() => { @@ -123,11 +116,7 @@ export default function PlansPage() { [statusMutation], ); - const columns = usePlanColumns({ - onEdit: openEdit, - onToggleStatus: toggleStatus, - pendingPlanId: statusMutation.variables?.id, - }); + const columns = usePlanColumns(); const plans = plansQuery.data?.items ?? []; const total = plansQuery.data?.total ?? plansQuery.data?.totalItems ?? 0; @@ -172,6 +161,9 @@ export default function PlansPage() { onPageChange={setSkip} onLimitChange={setLimit} onSortingChange={setSorting} + onEdit={openEdit} + onToggleStatus={toggleStatus} + pendingPlanId={statusMutation.variables?.id} /> diff --git a/src/app/(modules)/project/components/ProjectDialog.tsx b/src/app/(modules)/project/components/ProjectDialog.tsx index 06f7fc0..94d35fa 100644 --- a/src/app/(modules)/project/components/ProjectDialog.tsx +++ b/src/app/(modules)/project/components/ProjectDialog.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ComponentProps } from 'react'; -import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form'; +import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import { Layers, Loader2, MapPin, Route } from 'lucide-react'; import { FormField } from '@/components/form'; @@ -24,7 +24,7 @@ interface ProjectDialogProps { projectId?: string; register: UseFormRegister; errors: FieldErrors; - touchedFields: UseFormReturn['formState']['touchedFields']; + isSubmitted: boolean; onSubmit: ComponentProps<'form'>['onSubmit']; canSubmit: boolean; isSaving: boolean; @@ -36,16 +36,16 @@ export function ProjectDialog({ projectId, register, errors, - touchedFields, + isSubmitted, onSubmit, canSubmit, isSaving, }: ProjectDialogProps) { - const nameErrorMessage = touchedFields.name ? errors.name?.message : undefined; - const startLatErrorMessage = touchedFields.start_lat ? errors.start_lat?.message : undefined; - const startLngErrorMessage = touchedFields.start_lng ? errors.start_lng?.message : undefined; - const endLatErrorMessage = touchedFields.end_lat ? errors.end_lat?.message : undefined; - const endLngErrorMessage = touchedFields.end_lng ? errors.end_lng?.message : undefined; + const nameErrorMessage = isSubmitted ? errors.name?.message : undefined; + const startLatErrorMessage = isSubmitted ? errors.start_lat?.message : undefined; + const startLngErrorMessage = isSubmitted ? errors.start_lng?.message : undefined; + const endLatErrorMessage = isSubmitted ? errors.end_lat?.message : undefined; + const endLngErrorMessage = isSubmitted ? errors.end_lng?.message : undefined; return ( diff --git a/src/app/(modules)/project/components/ProjectTable.tsx b/src/app/(modules)/project/components/ProjectTable.tsx index 4a9b6e3..651a319 100644 --- a/src/app/(modules)/project/components/ProjectTable.tsx +++ b/src/app/(modules)/project/components/ProjectTable.tsx @@ -1,6 +1,7 @@ 'use client'; import type { ColumnDef } from '@tanstack/react-table'; +import { Edit3, Trash2 } from 'lucide-react'; import { DataTable } from '@/components/data-table'; import type { Project } from '@/types'; @@ -35,8 +36,19 @@ export function ProjectTable({ title="Projects" data={projects} columns={columns} - onEdit={onEdit} - onDelete={onDelete} + actions={[ + { + label: 'Edit', + icon: , + onClick: onEdit, + }, + { + label: 'Delete', + icon: , + className: 'text-destructive', + onClick: onDelete, + }, + ]} isLoading={isLoading} emptyTitle="No projects found." pagination={{ diff --git a/src/app/(modules)/project/hooks/useProjectForm.ts b/src/app/(modules)/project/hooks/useProjectForm.ts index 234832d..cae59fd 100644 --- a/src/app/(modules)/project/hooks/useProjectForm.ts +++ b/src/app/(modules)/project/hooks/useProjectForm.ts @@ -72,10 +72,10 @@ export function useProjectForm({ onSaved }: { onSaved: () => void }) { handleSubmit, reset, control, - formState: { errors, isSubmitting, touchedFields }, + formState: { errors, isSubmitting, isSubmitted }, } = useForm({ defaultValues, - mode: 'onTouched', + mode: 'onSubmit', reValidateMode: 'onChange', resolver: zodResolver(projectFormSchema), }); @@ -139,7 +139,7 @@ export function useProjectForm({ onSaved }: { onSaved: () => void }) { resetForm: () => reset(defaultValues), projectId, errors, - touchedFields, + isSubmitted, canSubmit, isSaving: isSubmitting || saveMutation.isPending, }; diff --git a/src/app/(modules)/project/page.tsx b/src/app/(modules)/project/page.tsx index 4b802af..aa729a4 100644 --- a/src/app/(modules)/project/page.tsx +++ b/src/app/(modules)/project/page.tsx @@ -30,7 +30,7 @@ export default function ProjectPage() { resetForm, projectId, errors, - touchedFields, + isSubmitted, canSubmit, isSaving, } = projectForm; @@ -107,7 +107,7 @@ export default function ProjectPage() { projectId={projectId} register={register} errors={errors} - touchedFields={touchedFields} + isSubmitted={isSubmitted} onSubmit={onSubmit} canSubmit={canSubmit} isSaving={isSaving} diff --git a/src/app/(modules)/roles/components/RoleColumns.tsx b/src/app/(modules)/roles/components/RoleColumns.tsx index 481df75..f50d6b1 100644 --- a/src/app/(modules)/roles/components/RoleColumns.tsx +++ b/src/app/(modules)/roles/components/RoleColumns.tsx @@ -2,31 +2,13 @@ import { useMemo } from 'react'; import type { ColumnDef } from '@tanstack/react-table'; -import { Edit, RotateCcw } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { PERMISSIONS } from '@/constants/permissions'; -import { usePermissions } from '@/hooks/usePermissions'; import type { Role } from '@/types'; -interface UseRoleColumnsParams { - onEdit: (role: Role) => void; - onToggleStatus: (role: Role) => void; - isStatusPending: boolean; -} - -export function useRoleColumns({ - onEdit, - onToggleStatus, - isStatusPending, -}: UseRoleColumnsParams): ColumnDef[] { - const { hasPermission } = usePermissions(); - const canEdit = hasPermission(PERMISSIONS.ROLE.UPDATE); - const canDelete = hasPermission(PERMISSIONS.ROLE.DELETE); - +export function useRoleColumns(): ColumnDef[] { return useMemo(() => { - const columns: ColumnDef[] = [ + return [ { accessorKey: 'name', header: 'Name', @@ -60,38 +42,5 @@ export function useRoleColumns({ ), }, ]; - - if (!canEdit && !canDelete) return columns; - - columns.push({ - id: 'actions', - header: () =>
Actions
, - enableSorting: false, - cell: ({ row }) => { - const role = row.original; - return ( -
- {canEdit ? ( - - ) : null} - {canDelete ? ( - - ) : null} -
- ); - }, - }); - - return columns; - }, [canDelete, canEdit, isStatusPending, onEdit, onToggleStatus]); + }, []); } diff --git a/src/app/(modules)/roles/components/RoleSheet.tsx b/src/app/(modules)/roles/components/RoleSheet.tsx index 46c0dca..ccb7cdb 100644 --- a/src/app/(modules)/roles/components/RoleSheet.tsx +++ b/src/app/(modules)/roles/components/RoleSheet.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ComponentProps } from 'react'; -import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form'; +import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import { Loader2 } from 'lucide-react'; import { FormField } from '@/components/form'; @@ -28,7 +28,7 @@ interface RoleSheetProps { roleId?: number; register: UseFormRegister; errors: FieldErrors; - touchedFields: UseFormReturn['formState']['touchedFields']; + isSubmitted: boolean; onSubmit: ComponentProps<'form'>['onSubmit']; permissionTree: PermissionTreeItem[]; permissionIds: number[]; @@ -44,7 +44,7 @@ export function RoleSheet({ roleId, register, errors, - touchedFields, + isSubmitted, onSubmit, permissionTree, permissionIds, @@ -53,10 +53,8 @@ export function RoleSheet({ canSubmit, isSaving, }: RoleSheetProps) { - const nameErrorMessage = touchedFields.name ? errors.name?.message : undefined; - const displayNameErrorMessage = touchedFields.display_name - ? errors.display_name?.message - : undefined; + const nameErrorMessage = isSubmitted ? errors.name?.message : undefined; + const displayNameErrorMessage = isSubmitted ? errors.display_name?.message : undefined; return ( @@ -74,12 +72,7 @@ export function RoleSheet({
- + {permissionIds.length} selected diff --git a/src/app/(modules)/roles/components/RoleTable.tsx b/src/app/(modules)/roles/components/RoleTable.tsx index 569396e..66551b9 100644 --- a/src/app/(modules)/roles/components/RoleTable.tsx +++ b/src/app/(modules)/roles/components/RoleTable.tsx @@ -2,8 +2,10 @@ import type { ColumnDef, SortingState } from '@tanstack/react-table'; import type { ReactNode } from 'react'; +import { Edit, RotateCcw } from 'lucide-react'; import { DataTable } from '@/components/data-table'; +import { PERMISSIONS } from '@/constants/permissions'; import type { Role } from '@/types'; interface RoleTableProps { @@ -18,6 +20,9 @@ interface RoleTableProps { onPageChange: (skip: number) => void; onLimitChange: (limit: number) => void; onSortingChange: (sorting: SortingState) => void; + onEdit: (role: Role) => void; + onToggleStatus: (role: Role) => void; + isStatusPending: boolean; } export function RoleTable({ @@ -32,6 +37,9 @@ export function RoleTable({ onPageChange, onLimitChange, onSortingChange, + onEdit, + onToggleStatus, + isStatusPending, }: RoleTableProps) { return ( , + permission: PERMISSIONS.ROLE.UPDATE, + onClick: onEdit, + }, + { + label: (role) => (role.effective_status ? 'Deactivate' : 'Activate'), + icon: , + permission: PERMISSIONS.ROLE.DELETE, + disabled: () => isStatusPending, + onClick: onToggleStatus, + }, + ]} pagination={{ skip, limit, diff --git a/src/app/(modules)/roles/hooks/useRoleForm.ts b/src/app/(modules)/roles/hooks/useRoleForm.ts index 351e774..8d0e370 100644 --- a/src/app/(modules)/roles/hooks/useRoleForm.ts +++ b/src/app/(modules)/roles/hooks/useRoleForm.ts @@ -48,10 +48,10 @@ export function useRoleForm({ handleSubmit, reset, setValue, - formState: { errors, isSubmitting, touchedFields }, + formState: { errors, isSubmitting, isSubmitted }, } = useForm({ defaultValues, - mode: 'onTouched', + mode: 'onSubmit', reValidateMode: 'onChange', resolver: zodResolver(roleFormSchema), }); @@ -136,7 +136,7 @@ export function useRoleForm({ permissionIds, roleId, errors, - touchedFields, + isSubmitted, canSubmit, isSaving: isSubmitting || saveMutation.isPending, }; diff --git a/src/app/(modules)/roles/page.tsx b/src/app/(modules)/roles/page.tsx index 647e6e1..77fc983 100644 --- a/src/app/(modules)/roles/page.tsx +++ b/src/app/(modules)/roles/page.tsx @@ -61,7 +61,7 @@ export default function RolesPage() { permissionIds, setPermissionIds, errors, - touchedFields, + isSubmitted, canSubmit, isSaving, } = roleForm; @@ -88,11 +88,7 @@ export default function RolesPage() { [updateRoleStatus], ); - const columns = useRoleColumns({ - onEdit: openEdit, - onToggleStatus: toggleStatus, - isStatusPending, - }); + const columns = useRoleColumns(); const total = rolesQuery.data?.total ?? 0; const roles = rolesQuery.data?.items ?? []; @@ -134,6 +130,9 @@ export default function RolesPage() { onPageChange={setSkip} onLimitChange={setLimit} onSortingChange={setSorting} + onEdit={openEdit} + onToggleStatus={toggleStatus} + isStatusPending={isStatusPending} /> @@ -143,7 +142,7 @@ export default function RolesPage() { roleId={roleId} register={register} errors={errors} - touchedFields={touchedFields} + isSubmitted={isSubmitted} onSubmit={onSubmit} permissionTree={permissionsQuery.permissionTree} permissionIds={permissionIds} diff --git a/src/app/(modules)/segment/components/SegmentDialog.tsx b/src/app/(modules)/segment/components/SegmentDialog.tsx index a44440c..70fc9fb 100644 --- a/src/app/(modules)/segment/components/SegmentDialog.tsx +++ b/src/app/(modules)/segment/components/SegmentDialog.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ComponentProps } from 'react'; -import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form'; +import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import { Loader2, MapPin, Milestone } from 'lucide-react'; import { FormField } from '@/components/form'; @@ -42,7 +42,7 @@ interface SegmentDialogProps { onDirectionChange: (direction: 'UP' | 'DOWN') => void; register: UseFormRegister; errors: FieldErrors; - touchedFields: UseFormReturn['formState']['touchedFields']; + isSubmitted: boolean; onSubmit: ComponentProps<'form'>['onSubmit']; canSubmit: boolean; isSaving: boolean; @@ -64,13 +64,13 @@ export function SegmentDialog({ onDirectionChange, register, errors, - touchedFields, + isSubmitted, onSubmit, canSubmit, isSaving, }: SegmentDialogProps) { const getError = (field: keyof SegmentFormValues) => - touchedFields[field] ? errors[field]?.message : undefined; + isSubmitted ? errors[field]?.message : undefined; return ( @@ -131,7 +131,9 @@ export function SegmentDialog({ Loading... ) : ( - + )} @@ -278,7 +280,12 @@ export function SegmentDialog({
- - ) : null} - {canDelete ? ( - - ) : null} -
- ); - }, - }); - - return columns; - }, [canDelete, canEdit, onDelete, onEdit, pendingDeleteId]); + }, []); } diff --git a/src/app/(modules)/tenants/components/TenantSheet.tsx b/src/app/(modules)/tenants/components/TenantSheet.tsx index f8bc15f..d881013 100644 --- a/src/app/(modules)/tenants/components/TenantSheet.tsx +++ b/src/app/(modules)/tenants/components/TenantSheet.tsx @@ -108,7 +108,9 @@ export function TenantSheet({ - + {plans.map((plan) => ( @@ -147,7 +151,7 @@ export function TenantSheet({ id="tenant-description" placeholder="North zone operations" {...register('description')} - className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 outline-none focus-visible:border-ring" /> diff --git a/src/app/(modules)/tenants/components/TenantTable.tsx b/src/app/(modules)/tenants/components/TenantTable.tsx index b7fb22f..da2d73c 100644 --- a/src/app/(modules)/tenants/components/TenantTable.tsx +++ b/src/app/(modules)/tenants/components/TenantTable.tsx @@ -2,8 +2,10 @@ import type { ReactNode } from 'react'; import type { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Edit, Trash2 } from 'lucide-react'; import { DataTable } from '@/components/data-table'; +import { PERMISSIONS } from '@/constants/permissions'; import type { Tenant } from '@/types'; interface TenantTableProps { @@ -18,6 +20,9 @@ interface TenantTableProps { onPageChange: (skip: number) => void; onLimitChange: (limit: number) => void; onSortingChange: (sorting: SortingState) => void; + onEdit: (tenant: Tenant) => void; + onDelete: (tenant: Tenant) => void; + pendingDeleteId?: number; } export function TenantTable({ @@ -32,6 +37,9 @@ export function TenantTable({ onPageChange, onLimitChange, onSortingChange, + onEdit, + onDelete, + pendingDeleteId, }: TenantTableProps) { return ( , + permission: PERMISSIONS.TENANT.UPDATE, + onClick: onEdit, + }, + { + label: 'Delete', + icon: , + permission: PERMISSIONS.TENANT.DELETE, + className: 'text-destructive', + disabled: (tenant) => pendingDeleteId === tenant.id, + onClick: onDelete, + }, + ]} pagination={{ skip, limit, diff --git a/src/app/(modules)/tenants/page.tsx b/src/app/(modules)/tenants/page.tsx index 8b9982f..c14f6c1 100644 --- a/src/app/(modules)/tenants/page.tsx +++ b/src/app/(modules)/tenants/page.tsx @@ -116,11 +116,7 @@ export default function TenantsPage() { [deleteMutation], ); - const columns = useTenantColumns({ - onEdit: openEdit, - onDelete: handleDelete, - pendingDeleteId: deleteMutation.isPending ? deleteMutation.variables : undefined, - }); + const columns = useTenantColumns(); const tenants = tenantsQuery.data?.items ?? []; const total = tenantsQuery.data?.total ?? 0; @@ -169,6 +165,9 @@ export default function TenantsPage() { onPageChange={setSkip} onLimitChange={setLimit} onSortingChange={setSorting} + onEdit={openEdit} + onDelete={handleDelete} + pendingDeleteId={deleteMutation.isPending ? deleteMutation.variables : undefined} /> diff --git a/src/app/(modules)/users/components/UserColumns.tsx b/src/app/(modules)/users/components/UserColumns.tsx index 6301c26..01584c9 100644 --- a/src/app/(modules)/users/components/UserColumns.tsx +++ b/src/app/(modules)/users/components/UserColumns.tsx @@ -2,12 +2,8 @@ import { useMemo } from 'react'; import type { ColumnDef } from '@tanstack/react-table'; -import { Edit, RotateCcw } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { PERMISSIONS } from '@/constants/permissions'; -import { usePermissions } from '@/hooks/usePermissions'; import type { AdministrationUser } from '@/types'; function formatDate(value?: string | null) { @@ -19,23 +15,9 @@ function formatDate(value?: string | null) { }).format(new Date(value)); } -interface UseUserColumnsParams { - onEdit: (user: AdministrationUser) => void; - onToggleStatus: (user: AdministrationUser) => void; - pendingUserId?: number; -} - -export function useUserColumns({ - onEdit, - onToggleStatus, - pendingUserId, -}: UseUserColumnsParams): ColumnDef[] { - const { hasPermission } = usePermissions(); - const canEdit = hasPermission(PERMISSIONS.USER.UPDATE); - const canDelete = hasPermission(PERMISSIONS.USER.DELETE); - +export function useUserColumns(): ColumnDef[] { return useMemo(() => { - const columns: ColumnDef[] = [ + return [ { accessorKey: 'first_name', header: 'Name', @@ -88,38 +70,5 @@ export function useUserColumns({ ), }, ]; - - if (!canEdit && !canDelete) return columns; - - columns.push({ - id: 'actions', - header: () =>
Actions
, - enableSorting: false, - cell: ({ row }) => { - const user = row.original; - return ( -
- {canEdit ? ( - - ) : null} - {canDelete ? ( - - ) : null} -
- ); - }, - }); - - return columns; - }, [canDelete, canEdit, onEdit, onToggleStatus, pendingUserId]); + }, []); } diff --git a/src/app/(modules)/users/components/UserSheet.tsx b/src/app/(modules)/users/components/UserSheet.tsx index 34632be..f4a458d 100644 --- a/src/app/(modules)/users/components/UserSheet.tsx +++ b/src/app/(modules)/users/components/UserSheet.tsx @@ -1,7 +1,7 @@ 'use client'; import { useRef, type ComponentProps } from 'react'; -import type { FieldErrors, UseFormRegister, UseFormReturn } from 'react-hook-form'; +import type { FieldErrors, UseFormRegister } from 'react-hook-form'; import { Loader2 } from 'lucide-react'; import { FormField } from '@/components/form'; @@ -24,7 +24,7 @@ interface UserSheetProps { userId?: number; register: UseFormRegister; errors: FieldErrors; - touchedFields: UseFormReturn['formState']['touchedFields']; + isSubmitted: boolean; onSubmit: ComponentProps<'form'>['onSubmit']; roleId: string; onRoleChange: (roleId: string) => void; @@ -38,7 +38,7 @@ export function UserSheet({ userId, register, errors, - touchedFields, + isSubmitted, onSubmit, roleId, onRoleChange, @@ -46,11 +46,11 @@ export function UserSheet({ isSaving, }: UserSheetProps) { const roleComboboxPortalRef = useRef(null); - const firstNameErrorMessage = touchedFields.first_name ? errors.first_name?.message : undefined; - const lastNameErrorMessage = touchedFields.last_name ? errors.last_name?.message : undefined; - const emailErrorMessage = touchedFields.email ? errors.email?.message : undefined; - const phoneErrorMessage = touchedFields.phone_number ? errors.phone_number?.message : undefined; - const roleErrorMessage = touchedFields.role_id ? errors.role_id?.message : undefined; + const firstNameErrorMessage = isSubmitted ? errors.first_name?.message : undefined; + const lastNameErrorMessage = isSubmitted ? errors.last_name?.message : undefined; + const emailErrorMessage = isSubmitted ? errors.email?.message : undefined; + const phoneErrorMessage = isSubmitted ? errors.phone_number?.message : undefined; + const roleErrorMessage = isSubmitted ? errors.role_id?.message : undefined; return ( @@ -63,12 +63,7 @@ export function UserSheet({
- + - +
- + - + - + diff --git a/src/app/(modules)/users/components/UserTable.tsx b/src/app/(modules)/users/components/UserTable.tsx index cfd1795..2a0c164 100644 --- a/src/app/(modules)/users/components/UserTable.tsx +++ b/src/app/(modules)/users/components/UserTable.tsx @@ -2,8 +2,10 @@ import type { ReactNode } from 'react'; import type { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Edit, RotateCcw } from 'lucide-react'; import { DataTable } from '@/components/data-table'; +import { PERMISSIONS } from '@/constants/permissions'; import type { AdministrationUser } from '@/types'; interface UserTableProps { @@ -18,6 +20,9 @@ interface UserTableProps { onPageChange: (skip: number) => void; onLimitChange: (limit: number) => void; onSortingChange: (sorting: SortingState) => void; + onEdit: (user: AdministrationUser) => void; + onToggleStatus: (user: AdministrationUser) => void; + pendingUserId?: number; } export function UserTable({ @@ -32,6 +37,9 @@ export function UserTable({ onPageChange, onLimitChange, onSortingChange, + onEdit, + onToggleStatus, + pendingUserId, }: UserTableProps) { return ( , + permission: PERMISSIONS.USER.UPDATE, + onClick: onEdit, + }, + { + label: (user) => (user.effective_status === 'active' ? 'Deactivate' : 'Activate'), + icon: , + permission: PERMISSIONS.USER.DELETE, + disabled: (user) => pendingUserId === user.id || user.effective_status === 'pending', + onClick: onToggleStatus, + }, + ]} pagination={{ skip, limit, diff --git a/src/app/(modules)/users/hooks/useUserForm.ts b/src/app/(modules)/users/hooks/useUserForm.ts index a66d4f9..eb135a0 100644 --- a/src/app/(modules)/users/hooks/useUserForm.ts +++ b/src/app/(modules)/users/hooks/useUserForm.ts @@ -42,10 +42,10 @@ export function useUserForm({ onSaved }: { onSaved: () => void }) { handleSubmit: submitForm, reset, setValue, - formState: { errors, isSubmitting, touchedFields }, + formState: { errors, isSubmitting, isSubmitted }, } = useForm({ defaultValues, - mode: 'onTouched', + mode: 'onSubmit', reValidateMode: 'onChange', resolver: zodResolver(userFormSchema), }); @@ -62,7 +62,8 @@ export function useUserForm({ onSaved }: { onSaved: () => void }) { const lastName = useWatch({ control, name: 'last_name' }) || ''; const email = useWatch({ control, name: 'email' }) || ''; const phoneNumber = useWatch({ control, name: 'phone_number' }) || ''; - const isPhoneValid = phoneNumber.trim() === '' || /^\+(?:[0-9] ?){6,14}[0-9]$/.test(phoneNumber.trim()); + const isPhoneValid = + phoneNumber.trim() === '' || /^\+(?:[0-9] ?){6,14}[0-9]$/.test(phoneNumber.trim()); const canSubmit = firstName.trim().length > 0 && lastName.trim().length > 0 && @@ -131,7 +132,7 @@ export function useUserForm({ onSaved }: { onSaved: () => void }) { roleId, userId, errors, - touchedFields, + isSubmitted, canSubmit, isSaving: isSubmitting || saveMutation.isPending, }; diff --git a/src/app/(modules)/users/page.tsx b/src/app/(modules)/users/page.tsx index f0e5616..11e1d2e 100644 --- a/src/app/(modules)/users/page.tsx +++ b/src/app/(modules)/users/page.tsx @@ -54,7 +54,7 @@ export default function UsersPage() { roleId, setRoleId, errors, - touchedFields, + isSubmitted, canSubmit, isSaving, } = userForm; @@ -81,11 +81,7 @@ export default function UsersPage() { [updateUserStatus], ); - const columns = useUserColumns({ - onEdit: openEdit, - onToggleStatus: toggleStatus, - pendingUserId, - }); + const columns = useUserColumns(); const total = usersQuery.data?.total ?? 0; const users = usersQuery.data?.items ?? []; @@ -137,6 +133,9 @@ export default function UsersPage() { onPageChange={setSkip} onLimitChange={setLimit} onSortingChange={setSorting} + onEdit={openEdit} + onToggleStatus={toggleStatus} + pendingUserId={pendingUserId} /> @@ -146,7 +145,7 @@ export default function UsersPage() { userId={userId} register={register} errors={errors} - touchedFields={touchedFields} + isSubmitted={isSubmitted} onSubmit={handleSubmit} roleId={roleId} onRoleChange={setRoleId} diff --git a/src/app/globals.css b/src/app/globals.css index fbf551d..8baa5e0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -24,13 +24,13 @@ --border: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32); --ring: oklch(0.702 0.183 293.541); - --chart-1: oklch(0.65 0.18 25); /* Soft Red/Rose - High Priority */ - --chart-2: oklch(0.78 0.12 75); /* Warm Amber - Distinct */ - --chart-3: oklch(0.68 0.12 245); /* Azure Blue - Cool Professional */ - --chart-4: oklch(0.75 0.1 165); /* Mint Teal - Balanced */ - --chart-5: oklch(0.85 0.08 195); /* Soft Cyan - Subdued */ - --chart-6: oklch(0.62 0.22 295); /* Deep Violet - Theme Primary */ - --chart-8: oklch(0.58 0.2 335); /* Cool Magenta - Distant Accent */ + --chart-1: oklch(0.65 0.18 25); /* Soft Red/Rose - High Priority */ + --chart-2: oklch(0.78 0.12 75); /* Warm Amber - Distinct */ + --chart-3: oklch(0.68 0.12 245); /* Azure Blue - Cool Professional */ + --chart-4: oklch(0.75 0.1 165); /* Mint Teal - Balanced */ + --chart-5: oklch(0.85 0.08 195); /* Soft Cyan - Subdued */ + --chart-6: oklch(0.62 0.22 295); /* Deep Violet - Theme Primary */ + --chart-8: oklch(0.58 0.2 335); /* Cool Magenta - Distant Accent */ --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.141 0.005 285.823); --sidebar-primary: oklch(0.541 0.281 293.009); @@ -123,7 +123,7 @@ @layer base { * { - @apply border-border outline-ring/50; + @apply border-border outline-none; } body { diff --git a/src/components/data-table/TableActionButton.tsx b/src/components/data-table/TableActionButton.tsx new file mode 100644 index 0000000..215e2a3 --- /dev/null +++ b/src/components/data-table/TableActionButton.tsx @@ -0,0 +1,48 @@ +'use client'; + +import type { MouseEvent, ReactNode } from 'react'; + +import { PermissionGuard } from '@/guards'; +import type { PermissionInput } from '@/hooks/usePermissions'; +import { Button } from '@/components/ui/button'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; + +interface TableActionButtonProps { + label: string; + permission?: PermissionInput; + onClick?: (event: MouseEvent) => void; + disabled?: boolean; + className?: string; + children: ReactNode; +} + +export function TableActionButton({ + label, + permission, + onClick, + disabled, + className, + children, +}: TableActionButtonProps) { + return ( + + + + + + {label} + + + ); +} diff --git a/src/components/data-table/index.tsx b/src/components/data-table/index.tsx index c645998..a87932f 100644 --- a/src/components/data-table/index.tsx +++ b/src/components/data-table/index.tsx @@ -12,12 +12,22 @@ import { import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'; import { Skeleton } from '@/components/ui/skeleton'; -import { Button } from '@/components/ui/button'; -import { Edit3, Trash2 } from 'lucide-react'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import type { PermissionInput } from '@/hooks/usePermissions'; import TopHeader from './Header'; import TableHeader from './TableHeader'; import { TableFooter } from './Footer'; +import { TableActionButton } from './TableActionButton'; + +export interface DataTableAction { + label: string | ((item: TData) => string); + icon: React.ReactNode; + onClick: (item: TData) => void; + permission?: PermissionInput; + disabled?: (item: TData) => boolean; + className?: string; +} export interface DataTableProps { columns: ColumnDef[]; @@ -26,8 +36,7 @@ export interface DataTableProps { onAddNew?: () => void; addButtonText?: string; isLoading?: boolean; - onEdit?: (item: TData) => void; - onDelete?: (item: TData) => void; + actions?: DataTableAction[]; toolbar?: React.ReactNode; emptyTitle?: string; emptyDescription?: string; @@ -49,8 +58,7 @@ export function DataTable({ onAddNew, addButtonText, isLoading = false, - onEdit, - onDelete, + actions, toolbar, emptyTitle = 'No results found.', emptyDescription = 'Try adjusting your filters or search terms.', @@ -77,7 +85,7 @@ export function DataTable({ const columns = React.useMemo(() => { const cols: ColumnDef[] = [...initialColumns]; - if (onEdit || onDelete) { + if (actions?.length) { cols.push({ id: 'actions', header: () =>
Actions
, @@ -85,37 +93,33 @@ export function DataTable({ const item = row.original; return (
- {onEdit && ( - - )} - {onDelete && ( - - )} + {actions.map((action) => { + const label = + typeof action.label === 'function' ? action.label(item) : action.label; + + return ( + { + event.stopPropagation(); + action.onClick(item); + }} + > + {action.icon} + + ); + })}
); }, }); } return cols; - }, [initialColumns, onEdit, onDelete]); + }, [actions, initialColumns]); const table = useReactTable({ data, @@ -158,7 +162,8 @@ export function DataTable({ }); return ( -
+ +
({ )) ) : table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -216,6 +218,7 @@ export function DataTable({ pageSize={pagination?.limit} onPageSizeChange={pagination?.onLimitChange} /> -
+
+ ); } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 6e0a314..3554d74 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -5,14 +5,14 @@ import { Slot } from 'radix-ui'; import { cn } from '@/lib/utils'; const badgeVariants = cva( - 'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3', + 'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] outline-none aria-invalid:border-destructive [&>svg]:pointer-events-none [&>svg]:size-3', { variants: { variant: { default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90', secondary: 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', destructive: - 'bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90', + 'bg-destructive text-white dark:bg-destructive/60 [a&]:hover:bg-destructive/90', outline: 'border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', ghost: '[a&]:hover:bg-accent [a&]:hover:text-accent-foreground', diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 4d38506..373e101 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,54 +1,51 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { Slot } from 'radix-ui'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; const buttonVariants = cva( - "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-white hover:bg-destructive/90 dark:bg-destructive/60', outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground focus-visible:border-ring dark:border-input dark:bg-input/30 dark:hover:bg-input/50', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", + default: 'h-9 px-4 py-2 has-[>svg]:px-3', xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8", - "icon-lg": "size-10", + sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + 'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + 'icon-sm': 'size-8', + 'icon-lg': 'size-10', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, - } -) + }, +); function Button({ className, - variant = "default", - size = "default", + variant = 'default', + size = 'default', asChild = false, ...props -}: React.ComponentProps<"button"> & +}: React.ComponentProps<'button'> & VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot.Root : "button" + const Comp = asChild ? Slot.Root : 'button'; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx index 772b836..80ebd2b 100644 --- a/src/components/ui/combobox.tsx +++ b/src/components/ui/combobox.tsx @@ -1,42 +1,58 @@ -"use client" +'use client'; -import * as React from "react" -import { Combobox as ComboboxPrimitive } from "@base-ui/react" -import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react" +import * as React from 'react'; +import { Combobox as ComboboxPrimitive } from '@base-ui/react'; +import { CheckIcon, ChevronDownIcon, XIcon } from 'lucide-react'; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, -} from "@/components/ui/input-group" +} from '@/components/ui/input-group'; -const Combobox = ComboboxPrimitive.Root +const Combobox = ComboboxPrimitive.Root; function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { - return + return ; } function ComboboxTrigger({ className, children, + render, ...props }: ComboboxPrimitive.Trigger.Props) { + const icon = ( + + ); + const triggerRender = React.isValidElement<{ children?: React.ReactNode }>(render) + ? React.cloneElement(render, { + children: ( + <> + {render.props.children} + {icon} + + ), + }) + : render; + return ( {children} - + {render ? null : icon} - ) + ); } function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { @@ -49,7 +65,7 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { > - ) + ); } function ComboboxInput({ @@ -60,15 +76,12 @@ function ComboboxInput({ showClear = false, ...props }: ComboboxPrimitive.Input.Props & { - showTrigger?: boolean - showClear?: boolean + showTrigger?: boolean; + showClear?: boolean; }) { return ( - - } - {...props} - /> + + } {...props} /> {showTrigger && ( {children} - ) + ); } function ComboboxContent({ className, - side = "bottom", + side = 'bottom', sideOffset = 6, - align = "start", + align = 'start', alignOffset = 0, anchor, container, @@ -101,9 +114,9 @@ function ComboboxContent({ }: ComboboxPrimitive.Popup.Props & Pick< ComboboxPrimitive.Positioner.Props, - "side" | "align" | "sideOffset" | "alignOffset" | "anchor" + 'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor' > & { - container?: ComboboxPrimitive.Portal.Props["container"] + container?: ComboboxPrimitive.Portal.Props['container']; }) { return ( @@ -119,14 +132,14 @@ function ComboboxContent({ data-slot="combobox-content" data-chips={!!anchor} className={cn( - "group/combobox-content relative max-h-96 w-[var(--anchor-width)] max-w-[var(--available-width)] min-w-[calc(var(--anchor-width)+1.75rem)] origin-[var(--transform-origin)] overflow-hidden rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-[var(--anchor-width)] data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", - className + 'group/combobox-content relative max-h-96 w-[var(--anchor-width)] max-w-[var(--available-width)] min-w-[calc(var(--anchor-width)+1.75rem)] origin-[var(--transform-origin)] overflow-hidden rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-[var(--anchor-width)] data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95', + className, )} {...props} /> - ) + ); } function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { @@ -134,25 +147,21 @@ function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { - ) + ); } -function ComboboxItem({ - className, - children, - ...props -}: ComboboxPrimitive.Item.Props) { +function ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) { return ( @@ -166,39 +175,30 @@ function ComboboxItem({ - ) + ); } function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { return ( - - ) + + ); } -function ComboboxLabel({ - className, - ...props -}: ComboboxPrimitive.GroupLabel.Props) { +function ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) { return ( - ) + ); } function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { - return ( - - ) + return ; } function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { @@ -206,42 +206,38 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { - ) + ); } -function ComboboxSeparator({ - className, - ...props -}: ComboboxPrimitive.Separator.Props) { +function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) { return ( - ) + ); } function ComboboxChips({ className, ...props -}: React.ComponentPropsWithRef & - ComboboxPrimitive.Chips.Props) { +}: React.ComponentPropsWithRef & ComboboxPrimitive.Chips.Props) { return ( - ) + ); } function ComboboxChip({ @@ -250,14 +246,14 @@ function ComboboxChip({ showRemove = true, ...props }: ComboboxPrimitive.Chip.Props & { - showRemove?: boolean + showRemove?: boolean; }) { return ( @@ -272,25 +268,21 @@ function ComboboxChip({ )} - ) + ); } -function ComboboxChipsInput({ - className, - children, - ...props -}: ComboboxPrimitive.Input.Props) { +function ComboboxChipsInput({ className, children, ...props }: ComboboxPrimitive.Input.Props) { return ( - ) + ); } function useComboboxAnchor() { - return React.useRef(null) + return React.useRef(null); } export { @@ -310,4 +302,4 @@ export { ComboboxTrigger, ComboboxValue, useComboboxAnchor, -} +}; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index e3138b0..5f36cf0 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,34 +1,26 @@ -"use client" +'use client'; -import * as React from "react" -import { XIcon } from "lucide-react" -import { Dialog as DialogPrimitive } from "radix-ui" +import * as React from 'react'; +import { XIcon } from 'lucide-react'; +import { Dialog as DialogPrimitive } from 'radix-ui'; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; -function Dialog({ - ...props -}: React.ComponentProps) { - return +function Dialog({ ...props }: React.ComponentProps) { + return ; } -function DialogTrigger({ - ...props -}: React.ComponentProps) { - return +function DialogTrigger({ ...props }: React.ComponentProps) { + return ; } -function DialogPortal({ - ...props -}: React.ComponentProps) { - return +function DialogPortal({ ...props }: React.ComponentProps) { + return ; } -function DialogClose({ - ...props -}: React.ComponentProps) { - return +function DialogClose({ ...props }: React.ComponentProps) { + return ; } function DialogOverlay({ @@ -39,12 +31,12 @@ function DialogOverlay({ - ) + ); } function DialogContent({ @@ -54,7 +46,7 @@ function DialogContent({ onInteractOutside, ...props }: React.ComponentProps & { - showCloseButton?: boolean + showCloseButton?: boolean; }) { return ( @@ -62,12 +54,12 @@ function DialogContent({ { - event.preventDefault() - onInteractOutside?.(event) + event.preventDefault(); + onInteractOutside?.(event); }} {...props} > @@ -75,7 +67,7 @@ function DialogContent({ {showCloseButton && ( Close @@ -83,17 +75,17 @@ function DialogContent({ )} - ) + ); } -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
- ) + ); } function DialogFooter({ @@ -101,16 +93,13 @@ function DialogFooter({ showCloseButton = false, children, ...props -}: React.ComponentProps<"div"> & { - showCloseButton?: boolean +}: React.ComponentProps<'div'> & { + showCloseButton?: boolean; }) { return (
{children} @@ -120,20 +109,11 @@ function DialogFooter({ )}
- ) + ); } -function DialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) +function DialogTitle({ className, ...props }: React.ComponentProps) { + return ; } function DialogDescription({ @@ -143,10 +123,10 @@ function DialogDescription({ return ( - ) + ); } export { @@ -160,4 +140,4 @@ export { DialogPortal, DialogTitle, DialogTrigger, -} +}; diff --git a/src/components/ui/input-group.tsx b/src/components/ui/input-group.tsx index a7652d9..c0cfff0 100644 --- a/src/components/ui/input-group.tsx +++ b/src/components/ui/input-group.tsx @@ -1,39 +1,39 @@ -"use client" +'use client'; -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; -function InputGroup({ className, ...props }: React.ComponentProps<"div">) { +function InputGroup({ className, ...props }: React.ComponentProps<'div'>) { return (
textarea]:h-auto", + 'group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30', + 'h-9 min-w-0 has-[>textarea]:h-auto', // Variants based on alignment. - "has-[>[data-align=inline-start]]:[&>input]:pl-2", - "has-[>[data-align=inline-end]]:[&>input]:pr-2", - "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3", - "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3", + 'has-[>[data-align=inline-start]]:[&>input]:pl-2', + 'has-[>[data-align=inline-end]]:[&>input]:pr-2', + 'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3', + 'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3', // Focus state. - "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50", + 'has-[[data-slot=input-group-control]:focus-visible]:border-ring', // Error state. - "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", + 'has-[[data-slot][aria-invalid=true]]:border-destructive', - className + className, )} {...props} /> - ) + ); } const inputGroupAddonVariants = cva( @@ -41,27 +41,25 @@ const inputGroupAddonVariants = cva( { variants: { align: { - "inline-start": - "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]", - "inline-end": - "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]", - "block-start": - "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3", - "block-end": - "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3", + 'inline-start': 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]', + 'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]', + 'block-start': + 'order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3', + 'block-end': + 'order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3', }, }, defaultVariants: { - align: "inline-start", + align: 'inline-start', }, - } -) + }, +); function InputGroupAddon({ className, - align = "inline-start", + align = 'inline-start', ...props -}: React.ComponentProps<"div"> & VariantProps) { +}: React.ComponentProps<'div'> & VariantProps) { return (
{ - if ((e.target as HTMLElement).closest("button")) { - return + if ((e.target as HTMLElement).closest('button')) { + return; } - e.currentTarget.parentElement?.querySelector("input")?.focus() + e.currentTarget.parentElement?.querySelector('input')?.focus(); }} {...props} /> - ) + ); } -const inputGroupButtonVariants = cva( - "flex items-center gap-2 text-sm shadow-none", - { - variants: { - size: { - xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5", - sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5", - "icon-xs": - "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0", - "icon-sm": "size-8 p-0 has-[>svg]:p-0", - }, +const inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', { + variants: { + size: { + xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5", + sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5', + 'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0', + 'icon-sm': 'size-8 p-0 has-[>svg]:p-0', }, - defaultVariants: { - size: "xs", - }, - } -) + }, + defaultVariants: { + size: 'xs', + }, +}); function InputGroupButton({ className, - type = "button", - variant = "ghost", - size = "xs", + type = 'button', + variant = 'ghost', + size = 'xs', ...props -}: Omit, "size"> & +}: Omit, 'size'> & VariantProps) { return (