This commit is contained in:
hamid zarghami
2025-07-15 14:57:54 +03:30
parent 558d03533b
commit ed8cd1392d
6 changed files with 504 additions and 361 deletions
@@ -149,10 +149,7 @@ const renderBlockToHTML = (block: Block): string => {
};
// Generate CSS for email clients
const generateEmailCSS = (
template: Template,
options: ExportOptions
): string => {
const generateEmailCSS = (options: ExportOptions): string => {
if (!options.includeCSS) return "";
return `
@@ -213,7 +210,7 @@ export const exportToHTML = (
): string => {
const finalOptions = { ...defaultExportOptions, ...options };
const css = generateEmailCSS(template, finalOptions);
const css = generateEmailCSS(finalOptions);
const bodyHTML = template.blocks
.map((block) => renderBlockToHTML(block))
.join("");
+30 -351
View File
@@ -1,26 +1,10 @@
import Input from '@/components/Input'
import Select from '@/components/Select'
import Table from '@/components/Table'
import { ColumnType } from '@/components/types/TableTypes'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/components/Button'
import { TickCircle, Edit2, Trash } from 'iconsax-react'
import { useForm } from 'react-hook-form'
import { AddressData, CreateAddressType, UpdateAddressType } from './types/Types'
import { useGetDomains } from '../domain/hooks/useDomainData'
import { useCreateAddress, useGetAddress, useUpdateAddress, useDeleteAddress } from './hooks/useAddressData'
import { useGetSpace } from '../domain/hooks/useDomainData'
import { toast } from '@/components/Toast'
import { ErrorType } from '@/helpers/types'
import ProgressBarEmail from './components/ProgressBarEmail'
import ToggleStatus from './components/ToggleStatus'
import RowActionsDropdown, { RowActionItem } from '@/components/RowActionsDropdown'
import ModalConfrim from '@/components/ModalConfrim'
import { AddressData } from './types/Types'
import AddressTable from './components/AddressTable'
import AddressForm from './components/AddressForm'
import AddressDeleteModal from './components/AddressDeleteModal'
const Address: FC = () => {
const { t } = useTranslation()
const [search, setSearch] = useState<string>('')
const [status, setStatus] = useState<string>('all')
const [currentPage, setCurrentPage] = useState<number>(1)
@@ -29,70 +13,9 @@ const Address: FC = () => {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [addressToDelete, setAddressToDelete] = useState<AddressData | null>(null)
const { data: domain } = useGetDomains()
const { data: spaceData } = useGetSpace()
const { mutate: createAddress, isPending } = useCreateAddress()
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress()
const { mutate: deleteAddress } = useDeleteAddress()
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status, currentPage, 10)
type FormData = CreateAddressType & { password?: string; quotaMB?: number }
const { register, handleSubmit, formState: { errors }, setValue, reset, setError, clearErrors
} = useForm<FormData>({
defaultValues: {
domainId: domain?.data?.domain?.id,
}
})
const validateForm = (params: FormData): boolean => {
clearErrors()
let isValid = true
if (!params.title) {
setError('title', { message: t('errors.required') })
isValid = false
}
if (!params.username) {
setError('username', { message: t('errors.required') })
isValid = false
}
if (!isEditMode && !params.password) {
setError('password', { message: t('errors.required') })
isValid = false
}
if (!params.domainId) {
setError('domainId', { message: t('errors.required') })
isValid = false
}
// اعتبارسنجی quota
if (params.quotaMB) {
const quotaInBytes = params.quotaMB * 1024 * 1024
const remainingSpace = spaceData?.data?.quota?.totalInGB * 1024 * 1024 * 1024 - spaceData?.data?.quota?.usedInGB * 1024 * 1024 * 1024
if (quotaInBytes > remainingSpace) {
setError('quotaMB', { message: 'فضای وارد شده بیش از فضای باقی‌مانده است' })
isValid = false
}
}
return isValid
}
const handleEdit = (address: AddressData) => {
setSelectedAddress(address)
setIsEditMode(true)
reset({
title: address.title,
username: address.displayName,
password: '',
domainId: address.domain.id,
quotaMB: address.emailQuota ? Math.round(address.emailQuota / (1024 * 1024)) : undefined,
})
}
const handleDelete = (address: AddressData) => {
@@ -100,153 +23,24 @@ const Address: FC = () => {
setIsDeleteModalOpen(true)
}
const handleConfirmDelete = () => {
if (addressToDelete) {
deleteAddress(addressToDelete.id, {
onSuccess: (data) => {
toast(data?.data?.message || 'آدرس با موفقیت حذف شد', 'success')
setIsDeleteModalOpen(false)
setAddressToDelete(null)
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0] || 'خطا در حذف', 'error')
setIsDeleteModalOpen(false)
setAddressToDelete(null)
}
})
}
}
const handleCloseDeleteModal = () => {
setIsDeleteModalOpen(false)
setAddressToDelete(null)
}
const handleCancelEdit = () => {
setSelectedAddress(null)
setIsEditMode(false)
reset({
title: '',
username: '',
password: '',
domainId: domain?.data?.domain?.id,
quotaMB: undefined,
})
}
const getRowActions = (address: AddressData): RowActionItem[] => [
{
label: 'ویرایش',
icon: <Edit2 color='black' size={16} />,
onClick: () => handleEdit(address),
},
{
label: 'حذف',
icon: <Trash color='red' size={16} />,
onClick: () => handleDelete(address),
className: 'text-red-600 hover:bg-red-50'
}
]
const columns: ColumnType<AddressData>[] = [
{
title: t('setting.address_title'),
key: 'title',
width: '200px'
},
{
title: t('setting.email'),
key: 'email',
width: '300px',
render: (item: AddressData) => (
<div key={item.id} className='dltr flex justify-end'>
{item.emailAddress}
</div>
)
},
{
title: t('setting.quota'),
key: 'quota',
width: '300px',
render: (item: AddressData) => {
return (
<ProgressBarEmail key={item.id} item={item} />
)
}
},
{
title: t('setting.status'),
key: 'status',
width: '120px',
align: 'center',
render: (item: AddressData) => (
<div key={item.id} className='dltr flex justify-end'>
<ToggleStatus defaultStatus={item.isActive} id={item.id} />
</div>
)
},
{
title: 'عملیات',
key: 'actions',
width: '120px',
align: 'center',
render: (item: AddressData) => (
<RowActionsDropdown
key={item.id}
actions={getRowActions(item)}
/>
)
}
]
const statusOptions = [
{ value: 'all', label: 'همه' },
{ value: 'active', label: t('setting.active') },
{ value: 'inactive', label: t('setting.inactive') }
]
const onSubmit = (params: FormData) => {
if (!validateForm(params)) {
return
const handleFormSuccess = () => {
setSelectedAddress(null)
setIsEditMode(false)
}
if (isEditMode && selectedAddress) {
const updateParams: UpdateAddressType = {
title: params.title,
password: params.password || undefined,
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
}
updateAddress({ id: selectedAddress.id, params: updateParams }, {
onSuccess: (data) => {
toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success')
handleCancelEdit()
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0] || 'خطا در آپدیت', 'error')
}
})
} else {
const createParams: CreateAddressType = {
...params,
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
}
createAddress(createParams, {
onSuccess: (data) => {
toast(data?.data?.message, 'success')
reset()
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
const handleDeleteSuccess = () => {
setAddressToDelete(null)
}
useEffect(() => {
if (domain?.data?.domain?.id && !isEditMode) {
setValue('domainId', domain?.data?.domain?.id)
const handleDeleteModalClose = () => {
setIsDeleteModalOpen(false)
setAddressToDelete(null)
}
}, [domain, isEditMode])
useEffect(() => {
setCurrentPage(1)
@@ -254,144 +48,29 @@ const Address: FC = () => {
return (
<div className='flex xl:flex-row flex-col-reverse gap-8 mt-8'>
<div className='flex-1'>
<div className='flex gap-2 justify-between'>
<div>
<Select
placeholder={t('setting.status')}
items={statusOptions}
className='w-[120px]'
onChange={(e) => setStatus(e.target.value)}
<AddressTable
search={search}
status={status}
currentPage={currentPage}
onSearchChange={setSearch}
onStatusChange={setStatus}
onPageChange={setCurrentPage}
onEdit={handleEdit}
onDelete={handleDelete}
/>
</div>
<div className='flex-1'>
<Input
variant='search'
placeholder={t('setting.search')}
className='xl:w-[300px] flex-1'
onChangeSearchFinal={(e) => setSearch(e)}
<AddressForm
selectedAddress={selectedAddress}
isEditMode={isEditMode}
onCancel={handleCancelEdit}
onSuccess={handleFormSuccess}
/>
</div>
</div>
<Table
isLoading={isPendingAddress}
columns={columns}
data={addresses?.data?.users}
pagination={{
currentPage: currentPage,
totalPages: addresses?.data?.pager?.totalPages || 1,
onPageChange: (page) => setCurrentPage(page)
}}
/>
</div>
<div className='xl:w-[350px] w-full bg-white rounded-4xl p-8'>
<div className='flex justify-between items-center'>
<div>
{isEditMode ? 'ویرایش آدرس' : t('setting.add_address')}
</div>
{isEditMode && (
<Button
variant='secondary'
onClick={handleCancelEdit}
className='px-4 w-fit text-xs'
>
انصراف
</Button>
)}
</div>
<div className='mt-6'>
<Input
label={t('setting.domain1')}
value={domain?.data?.domain?.name}
readOnly
/>
</div>
<div className='mt-6'>
<Input
label={t('setting.title1')}
{...register('title')}
error_text={errors.title?.message as string}
/>
</div>
<div className='mt-6 relative'>
<Input
label={t('setting.username')}
className='text-left'
{...register('username')}
error_text={errors.username?.message as string}
readOnly={isEditMode}
/>
<div className='absolute flex items-center dltr text-xs z-10 right-[1px] top-[29px] rounded-r-2.5 h-[38px] bg-[#EBEEF5] px-3'>
@ {domain?.data?.domain?.name}
</div>
</div>
<div className='mt-6'>
<Input
label='حجم ایمیل (مگابایت)'
type='number'
{...register('quotaMB', { valueAsNumber: true })}
error_text={errors.quotaMB?.message as string}
placeholder='حجم ایمیل را به مگابایت وارد کنید'
/>
<div className='text-xs text-gray-500 mt-1'>
فضای باقیمانده: {spaceData?.data?.quota ? Math.round((spaceData.data.quota.totalInGB - spaceData.data.quota.usedInGB) * 1024) : 0} مگابایت
</div>
</div>
<div className='mt-6'>
<Input
label={isEditMode ? 'رمز عبور جدید' : t('setting.password')}
type='password'
{...register('password')}
error_text={errors.password?.message as string}
placeholder={isEditMode ? 'برای تغییر رمز عبور وارد کنید' : ''}
/>
</div>
{!isEditMode && (
<div className='mt-2 text-xs'>
<div>رمز عبور میبایست:</div>
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
<li>حداقل ۸ کاراکتر باشد</li>
<li>ترکیبی از حروف کوچک و بزرگ باشد</li>
<li>شامل اعداد باشد</li>
<li>شامل کاراکتر های خاص (نماد ها) باشد</li>
</ul>
</div>
)}
<div className='mt-9 flex justify-end'>
<Button
className='w-fit px-20'
onClick={handleSubmit(onSubmit)}
loading={isPending || isUpdating}
>
<div className='flex gap-2'>
<TickCircle size={20} color='white' />
<div>
{isEditMode ? 'آپدیت' : t('setting.create')}
</div>
</div>
</Button>
</div>
</div>
<ModalConfrim
<AddressDeleteModal
isOpen={isDeleteModalOpen}
close={handleCloseDeleteModal}
onConfrim={handleConfirmDelete}
title={t('confrim.delete_address_title')}
label={addressToDelete ? t('confrim.delete_address_message').replace('{email}', addressToDelete.emailAddress) : ''}
isLoading={false}
addressToDelete={addressToDelete}
onClose={handleDeleteModalClose}
onSuccess={handleDeleteSuccess}
/>
</div>
)
@@ -0,0 +1,57 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import ModalConfrim from '@/components/ModalConfrim'
import { useDeleteAddress } from '../hooks/useAddressData'
import { AddressData } from '../types/Types'
import { toast } from '@/components/Toast'
import { ErrorType } from '@/helpers/types'
interface AddressDeleteModalProps {
isOpen: boolean
addressToDelete: AddressData | null
onClose: () => void
onSuccess: () => void
}
const AddressDeleteModal: FC<AddressDeleteModalProps> = ({
isOpen,
addressToDelete,
onClose,
onSuccess
}) => {
const { t } = useTranslation()
const { mutate: deleteAddress, isPending } = useDeleteAddress()
const handleConfirmDelete = () => {
if (addressToDelete) {
deleteAddress(addressToDelete.id, {
onSuccess: (data) => {
toast(data?.data?.message || 'آدرس با موفقیت حذف شد', 'success')
onSuccess()
onClose()
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0] || 'خطا در حذف', 'error')
onClose()
}
})
}
}
return (
<ModalConfrim
isOpen={isOpen}
close={onClose}
onConfrim={handleConfirmDelete}
title={t('confrim.delete_address_title')}
label={
addressToDelete
? t('confrim.delete_address_message').replace('{email}', addressToDelete.emailAddress)
: ''
}
isLoading={isPending}
/>
)
}
export default AddressDeleteModal
@@ -0,0 +1,261 @@
import Input from '@/components/Input'
import Select from '@/components/Select'
import Button from '@/components/Button'
import { TickCircle } from 'iconsax-react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { FC, useEffect } from 'react'
import { AddressData, CreateAddressType, UpdateAddressType } from '../types/Types'
import { useGetDomains } from '../../domain/hooks/useDomainData'
import { useCreateAddress, useUpdateAddress } from '../hooks/useAddressData'
import { useGetSpace } from '../../domain/hooks/useDomainData'
import { useGetTemplates } from '../../personality/hooks/usePersonalityData'
import { toast } from '@/components/Toast'
import { ErrorType } from '@/helpers/types'
interface AddressFormProps {
selectedAddress: AddressData | null
isEditMode: boolean
onCancel: () => void
onSuccess: () => void
}
const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCancel, onSuccess }) => {
const { t } = useTranslation()
const { data: templates } = useGetTemplates()
const { data: domain } = useGetDomains()
const { data: spaceData } = useGetSpace()
const { mutate: createAddress, isPending } = useCreateAddress()
const { mutate: updateAddress, isPending: isUpdating } = useUpdateAddress()
type FormData = CreateAddressType & { password?: string; quotaMB?: number }
const { register, handleSubmit, formState: { errors }, setValue, reset, setError, clearErrors, getValues
} = useForm<FormData>({
defaultValues: {
domainId: domain?.data?.domain?.id,
}
})
const validateForm = (params: FormData): boolean => {
clearErrors()
let isValid = true
if (!params.title) {
setError('title', { message: t('errors.required') })
isValid = false
}
if (!params.username) {
setError('username', { message: t('errors.required') })
isValid = false
}
if (!isEditMode && !params.password) {
setError('password', { message: t('errors.required') })
isValid = false
}
if (!params.domainId) {
setError('domainId', { message: t('errors.required') })
isValid = false
}
// اعتبارسنجی quota
if (params.quotaMB) {
const quotaInBytes = params.quotaMB * 1024 * 1024
const remainingSpace = spaceData?.data?.quota?.totalInGB * 1024 * 1024 * 1024 - spaceData?.data?.quota?.usedInGB * 1024 * 1024 * 1024
if (quotaInBytes > remainingSpace) {
setError('quotaMB', { message: 'فضای وارد شده بیش از فضای باقی‌مانده است' })
isValid = false
}
}
return isValid
}
const onSubmit = (params: FormData) => {
if (!validateForm(params)) {
return
}
if (isEditMode && selectedAddress) {
const updateParams: UpdateAddressType = {
title: params.title,
password: params.password || undefined,
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
}
updateAddress({ id: selectedAddress.id, params: updateParams }, {
onSuccess: (data) => {
toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success')
onSuccess()
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0] || 'خطا در آپدیت', 'error')
}
})
} else {
const createParams: CreateAddressType = {
...params,
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
}
createAddress(createParams, {
onSuccess: (data) => {
toast(data?.data?.message, 'success')
reset()
onSuccess()
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
}
useEffect(() => {
if (domain?.data?.domain?.id && !isEditMode) {
setValue('domainId', domain?.data?.domain?.id)
setValue('templateId', domain?.data?.template)
}
}, [domain, isEditMode, setValue])
useEffect(() => {
if (selectedAddress && isEditMode) {
reset({
title: selectedAddress.title,
username: selectedAddress.displayName,
password: '',
domainId: selectedAddress.domain.id,
quotaMB: selectedAddress.emailQuota ? Math.round(selectedAddress.emailQuota / (1024 * 1024)) : undefined,
templateId: selectedAddress.template || undefined,
})
} else if (!isEditMode) {
reset({
title: '',
username: '',
password: '',
domainId: domain?.data?.domain?.id,
quotaMB: undefined,
templateId: domain?.data?.template,
})
}
}, [selectedAddress, isEditMode, reset, domain])
return (
<div className='xl:w-[350px] w-full bg-white rounded-4xl p-8'>
<div className='flex justify-between items-center'>
<div>
{isEditMode ? 'ویرایش آدرس' : t('setting.add_address')}
</div>
{isEditMode && (
<Button
variant='secondary'
onClick={onCancel}
className='px-4 w-fit text-xs'
>
انصراف
</Button>
)}
</div>
<div className='mt-6'>
<Input
label={t('setting.domain1')}
value={domain?.data?.domain?.name}
readOnly
/>
</div>
<div className='mt-6'>
<Input
label={t('setting.title1')}
{...register('title')}
error_text={errors.title?.message as string}
/>
</div>
<div className='mt-6'>
<Select
placeholder='انتخاب قالب'
label={'قالب'}
items={
templates?.data?.templates
? templates.data.templates.map((template) => ({
value: template.id,
label: template.name
}))
: []
}
onChange={(e) => setValue('templateId', e.target.value)}
value={getValues('templateId')}
/>
</div>
<div className='mt-6 relative'>
<Input
label={t('setting.username')}
className='text-left'
{...register('username')}
error_text={errors.username?.message as string}
readOnly={isEditMode}
/>
<div className='absolute flex items-center dltr text-xs z-10 right-[1px] top-[29px] rounded-r-2.5 h-[38px] bg-[#EBEEF5] px-3'>
@ {domain?.data?.domain?.name}
</div>
</div>
<div className='mt-6'>
<Input
label='حجم ایمیل (مگابایت)'
type='number'
{...register('quotaMB', { valueAsNumber: true })}
error_text={errors.quotaMB?.message as string}
placeholder='حجم ایمیل را به مگابایت وارد کنید'
/>
<div className='text-xs text-gray-500 mt-1'>
فضای باقیمانده: {spaceData?.data?.quota ? Math.round((spaceData.data.quota.totalInGB - spaceData.data.quota.usedInGB) * 1024) : 0} مگابایت
</div>
</div>
<div className='mt-6'>
<Input
label={isEditMode ? 'رمز عبور جدید' : t('setting.password')}
type='password'
{...register('password')}
error_text={errors.password?.message as string}
placeholder={isEditMode ? 'برای تغییر رمز عبور وارد کنید' : ''}
/>
</div>
{!isEditMode && (
<div className='mt-2 text-xs'>
<div>رمز عبور میبایست:</div>
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
<li>حداقل ۸ کاراکتر باشد</li>
<li>ترکیبی از حروف کوچک و بزرگ باشد</li>
<li>شامل اعداد باشد</li>
<li>شامل کاراکتر های خاص (نماد ها) باشد</li>
</ul>
</div>
)}
<div className='mt-9 flex justify-end'>
<Button
className='w-fit px-20'
onClick={handleSubmit(onSubmit)}
loading={isPending || isUpdating}
>
<div className='flex gap-2'>
<TickCircle size={20} color='white' />
<div>
{isEditMode ? 'آپدیت' : t('setting.create')}
</div>
</div>
</Button>
</div>
</div>
)
}
export default AddressForm
@@ -0,0 +1,147 @@
import Input from '@/components/Input'
import Select from '@/components/Select'
import Table from '@/components/Table'
import { ColumnType } from '@/components/types/TableTypes'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Edit2, Trash } from 'iconsax-react'
import { AddressData } from '../types/Types'
import { useGetAddress } from '../hooks/useAddressData'
import ProgressBarEmail from './ProgressBarEmail'
import ToggleStatus from './ToggleStatus'
import RowActionsDropdown, { RowActionItem } from '@/components/RowActionsDropdown'
interface AddressTableProps {
search: string
status: string
currentPage: number
onSearchChange: (search: string) => void
onStatusChange: (status: string) => void
onPageChange: (page: number) => void
onEdit: (address: AddressData) => void
onDelete: (address: AddressData) => void
}
const AddressTable: FC<AddressTableProps> = ({
search,
status,
currentPage,
onSearchChange,
onStatusChange,
onPageChange,
onEdit,
onDelete
}) => {
const { t } = useTranslation()
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status, currentPage, 10)
const getRowActions = (address: AddressData): RowActionItem[] => [
{
label: 'ویرایش',
icon: <Edit2 color='black' size={16} />,
onClick: () => onEdit(address),
},
{
label: 'حذف',
icon: <Trash color='red' size={16} />,
onClick: () => onDelete(address),
className: 'text-red-600 hover:bg-red-50'
}
]
const columns: ColumnType<AddressData>[] = [
{
title: t('setting.address_title'),
key: 'title',
width: '200px'
},
{
title: t('setting.email'),
key: 'email',
width: '300px',
render: (item: AddressData) => (
<div key={item.id} className='dltr flex justify-end'>
{item.emailAddress}
</div>
)
},
{
title: t('setting.quota'),
key: 'quota',
width: '300px',
render: (item: AddressData) => {
return (
<ProgressBarEmail key={item.id} item={item} />
)
}
},
{
title: t('setting.status'),
key: 'status',
width: '120px',
align: 'center',
render: (item: AddressData) => (
<div key={item.id} className='dltr flex justify-end'>
<ToggleStatus defaultStatus={item.isActive} id={item.id} />
</div>
)
},
{
title: 'عملیات',
key: 'actions',
width: '120px',
align: 'center',
render: (item: AddressData) => (
<RowActionsDropdown
key={item.id}
actions={getRowActions(item)}
/>
)
}
]
const statusOptions = [
{ value: 'all', label: 'همه' },
{ value: 'active', label: t('setting.active') },
{ value: 'inactive', label: t('setting.inactive') }
]
return (
<div className='flex-1'>
<div className='flex gap-2 justify-between'>
<div>
<Select
placeholder={t('setting.status')}
items={statusOptions}
className='w-[120px]'
value={status}
onChange={(e) => onStatusChange(e.target.value)}
/>
</div>
<div className='flex-1'>
<Input
variant='search'
placeholder={t('setting.search')}
className='xl:w-[300px] flex-1'
value={search}
onChangeSearchFinal={onSearchChange}
/>
</div>
</div>
<Table
isLoading={isPendingAddress}
columns={columns}
data={addresses?.data?.users}
pagination={{
currentPage: currentPage,
totalPages: addresses?.data?.pager?.totalPages || 1,
onPageChange: onPageChange
}}
/>
</div>
)
}
export default AddressTable
+2
View File
@@ -4,6 +4,7 @@ export type CreateAddressType = {
domainId: string;
title: string;
quota?: number;
templateId?: string;
};
export type UpdateAddressType = {
@@ -64,6 +65,7 @@ export interface AddressData extends Record<string, unknown> {
dmarcPolicy: string | null;
business: string;
};
template: string | null;
}
export interface AddressResponse {