ManageCommandsProcessWithoutProxy.jsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import React, { useCallback, useEffect, useState } from 'react';
  2. import {
  3. defaultSupportedCommandsNameForBroadcastUse,
  4. defaultSupportedCommandsNameForSingleUse,
  5. defaultSupportedSlackEventActions,
  6. } from '@growi/slack';
  7. import { useTranslation } from 'next-i18next';
  8. import PropTypes from 'prop-types';
  9. import { apiv3Put } from '~/client/util/apiv3-client';
  10. import { toastError, toastSuccess } from '~/client/util/toastr';
  11. import loggerFactory from '~/utils/logger';
  12. const logger = loggerFactory('growi:SlackIntegration:ManageCommandsProcess');
  13. const PermissionTypes = {
  14. ALLOW_ALL: 'allowAll',
  15. DENY_ALL: 'denyAll',
  16. ALLOW_SPECIFIED: 'allowSpecified',
  17. };
  18. const defaultCommandsName = [
  19. ...defaultSupportedCommandsNameForBroadcastUse,
  20. ...defaultSupportedCommandsNameForSingleUse,
  21. ];
  22. // A utility function that returns the new state but identical to the previous state
  23. const getUpdatedChannelsList = (commandPermissionObj, commandName, value) => {
  24. // string to array
  25. const allowedChannelsArray = value.split(',');
  26. // trim whitespace from all elements
  27. const trimedAllowedChannelsArray = allowedChannelsArray.map((channelName) =>
  28. channelName.trim(),
  29. );
  30. commandPermissionObj[commandName] = trimedAllowedChannelsArray;
  31. return commandPermissionObj;
  32. };
  33. // A utility function that returns the new state
  34. const getUpdatedPermissionSettings = (
  35. commandPermissionObj,
  36. commandName,
  37. value,
  38. ) => {
  39. const editedCommandPermissionObj = { ...commandPermissionObj };
  40. switch (value) {
  41. case PermissionTypes.ALLOW_ALL:
  42. editedCommandPermissionObj[commandName] = true;
  43. break;
  44. case PermissionTypes.DENY_ALL:
  45. editedCommandPermissionObj[commandName] = false;
  46. break;
  47. case PermissionTypes.ALLOW_SPECIFIED:
  48. editedCommandPermissionObj[commandName] = [];
  49. break;
  50. default:
  51. logger.error('Not implemented');
  52. break;
  53. }
  54. return editedCommandPermissionObj;
  55. };
  56. const SinglePermissionSettingComponent = ({
  57. commandName,
  58. editingCommandPermission,
  59. onPermissionTypeClicked,
  60. onPermissionListChanged,
  61. }) => {
  62. const { t } = useTranslation();
  63. if (editingCommandPermission == null) {
  64. return null;
  65. }
  66. function permissionTypeClickHandler(e) {
  67. if (onPermissionTypeClicked == null) {
  68. return;
  69. }
  70. onPermissionTypeClicked(e);
  71. }
  72. function onPermissionListChangeHandler(e) {
  73. if (onPermissionListChanged == null) {
  74. return;
  75. }
  76. onPermissionListChanged(e);
  77. }
  78. const permission = editingCommandPermission[commandName];
  79. const hiddenClass = Array.isArray(permission) ? '' : 'd-none';
  80. const textareaDefaultValue = Array.isArray(permission)
  81. ? permission.join(',')
  82. : '';
  83. return (
  84. <div className="my-1 mb-2">
  85. <div className="row align-items-center mb-3">
  86. <p className="col my-auto text-capitalize align-middle">
  87. {commandName}
  88. </p>
  89. <div className="col dropdown">
  90. <button
  91. className="btn btn-outline-secondary dropdown-toggle text-end col-12 col-md-auto"
  92. type="button"
  93. id="dropdownMenuButton"
  94. data-bs-toggle="dropdown"
  95. aria-haspopup="true"
  96. aria-expanded="true"
  97. >
  98. <span className="float-start">
  99. {permission === true &&
  100. t('admin:slack_integration.accordion.allow_all')}
  101. {permission === false &&
  102. t('admin:slack_integration.accordion.deny_all')}
  103. {Array.isArray(permission) &&
  104. t('admin:slack_integration.accordion.allow_specified')}
  105. </span>
  106. </button>
  107. <div className="dropdown-menu">
  108. <button
  109. className="dropdown-item"
  110. type="button"
  111. name={commandName}
  112. value={PermissionTypes.ALLOW_ALL}
  113. onClick={(e) => permissionTypeClickHandler(e)}
  114. >
  115. {t('admin:slack_integration.accordion.allow_all_long')}
  116. </button>
  117. <button
  118. className="dropdown-item"
  119. type="button"
  120. name={commandName}
  121. value={PermissionTypes.DENY_ALL}
  122. onClick={(e) => permissionTypeClickHandler(e)}
  123. >
  124. {t('admin:slack_integration.accordion.deny_all_long')}
  125. </button>
  126. <button
  127. className="dropdown-item"
  128. type="button"
  129. name={commandName}
  130. value={PermissionTypes.ALLOW_SPECIFIED}
  131. onClick={(e) => permissionTypeClickHandler(e)}
  132. >
  133. {t('admin:slack_integration.accordion.allow_specified_long')}
  134. </button>
  135. </div>
  136. </div>
  137. </div>
  138. <div className={`row-12 row-md-6 ${hiddenClass}`}>
  139. <textarea
  140. className="form-control"
  141. type="textarea"
  142. name={commandName}
  143. value={textareaDefaultValue}
  144. onChange={(e) => onPermissionListChangeHandler(e)}
  145. />
  146. <p className="form-text text-muted small">
  147. {t('admin:slack_integration.accordion.allowed_channels_description', {
  148. commandName,
  149. })}
  150. <br />
  151. </p>
  152. </div>
  153. </div>
  154. );
  155. };
  156. SinglePermissionSettingComponent.propTypes = {
  157. commandName: PropTypes.string,
  158. editingCommandPermission: PropTypes.object,
  159. onPermissionTypeClicked: PropTypes.func,
  160. onPermissionListChanged: PropTypes.func,
  161. };
  162. // biome-ignore lint:*:noExplicitModuleBoundaryTypes: Temporary Alternative to @typescript-eslint/explicit-module-boundary-types
  163. const ManageCommandsProcessWithoutProxy = ({
  164. commandPermission,
  165. eventActionsPermission,
  166. }) => {
  167. const { t } = useTranslation();
  168. const [editingCommandPermission, setEditingCommandPermission] = useState({});
  169. const [editingEventActionsPermission, setEditingEventActionsPermission] =
  170. useState({});
  171. useEffect(() => {
  172. if (commandPermission == null) {
  173. return;
  174. }
  175. const updatedState = { ...commandPermission };
  176. setEditingCommandPermission(updatedState);
  177. }, [commandPermission]);
  178. useEffect(() => {
  179. if (eventActionsPermission == null) {
  180. return;
  181. }
  182. const updatedState = { ...eventActionsPermission };
  183. setEditingEventActionsPermission(updatedState);
  184. }, [eventActionsPermission]);
  185. const updatePermissionsCommandsState = useCallback((e) => {
  186. const { target } = e;
  187. const { name: commandName, value } = target;
  188. setEditingCommandPermission((commandPermissionObj) =>
  189. getUpdatedPermissionSettings(commandPermissionObj, commandName, value),
  190. );
  191. }, []);
  192. const updatePermissionsEventsState = useCallback((e) => {
  193. const { target } = e;
  194. const { name: actionName, value } = target;
  195. setEditingEventActionsPermission((eventActionPermissionObj) =>
  196. getUpdatedPermissionSettings(eventActionPermissionObj, actionName, value),
  197. );
  198. }, []);
  199. const updateCommandsChannelsListState = useCallback((e) => {
  200. const { target } = e;
  201. const { name: commandName, value } = target;
  202. setEditingCommandPermission((commandPermissionObj) => ({
  203. ...getUpdatedChannelsList(commandPermissionObj, commandName, value),
  204. }));
  205. }, []);
  206. const updateEventsChannelsListState = useCallback((e) => {
  207. const { target } = e;
  208. const { name: actionName, value } = target;
  209. setEditingEventActionsPermission((eventActionPermissionObj) => ({
  210. ...getUpdatedChannelsList(eventActionPermissionObj, actionName, value),
  211. }));
  212. }, []);
  213. const updateCommandsHandler = async (e) => {
  214. try {
  215. await apiv3Put(
  216. '/slack-integration-settings/without-proxy/update-permissions',
  217. {
  218. commandPermission: editingCommandPermission,
  219. eventActionsPermission: editingEventActionsPermission,
  220. },
  221. );
  222. toastSuccess(
  223. t('toaster.update_successed', {
  224. target: 'the permission for commands',
  225. ns: 'commons',
  226. }),
  227. );
  228. } catch (err) {
  229. toastError(err);
  230. logger.error(err);
  231. }
  232. };
  233. return (
  234. <div className="py-4 px-5">
  235. <p className="mb-4 fw-bold">
  236. {t('admin:slack_integration.accordion.growi_commands')}
  237. </p>
  238. <div className="row d-flex flex-column align-items-center">
  239. <div className="col-8">
  240. <div className="form-check">
  241. <div className="row mb-5 d-block">
  242. {defaultCommandsName.map((commandName) => {
  243. return (
  244. <SinglePermissionSettingComponent
  245. key={`${commandName}-component`}
  246. commandName={commandName}
  247. editingCommandPermission={editingCommandPermission}
  248. onPermissionTypeClicked={updatePermissionsCommandsState}
  249. onPermissionListChanged={updateCommandsChannelsListState}
  250. />
  251. );
  252. })}
  253. </div>
  254. </div>
  255. </div>
  256. </div>
  257. <p className="mb-4 fw-bold">Events</p>
  258. <div className="row d-flex flex-column align-items-center">
  259. <div className="col-8">
  260. <div className="form-check">
  261. <div className="row mb-5 d-block">
  262. {defaultSupportedSlackEventActions.map((actionName) => (
  263. <SinglePermissionSettingComponent
  264. key={`${actionName}-component`}
  265. commandName={actionName}
  266. editingCommandPermission={editingEventActionsPermission}
  267. onPermissionTypeClicked={updatePermissionsEventsState}
  268. onPermissionListChanged={updateEventsChannelsListState}
  269. />
  270. ))}
  271. </div>
  272. </div>
  273. </div>
  274. </div>
  275. <div className="row">
  276. <button
  277. type="submit"
  278. className="btn btn-primary mx-auto"
  279. onClick={updateCommandsHandler}
  280. >
  281. {t('Update')}
  282. </button>
  283. </div>
  284. </div>
  285. );
  286. };
  287. ManageCommandsProcessWithoutProxy.propTypes = {
  288. commandPermission: PropTypes.object,
  289. eventActionsPermission: PropTypes.object,
  290. };
  291. export default ManageCommandsProcessWithoutProxy;