ManageGlobalNotification.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import React, {
  2. type JSX,
  3. useCallback,
  4. useEffect,
  5. useMemo,
  6. useState,
  7. } from 'react';
  8. import Link from 'next/link';
  9. import { useRouter } from 'next/router';
  10. import { useAtomValue } from 'jotai';
  11. import { useTranslation } from 'next-i18next';
  12. import {
  13. NotifyType,
  14. TriggerEventType,
  15. } from '~/client/interfaces/global-notification';
  16. import { apiv3Post } from '~/client/util/apiv3-client';
  17. import { toastError } from '~/client/util/toastr';
  18. import { isMailerSetupAtom } from '~/states/server-configurations';
  19. import { useSWRxGlobalNotification } from '~/stores/global-notification';
  20. import loggerFactory from '~/utils/logger';
  21. import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
  22. import TriggerEventCheckBox from './TriggerEventCheckBox';
  23. const logger = loggerFactory('growi:manageGlobalNotification');
  24. type Props = {
  25. globalNotificationId?: string;
  26. };
  27. const ManageGlobalNotification = (props: Props): JSX.Element => {
  28. const [triggerPath, setTriggerPath] = useState('');
  29. const [notifyType, setNotifyType] = useState<NotifyType>(NotifyType.Email);
  30. const [emailToSend, setEmailToSend] = useState('');
  31. const [slackChannelToSend, setSlackChannelToSend] = useState('');
  32. const [triggerEvents, setTriggerEvents] = useState(new Set<string>());
  33. const { data: globalNotificationData, update: updateGlobalNotification } =
  34. useSWRxGlobalNotification(props.globalNotificationId || '');
  35. const globalNotification = useMemo(
  36. () => globalNotificationData?.globalNotification,
  37. [globalNotificationData?.globalNotification],
  38. );
  39. const router = useRouter();
  40. useEffect(() => {
  41. if (globalNotification != null) {
  42. const notifyType = globalNotification.__t;
  43. setNotifyType(notifyType);
  44. setTriggerPath(globalNotification.triggerPath);
  45. setTriggerEvents(new Set(globalNotification.triggerEvents));
  46. if (notifyType === NotifyType.Email) {
  47. setEmailToSend(globalNotification.toEmail);
  48. } else {
  49. setSlackChannelToSend(globalNotification.slackChannels);
  50. }
  51. }
  52. }, [globalNotification]);
  53. const isLoading = globalNotificationData === undefined;
  54. const notExistsGlobalNotification =
  55. !isLoading && globalNotificationData == null;
  56. useEffect(() => {
  57. if (notExistsGlobalNotification) {
  58. router.push('/admin/notification');
  59. }
  60. }, [notExistsGlobalNotification, router]);
  61. const onChangeTriggerEvents = useCallback(
  62. (triggerEvent: string) => {
  63. let newTriggerEvents: string[];
  64. if (triggerEvents.has(triggerEvent)) {
  65. newTriggerEvents = [...triggerEvents].filter(
  66. (item) => item !== triggerEvent,
  67. );
  68. setTriggerEvents(new Set(newTriggerEvents));
  69. } else {
  70. newTriggerEvents = [...triggerEvents, triggerEvent];
  71. setTriggerEvents(new Set(newTriggerEvents));
  72. }
  73. },
  74. [triggerEvents],
  75. );
  76. const updateButtonClickedHandler = useCallback(async () => {
  77. const requestParams = {
  78. triggerPath,
  79. notifyType,
  80. toEmail: emailToSend,
  81. slackChannels: slackChannelToSend,
  82. triggerEvents: [...triggerEvents],
  83. };
  84. try {
  85. if (props.globalNotificationId != null) {
  86. await updateGlobalNotification(requestParams);
  87. router.push('/admin/notification');
  88. } else {
  89. await apiv3Post(
  90. '/notification-setting/global-notification',
  91. requestParams,
  92. );
  93. router.push('/admin/notification');
  94. }
  95. } catch (err) {
  96. toastError(err);
  97. logger.error(err);
  98. }
  99. }, [
  100. emailToSend,
  101. notifyType,
  102. props.globalNotificationId,
  103. router,
  104. slackChannelToSend,
  105. triggerEvents,
  106. triggerPath,
  107. updateGlobalNotification,
  108. ]);
  109. // Mailer setup status (unused yet but kept for potential conditional logic)
  110. const isMailerSetup = useAtomValue(isMailerSetupAtom);
  111. const { t } = useTranslation('admin');
  112. return (
  113. <>
  114. <div className="my-3">
  115. <Link href="/admin/notification" className="btn btn-outline-secondary">
  116. <span className="material-symbols-outlined" aria-hidden="true">
  117. arrow_left_alt
  118. </span>
  119. {t('notification_settings.back_to_list')}
  120. </Link>
  121. </div>
  122. <div className="row">
  123. <div className="form-box col-md-12">
  124. <h2 className="border-bottom mb-5">
  125. {t('notification_settings.notification_detail')}
  126. </h2>
  127. </div>
  128. <div className="col-sm-4">
  129. <h3>
  130. <label htmlFor="triggerPath" className="form-label">
  131. {t('notification_settings.trigger_path')}
  132. <small
  133. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  134. dangerouslySetInnerHTML={{
  135. __html: t(
  136. 'notification_settings.trigger_path_help',
  137. '<code>*</code>',
  138. ),
  139. }}
  140. />
  141. </label>
  142. </h3>
  143. <div>
  144. <input
  145. className="form-control"
  146. type="text"
  147. name="triggerPath"
  148. value={triggerPath}
  149. onChange={(e) => {
  150. setTriggerPath(e.target.value);
  151. }}
  152. required
  153. />
  154. </div>
  155. <h3>{t('notification_settings.notify_to')}</h3>
  156. <div>
  157. <div className="form-check">
  158. <input
  159. className="form-check-input"
  160. type="radio"
  161. id="mail"
  162. name="notifyType"
  163. value="mail"
  164. checked={notifyType === NotifyType.Email}
  165. onChange={() => {
  166. setNotifyType(NotifyType.Email);
  167. }}
  168. />
  169. <label className="form-label form-check-label" htmlFor="mail">
  170. <p className="fw-bold">Email</p>
  171. </label>
  172. </div>
  173. <div className="form-check ms-2">
  174. <input
  175. className="form-check-input"
  176. type="radio"
  177. id="slack"
  178. name="notifyType"
  179. value="slack"
  180. checked={notifyType === NotifyType.SLACK}
  181. onChange={() => {
  182. setNotifyType(NotifyType.SLACK);
  183. }}
  184. />
  185. <label className="form-label form-check-label" htmlFor="slack">
  186. <p className="fw-bold">Slack</p>
  187. </label>
  188. </div>
  189. </div>
  190. {notifyType === NotifyType.Email ? (
  191. <>
  192. <div className="input-group notify-to-option" id="mail-input">
  193. <div>
  194. <span className="input-group-text" id="mail-addon"></span>
  195. <span className="material-symbols-outlined">mail</span>
  196. </div>
  197. <input
  198. className="form-control"
  199. type="text"
  200. aria-describedby="mail-addon"
  201. name="toEmail"
  202. placeholder="Email"
  203. value={emailToSend}
  204. onChange={(e) => {
  205. setEmailToSend(e.target.value);
  206. }}
  207. />
  208. </div>
  209. <p className="p-2">
  210. {!isMailerSetup && (
  211. <span
  212. className="form-text text-muted"
  213. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  214. dangerouslySetInnerHTML={{
  215. __html: t('admin:mailer_setup_required'),
  216. }}
  217. />
  218. )}
  219. <b>Hint: </b>
  220. <a href="https://ifttt.com/create" target="blank">
  221. {t('notification_settings.email.ifttt_link')}
  222. <span className="material-symbols-outlined">share</span>
  223. </a>
  224. </p>
  225. </>
  226. ) : (
  227. <>
  228. <div className="input-group notify-to-option" id="slack-input">
  229. <div>
  230. <span
  231. className="input-group-text"
  232. id="slack-channel-addon"
  233. ></span>
  234. <span className="material-symbols-outlined">tag</span>
  235. </div>
  236. <input
  237. className="form-control"
  238. type="text"
  239. aria-describedby="slack-channel-addon"
  240. name="notificationGlobal[slackChannels]"
  241. placeholder="Slack Channel"
  242. value={slackChannelToSend}
  243. onChange={(e) => {
  244. setSlackChannelToSend(e.target.value);
  245. }}
  246. />
  247. </div>
  248. <p className="p-2">
  249. <span
  250. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  251. dangerouslySetInnerHTML={{
  252. __html: t('notification_settings.channel_desc'),
  253. }}
  254. />
  255. </p>
  256. </>
  257. )}
  258. </div>
  259. <div className="offset-1 col-sm-5">
  260. <div>
  261. <h3>{t('notification_settings.trigger_events')}</h3>
  262. <div className="my-1">
  263. <TriggerEventCheckBox
  264. checkbox="success"
  265. event={TriggerEventType.CREATE}
  266. checked={triggerEvents.has(TriggerEventType.CREATE)}
  267. onChange={() => onChangeTriggerEvents(TriggerEventType.CREATE)}
  268. >
  269. <span className="badge rounded-pill bg-success">
  270. <span className="material-symbols-outlined">edit_note</span>{' '}
  271. CREATE
  272. </span>
  273. </TriggerEventCheckBox>
  274. </div>
  275. <div className="my-1">
  276. <TriggerEventCheckBox
  277. checkbox="warning"
  278. event={TriggerEventType.EDIT}
  279. checked={triggerEvents.has(TriggerEventType.EDIT)}
  280. onChange={() => onChangeTriggerEvents(TriggerEventType.EDIT)}
  281. >
  282. <span className="badge rounded-pill bg-warning text-dark">
  283. <span className="material-symbols-outlined">edit</span> EDIT
  284. </span>
  285. </TriggerEventCheckBox>
  286. </div>
  287. <div className="my-1">
  288. <TriggerEventCheckBox
  289. checkbox="pink"
  290. event={TriggerEventType.MOVE}
  291. checked={triggerEvents.has(TriggerEventType.MOVE)}
  292. onChange={() => onChangeTriggerEvents(TriggerEventType.MOVE)}
  293. >
  294. <span className="badge rounded-pill bg-secondary">
  295. <span className="material-symbols-outlined">redo</span>MOVE
  296. </span>
  297. </TriggerEventCheckBox>
  298. </div>
  299. <div className="my-1">
  300. <TriggerEventCheckBox
  301. checkbox="danger"
  302. event="pageDelete"
  303. checked={triggerEvents.has(TriggerEventType.DELETE)}
  304. onChange={() => onChangeTriggerEvents(TriggerEventType.DELETE)}
  305. >
  306. <span className="badge rounded-pill bg-danger">
  307. <span className="material-symbols-outlined">
  308. delete_forever
  309. </span>
  310. DELETE
  311. </span>
  312. </TriggerEventCheckBox>
  313. </div>
  314. <div className="my-1">
  315. <TriggerEventCheckBox
  316. checkbox="info"
  317. event={TriggerEventType.LIKE}
  318. checked={triggerEvents.has(TriggerEventType.LIKE)}
  319. onChange={() => onChangeTriggerEvents(TriggerEventType.LIKE)}
  320. >
  321. <span className="badge rounded-pill bg-info">
  322. <span className="material-symbols-outlined">favorite</span>
  323. LIKE
  324. </span>
  325. </TriggerEventCheckBox>
  326. </div>
  327. <div className="my-1">
  328. <TriggerEventCheckBox
  329. checkbox="secondary"
  330. event={TriggerEventType.POST}
  331. checked={triggerEvents.has(TriggerEventType.POST)}
  332. onChange={() => onChangeTriggerEvents(TriggerEventType.POST)}
  333. >
  334. <span className="badge rounded-pill bg-primary">
  335. <span className="material-symbols-outlined">language</span>
  336. POST
  337. </span>
  338. </TriggerEventCheckBox>
  339. </div>
  340. </div>
  341. </div>
  342. </div>
  343. <AdminUpdateButtonRow
  344. onClick={updateButtonClickedHandler}
  345. disabled={false}
  346. />
  347. </>
  348. );
  349. };
  350. export default ManageGlobalNotification;