GroupRemoveClientsModal.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import { useEffect, useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Input, Modal, Space, Table, Tag, Typography, message } from 'antd';
  4. import type { ColumnsType } from 'antd/es/table';
  5. import type { ClientRecord } from '@/hooks/useClients';
  6. interface GroupRemoveClientsModalProps {
  7. open: boolean;
  8. groupName: string | null;
  9. members: ClientRecord[];
  10. onClose: () => void;
  11. onSubmit: (emails: string[]) => Promise<{ affected?: number } | null>;
  12. }
  13. interface ClientRow {
  14. email: string;
  15. comment: string;
  16. enable: boolean;
  17. }
  18. export default function GroupRemoveClientsModal({
  19. open,
  20. groupName,
  21. members,
  22. onClose,
  23. onSubmit,
  24. }: GroupRemoveClientsModalProps) {
  25. const { t } = useTranslation();
  26. const [messageApi, messageContextHolder] = message.useMessage();
  27. const [saving, setSaving] = useState(false);
  28. const [selectedEmails, setSelectedEmails] = useState<string[]>([]);
  29. const [search, setSearch] = useState('');
  30. const rows = useMemo<ClientRow[]>(
  31. () =>
  32. (members || [])
  33. .map((c) => ({
  34. email: (c.email || '').trim(),
  35. comment: (c.comment || '').trim(),
  36. enable: c.enable !== false,
  37. }))
  38. .filter((r) => r.email),
  39. [members],
  40. );
  41. useEffect(() => {
  42. if (!open) return;
  43. setSelectedEmails([]);
  44. setSearch('');
  45. }, [open, rows]);
  46. const filteredRows = useMemo(() => {
  47. const q = search.trim().toLowerCase();
  48. if (!q) return rows;
  49. return rows.filter(
  50. (r) => r.email.toLowerCase().includes(q) || r.comment.toLowerCase().includes(q),
  51. );
  52. }, [rows, search]);
  53. const columns: ColumnsType<ClientRow> = useMemo(
  54. () => [
  55. { title: t('pages.inbounds.email'), dataIndex: 'email', key: 'email', ellipsis: true },
  56. { title: t('comment'), dataIndex: 'comment', key: 'comment', ellipsis: true },
  57. {
  58. title: t('enable'),
  59. dataIndex: 'enable',
  60. key: 'enable',
  61. width: 90,
  62. render: (enabled: boolean) =>
  63. enabled ? (
  64. <Tag color="success">{t('enable')}</Tag>
  65. ) : (
  66. <Tag>{t('pages.inbounds.attachClientsStatusDisabled')}</Tag>
  67. ),
  68. },
  69. ],
  70. [t],
  71. );
  72. async function submit() {
  73. if (!groupName || selectedEmails.length === 0) return;
  74. setSaving(true);
  75. try {
  76. const result = await onSubmit(selectedEmails);
  77. if (!result) return;
  78. const affected = result.affected ?? selectedEmails.length;
  79. messageApi.success(
  80. t('pages.groups.removeFromGroupResult', { count: affected, name: groupName }),
  81. );
  82. onClose();
  83. } finally {
  84. setSaving(false);
  85. }
  86. }
  87. return (
  88. <Modal
  89. open={open}
  90. onCancel={onClose}
  91. onOk={submit}
  92. okButtonProps={{ danger: true, disabled: selectedEmails.length === 0, loading: saving }}
  93. okText={t('remove')}
  94. cancelText={t('cancel')}
  95. title={t('pages.groups.removeFromGroupTitle', { name: groupName ?? '' })}
  96. width={680}
  97. >
  98. {messageContextHolder}
  99. <Typography.Paragraph type="secondary">
  100. {t('pages.groups.removeFromGroupDesc')}
  101. </Typography.Paragraph>
  102. <Space direction="vertical" size="small" style={{ width: '100%' }}>
  103. <Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
  104. <Input.Search
  105. allowClear
  106. value={search}
  107. onChange={(e) => setSearch(e.target.value)}
  108. placeholder={t('pages.inbounds.attachClientsSearchPlaceholder')}
  109. style={{ maxWidth: 320 }}
  110. />
  111. <Typography.Text type="secondary">
  112. {t('pages.inbounds.attachClientsSelectedCount', {
  113. selected: selectedEmails.length,
  114. total: rows.length,
  115. })}
  116. </Typography.Text>
  117. </Space>
  118. <Table<ClientRow>
  119. size="small"
  120. rowKey="email"
  121. columns={columns}
  122. dataSource={filteredRows}
  123. pagination={false}
  124. scroll={{ y: 280 }}
  125. rowSelection={{
  126. selectedRowKeys: selectedEmails,
  127. onChange: (keys) => setSelectedEmails(keys as string[]),
  128. preserveSelectedRowKeys: true,
  129. }}
  130. />
  131. </Space>
  132. </Modal>
  133. );
  134. }