Notification Setting düzenlemesi
This commit is contained in:
parent
368756f0ac
commit
c79cee0df7
7 changed files with 307 additions and 166 deletions
|
|
@ -1929,20 +1929,20 @@
|
|||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.NotificationChannel.Sms",
|
||||
"en": "SMS",
|
||||
"tr": "SMS"
|
||||
"en": "Sms",
|
||||
"tr": "Sms"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.NotificationChannel.Mail",
|
||||
"en": "E-mail",
|
||||
"en": "Mail",
|
||||
"tr": "E-posta"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.NotificationChannel.Rocket",
|
||||
"en": "Rocket.Chat",
|
||||
"tr": "Rocket.Chat"
|
||||
"en": "Rocket",
|
||||
"tr": "Rocket"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
|
|
@ -1953,14 +1953,14 @@
|
|||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.NotificationChannel.UiActivity",
|
||||
"en": "Activity",
|
||||
"tr": "Etkinlik"
|
||||
"en": "UI Activity",
|
||||
"tr": "Arayüz Etkinliği"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.NotificationChannel.UiToast",
|
||||
"en": "In-app",
|
||||
"tr": "Uygulama içi"
|
||||
"en": "UI Toast",
|
||||
"tr": "Arayüz Bildirimi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
|
|
@ -1980,6 +1980,30 @@
|
|||
"en": "Reset to default",
|
||||
"tr": "Varsayılana döndür"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Abp.Identity.NotificationSettings.Description",
|
||||
"en": "Choose how you want to be notified for each notification type.",
|
||||
"tr": "Her bildirim türü için nasıl haberdar olmak istediğinizi seçin."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Abp.Identity.NotificationSettings.Type",
|
||||
"en": "Notification type",
|
||||
"tr": "Bildirim türü"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Abp.Identity.NotificationSettings.Customized",
|
||||
"en": "Customized by you",
|
||||
"tr": "Sizin tarafınızdan değiştirildi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Abp.Identity.NotificationSettings.Fixed",
|
||||
"en": "Fixed by the administrator",
|
||||
"tr": "Yönetici tarafından sabitlenmiş"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "Abp.Identity.NotificationSettings.NoRule",
|
||||
|
|
@ -19578,12 +19602,6 @@
|
|||
"en": "DELETE",
|
||||
"tr": "DELETE"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.StaticLookup.Desktop",
|
||||
"en": "Desktop",
|
||||
"tr": "Masaüstü"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.StaticLookup.Double",
|
||||
|
|
@ -19674,12 +19692,6 @@
|
|||
"en": "List",
|
||||
"tr": "Liste"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.StaticLookup.Mail",
|
||||
"en": "Mail",
|
||||
"tr": "E-posta"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.StaticLookup.MailQueueWorker",
|
||||
|
|
@ -19914,12 +19926,6 @@
|
|||
"en": "Reference",
|
||||
"tr": "Referans"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.StaticLookup.Rocket",
|
||||
"en": "Rocket",
|
||||
"tr": "Rocket"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.StaticLookup.Role",
|
||||
|
|
@ -20028,12 +20034,6 @@
|
|||
"en": "Time",
|
||||
"tr": "Saat"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.StaticLookup.UiActivity",
|
||||
"en": "UI Activity",
|
||||
"tr": "Arayüz Etkinliği"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.StaticLookup.UInt16",
|
||||
|
|
@ -20052,12 +20052,6 @@
|
|||
"en": "UInt64",
|
||||
"tr": "UInt64"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.StaticLookup.UiToast",
|
||||
"en": "UI Toast",
|
||||
"tr": "Arayüz Bildirimi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Platform.User",
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
SeedSyncInsert = true,
|
||||
SeedSyncUpdate = true,
|
||||
SeedSyncDelete = true,
|
||||
SortMode = GridOptions.SortModeSingle,
|
||||
SortMode = GridOptions.SortModeMultiple,
|
||||
FilterRowJson = DefaultFilterRowJson(),
|
||||
HeaderFilterJson = DefaultHeaderFilterJson(),
|
||||
SearchPanelJson = DefaultSearchPanelJson(),
|
||||
|
|
@ -454,6 +454,8 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
Visible = true,
|
||||
IsActive = true,
|
||||
|
||||
SortIndex = 2,
|
||||
SortDirection = GridColumnOptions.SortOrderAsc,
|
||||
LookupJson = JsonSerializer.Serialize(new LookupDto
|
||||
{
|
||||
DataSourceType = UiLookupDataSourceTypeEnum.Query,
|
||||
|
|
@ -7238,8 +7240,8 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
}),
|
||||
CommandColumnJson = JsonSerializer.Serialize(new CommandColumnDto[] {
|
||||
new CommandColumnDto() {
|
||||
Hint = "Create",
|
||||
Text = "Create",
|
||||
Hint = "App.Platform.Create",
|
||||
Text = "App.Platform.Create",
|
||||
AuthName = listFormName,
|
||||
DialogName = "CreateNotification",
|
||||
DialogParameters = JsonSerializer.Serialize(new { name = "id", id = "@Id" }),
|
||||
|
|
|
|||
|
|
@ -64,14 +64,14 @@ public static class ListFormSeeder_DefaultJsons
|
|||
ValueExpr = "key",
|
||||
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[]
|
||||
{
|
||||
new() { Key = "Sms", Name = "App.Sender.Sms" },
|
||||
new() { Key = "Mail", Name = "App.StaticLookup.Mail" },
|
||||
new() { Key = "Rocket", Name = "App.StaticLookup.Rocket" },
|
||||
new() { Key = "Desktop", Name = "App.StaticLookup.Desktop" },
|
||||
new() { Key = "UiActivity", Name = "App.StaticLookup.UiActivity" },
|
||||
new() { Key = "UiToast", Name = "App.StaticLookup.UiToast" },
|
||||
new() { Key = "WhatsApp", Name = "App.Sender.WhatsApp" },
|
||||
new() { Key = "Telegram", Name = "App.Sender.Telegram" },
|
||||
new() { Key = "Sms", Name = "App.NotificationChannel.Sms" },
|
||||
new() { Key = "Mail", Name = "App.NotificationChannel.Mail" },
|
||||
new() { Key = "Rocket", Name = "App.NotificationChannel.Rocket" },
|
||||
new() { Key = "Desktop", Name = "App.NotificationChannel.Desktop" },
|
||||
new() { Key = "UiActivity", Name = "App.NotificationChannel.UiActivity" },
|
||||
new() { Key = "UiToast", Name = "App.NotificationChannel.UiToast" },
|
||||
new() { Key = "WhatsApp", Name = "App.NotificationChannel.WhatsApp" },
|
||||
new() { Key = "Telegram", Name = "App.NotificationChannel.Telegram" },
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,15 @@
|
|||
import type { IconType } from 'react-icons'
|
||||
import {
|
||||
FaBell,
|
||||
FaDesktop,
|
||||
FaEnvelope,
|
||||
FaListUl,
|
||||
FaRocketchat,
|
||||
FaSms,
|
||||
FaTelegramPlane,
|
||||
FaWhatsapp,
|
||||
} from 'react-icons/fa'
|
||||
|
||||
const NotificationChannels = {
|
||||
Sms: 'Sms',
|
||||
Mail: 'Mail',
|
||||
|
|
@ -20,4 +32,16 @@ export const NotificationChannelColors: Record<string, string> = {
|
|||
Telegram: 'bg-purple-900',
|
||||
}
|
||||
|
||||
/** Kanal kayıt defterinin görsel yüzü; yeni kanal eklendiğinde ikon da burada tanımlanır. */
|
||||
export const NotificationChannelIcons: Record<string, IconType> = {
|
||||
Sms: FaSms,
|
||||
Mail: FaEnvelope,
|
||||
Rocket: FaRocketchat,
|
||||
Desktop: FaDesktop,
|
||||
UiActivity: FaListUl,
|
||||
UiToast: FaBell,
|
||||
WhatsApp: FaWhatsapp,
|
||||
Telegram: FaTelegramPlane,
|
||||
}
|
||||
|
||||
export default NotificationChannels
|
||||
|
|
|
|||
|
|
@ -37,6 +37,5 @@ export const postMyNotificationRule = (data: {
|
|||
export const deleteMyNotificationRules = (notificationTypeId: string) =>
|
||||
apiService.fetchData<NotificationRuleDto[]>({
|
||||
method: 'DELETE',
|
||||
url: `/api/app/notification-rule/my-notification-rules`,
|
||||
params: { notificationTypeId },
|
||||
url: `/api/app/notification-rule/my-notification-rules/${notificationTypeId}`,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,16 +7,8 @@ import classNames from 'classnames'
|
|||
|
||||
type CategoryTitleProps = CommonProps
|
||||
|
||||
const ticketCheckboxes = [
|
||||
{ label: NotificationChannels.Sms, value: NotificationChannels.Sms },
|
||||
{ label: NotificationChannels.Mail, value: NotificationChannels.Mail },
|
||||
{ label: NotificationChannels.Rocket, value: NotificationChannels.Rocket },
|
||||
{ label: NotificationChannels.Desktop, value: NotificationChannels.Desktop },
|
||||
{ label: NotificationChannels.UiActivity, value: NotificationChannels.UiActivity },
|
||||
{ label: NotificationChannels.UiToast, value: NotificationChannels.UiToast },
|
||||
{ label: NotificationChannels.WhatsApp, value: NotificationChannels.WhatsApp },
|
||||
{ label: NotificationChannels.Telegram, value: NotificationChannels.Telegram },
|
||||
]
|
||||
/** Kanal listesi kayit defterinden gelir; etiket her ekranda ayni dil anahtarindan okunur. */
|
||||
const channels = Object.values(NotificationChannels)
|
||||
|
||||
const CategoryTitle = ({ children, className }: CategoryTitleProps) => {
|
||||
return (
|
||||
|
|
@ -64,9 +56,9 @@ const LogFilter = ({
|
|||
<CategoryTitle className="mb-3 text-gray-500 dark:text-gray-400">
|
||||
{translate('::Abp.Identity.ActivityLogs.Channels')}
|
||||
</CategoryTitle>
|
||||
{ticketCheckboxes.map((checkbox) => (
|
||||
<Checkbox key={checkbox.value} className="mb-3 dark:text-white" value={checkbox.value}>
|
||||
{checkbox.label}
|
||||
{channels.map((channel) => (
|
||||
<Checkbox key={channel} className="mb-3 dark:text-white" value={channel}>
|
||||
{translate(`::App.NotificationChannel.${channel}`)}
|
||||
</Checkbox>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { Button, Checkbox, Spinner, Table } from '@/components/ui'
|
||||
import NotificationChannels from '@/constants/notification-channel.enum'
|
||||
import { Button, Spinner, Switcher, Tooltip } from '@/components/ui'
|
||||
import NotificationChannels, {
|
||||
NotificationChannelColors,
|
||||
NotificationChannelIcons,
|
||||
} from '@/constants/notification-channel.enum'
|
||||
import { NotificationRuleDto, NotificationTypeDto } from '@/proxy/notification/models'
|
||||
import {
|
||||
deleteMyNotificationRules,
|
||||
|
|
@ -8,49 +11,64 @@ import {
|
|||
postMyNotificationRule,
|
||||
} from '@/services/notification-rule.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import classNames from 'classnames'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { FaHistory } from 'react-icons/fa'
|
||||
|
||||
const { Tr, Th, Td, THead, TBody } = Table
|
||||
import type { MouseEvent } from 'react'
|
||||
import { FaHistory, FaLock } from 'react-icons/fa'
|
||||
|
||||
/** Matris sütunları kanal kayıt defterinden gelir; yeni bir kanal eklendiğinde ekran kendiliğinden güncel olur. */
|
||||
const channels = Object.values(NotificationChannels)
|
||||
|
||||
const ruleKey = (notificationTypeId: string, channel: string) => `${notificationTypeId}|${channel}`
|
||||
|
||||
/** Sunucuda karşılığı olmayan tür/kanal kesişimi kapalı sayılır; matris boşluk bırakmaz. */
|
||||
const emptyRule = (notificationTypeId: string, channel: string): NotificationRuleDto => ({
|
||||
id: '',
|
||||
notificationTypeId,
|
||||
channel,
|
||||
isActive: false,
|
||||
isFixed: false,
|
||||
isCustomized: false,
|
||||
})
|
||||
|
||||
const ChannelIcon = ({ channel, size = 'md' }: { channel: string; size?: 'sm' | 'md' }) => {
|
||||
const Icon = NotificationChannelIcons[channel]
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
'flex items-center justify-center rounded-full text-white',
|
||||
size === 'sm' ? 'h-6 w-6 text-xs' : 'h-7 w-7 text-sm',
|
||||
NotificationChannelColors[channel] ?? 'bg-gray-400',
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon />}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const NotificationSettings = () => {
|
||||
const { translate } = useLocalization()
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [list, setList] = useState<NotificationRuleDto[][]>([])
|
||||
const [rules, setRules] = useState<Record<string, NotificationRuleDto>>({})
|
||||
const [notificationTypes, setNotificationTypes] = useState<NotificationTypeDto[]>([])
|
||||
// Yalnızca kaydedilen hücre/satır kilitlenir; matrisin tamamını kilitlemek tek tıkta ekranı donduruyordu.
|
||||
const [pending, setPending] = useState<string[]>([])
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [typesResponse, rulesResponse] = await Promise.all([
|
||||
getMyNotificationTypes(),
|
||||
getMyNotificationRules(),
|
||||
])
|
||||
|
||||
const types = typesResponse.data ?? []
|
||||
const rules = rulesResponse.data ?? []
|
||||
|
||||
setNotificationTypes(types)
|
||||
setList(
|
||||
types.map((type) =>
|
||||
channels.map(
|
||||
(channel) =>
|
||||
rules.find(
|
||||
(rule) => rule.channel === channel && rule.notificationTypeId === type.id,
|
||||
) ?? {
|
||||
id: '',
|
||||
notificationTypeId: type.id,
|
||||
channel,
|
||||
isActive: false,
|
||||
isFixed: false,
|
||||
isCustomized: false,
|
||||
},
|
||||
),
|
||||
setNotificationTypes(typesResponse.data ?? [])
|
||||
setRules(
|
||||
Object.fromEntries(
|
||||
(rulesResponse.data ?? []).map((rule) => [
|
||||
ruleKey(rule.notificationTypeId, rule.channel),
|
||||
rule,
|
||||
]),
|
||||
),
|
||||
)
|
||||
} finally {
|
||||
|
|
@ -63,94 +81,206 @@ const NotificationSettings = () => {
|
|||
fetchData()
|
||||
}, [fetchData])
|
||||
|
||||
const onClickCheckbox = async (checked: boolean, rule: NotificationRuleDto) => {
|
||||
// Kayıt sürerken kutular kilitlenir; art arda tıklamada son dönen yanıt kazanır ve
|
||||
// ekran sunucudaki gerçeğin tersine düşerdi.
|
||||
setSaving(true)
|
||||
const getRule = (notificationTypeId: string, channel: string) =>
|
||||
rules[ruleKey(notificationTypeId, channel)] ?? emptyRule(notificationTypeId, channel)
|
||||
|
||||
const isPending = (key: string) => pending.includes(key)
|
||||
|
||||
const withPending = async (key: string, action: () => Promise<unknown>) => {
|
||||
setPending((current) => [...current, key])
|
||||
try {
|
||||
await postMyNotificationRule({
|
||||
channel: rule.channel,
|
||||
notificationTypeId: rule.notificationTypeId,
|
||||
isActive: checked,
|
||||
})
|
||||
await fetchData()
|
||||
await action()
|
||||
} finally {
|
||||
setSaving(false)
|
||||
setPending((current) => current.filter((item) => item !== key))
|
||||
}
|
||||
}
|
||||
|
||||
const resetMyNotifications = async (notificationTypeId: string) => {
|
||||
setSaving(true)
|
||||
try {
|
||||
const onToggle = (checked: boolean, rule: NotificationRuleDto) => {
|
||||
const key = ruleKey(rule.notificationTypeId, rule.channel)
|
||||
// Anahtar hemen döner, sunucu yanıtından sonra gerçek durum geri yazılır; hata olursa eski değere döner.
|
||||
setRules((current) => ({
|
||||
...current,
|
||||
[key]: { ...rule, isActive: checked, isCustomized: true },
|
||||
}))
|
||||
|
||||
return withPending(key, async () => {
|
||||
try {
|
||||
await postMyNotificationRule({
|
||||
channel: rule.channel,
|
||||
notificationTypeId: rule.notificationTypeId,
|
||||
isActive: checked,
|
||||
})
|
||||
await fetchData()
|
||||
} catch {
|
||||
setRules((current) => ({ ...current, [key]: rule }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetMyNotifications = (notificationTypeId: string) =>
|
||||
withPending(notificationTypeId, async () => {
|
||||
await deleteMyNotificationRules(notificationTypeId)
|
||||
await fetchData()
|
||||
} finally {
|
||||
setSaving(false)
|
||||
})
|
||||
|
||||
const isCustomized = (notificationTypeId: string) =>
|
||||
channels.some((channel) => getRule(notificationTypeId, channel).isCustomized)
|
||||
|
||||
const renderSwitcher = (notificationTypeId: string, channel: string) => {
|
||||
const rule = getRule(notificationTypeId, channel)
|
||||
const busy = isPending(ruleKey(notificationTypeId, channel)) || isPending(notificationTypeId)
|
||||
|
||||
if (rule.isFixed) {
|
||||
return (
|
||||
<Tooltip title={translate('::Abp.Identity.NotificationSettings.Fixed')}>
|
||||
<span className={rule.isActive ? 'text-emerald-600' : 'text-gray-400'}>
|
||||
<FaLock />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Switcher
|
||||
checked={rule.isActive}
|
||||
isLoading={busy}
|
||||
disabled={busy}
|
||||
// Switcher denetimli kipte eski degeri geri veriyor; yeni deger kuraldan turetilir.
|
||||
onChange={() => onToggle(!rule.isActive, rule)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const renderResetButton = (notificationTypeId: string) => (
|
||||
<Tooltip title={translate('::Abp.Identity.NotificationSettings.Reset')}>
|
||||
<Button
|
||||
shape="circle"
|
||||
variant="plain"
|
||||
type="button"
|
||||
size="xs"
|
||||
disabled={!isCustomized(notificationTypeId) || isPending(notificationTypeId)}
|
||||
icon={<FaHistory />}
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
resetMyNotifications(notificationTypeId)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
const renderTypeTitle = (type: NotificationTypeDto) => (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold">{type.name}</span>
|
||||
{isCustomized(type.id) && <span className="h-2 w-2 shrink-0 rounded-full bg-amber-500" />}
|
||||
</div>
|
||||
{type.description && <p className="mt-0.5 text-xs text-gray-500">{type.description}</p>}
|
||||
</>
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-10">
|
||||
<Spinner size={40} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!notificationTypes.length) {
|
||||
return (
|
||||
<p className="py-10 text-center text-gray-500">
|
||||
{translate('::Abp.Identity.NotificationSettings.NoRule')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<Spinner size={40} />
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap items-end justify-between gap-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{translate('::Abp.Identity.NotificationSettings.Description')}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="h-2 w-2 rounded-full bg-amber-500" />
|
||||
{translate('::Abp.Identity.NotificationSettings.Customized')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FaLock />
|
||||
{translate('::Abp.Identity.NotificationSettings.Fixed')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{list.length ? (
|
||||
<Table compact={true}>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th></Th>
|
||||
{channels.map((channel) => (
|
||||
<Th key={channel} className="text-center">
|
||||
</div>
|
||||
|
||||
{/* Geniş ekranda matris, dar ekranda kart listesi: sekiz kanal dar ekranda tek satıra sığmıyor. */}
|
||||
<div className="hidden overflow-x-auto rounded-xl border border-gray-200 md:block dark:border-gray-700">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th className="sticky left-0 z-10 bg-gray-50 px-4 py-3 text-left font-semibold dark:bg-gray-700">
|
||||
{translate('::Abp.Identity.NotificationSettings.Type')}
|
||||
</th>
|
||||
{channels.map((channel) => (
|
||||
<th key={channel} className="px-3 py-3 font-semibold">
|
||||
<span className="flex flex-col items-center gap-1.5">
|
||||
<ChannelIcon channel={channel} />
|
||||
<span className="text-xs whitespace-nowrap">
|
||||
{translate(`::App.NotificationChannel.${channel}`)}
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{notificationTypes.map((type, m) => (
|
||||
<Tr key={type.id}>
|
||||
<Th>
|
||||
<Button
|
||||
shape="circle"
|
||||
variant="plain"
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={saving}
|
||||
title={translate('::Abp.Identity.NotificationSettings.Reset')}
|
||||
icon={<FaHistory />}
|
||||
onClick={async (e) => {
|
||||
e.preventDefault()
|
||||
resetMyNotifications(type.id)
|
||||
}}
|
||||
/>
|
||||
<span title={type.description ?? undefined}>{type.name}</span>
|
||||
</Th>
|
||||
{channels.map((channel, n) => (
|
||||
<Td key={type.id + channel} className="text-center">
|
||||
<Checkbox
|
||||
name={type.id + channel}
|
||||
disabled={list[m][n].isFixed || saving}
|
||||
checked={list[m][n].isActive}
|
||||
color={list[m][n].isCustomized ? 'red-500' : undefined}
|
||||
onChange={(e) => onClickCheckbox(e, list[m][n])}
|
||||
></Checkbox>
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
</span>
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
<th className="w-12 px-2 py-3" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{notificationTypes.map((type) => (
|
||||
<tr
|
||||
key={type.id}
|
||||
className="border-t border-gray-200 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-700"
|
||||
>
|
||||
<td className="sticky left-0 z-10 bg-white px-4 py-3 dark:bg-gray-800">
|
||||
{renderTypeTitle(type)}
|
||||
</td>
|
||||
{channels.map((channel) => (
|
||||
<td key={channel} className="px-3 py-3">
|
||||
<div className="flex justify-center">{renderSwitcher(type.id, channel)}</div>
|
||||
</td>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
) : (
|
||||
<p className="text-center">
|
||||
{translate('::Abp.Identity.NotificationSettings.NoRule')}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<td className="px-2 py-3 text-center">{renderResetButton(type.id)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 md:hidden">
|
||||
{notificationTypes.map((type) => (
|
||||
<div key={type.id} className="rounded-xl border border-gray-200 p-4 dark:border-gray-700">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>{renderTypeTitle(type)}</div>
|
||||
{renderResetButton(type.id)}
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{channels.map((channel) => (
|
||||
<div
|
||||
key={channel}
|
||||
className="flex items-center justify-between gap-3 rounded-lg bg-gray-50 px-3 py-2 dark:bg-gray-700"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<ChannelIcon channel={channel} size="sm" />
|
||||
<span className="text-sm">
|
||||
{translate(`::App.NotificationChannel.${channel}`)}
|
||||
</span>
|
||||
</span>
|
||||
{renderSwitcher(type.id, channel)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue