ManageCommandsProcess.jsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. import React, { useCallback, useState } from 'react';
  2. import PropTypes from 'prop-types';
  3. import { useTranslation } from 'react-i18next';
  4. import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse, defaultSupportedSlackEventActions } from '@growi/slack';
  5. import loggerFactory from '~/utils/logger';
  6. import { toastSuccess, toastError } from '../../../client/util/apiNotification';
  7. const logger = loggerFactory('growi:SlackIntegration:ManageCommandsProcess');
  8. const PermissionTypes = {
  9. ALLOW_ALL: 'allowAll',
  10. DENY_ALL: 'denyAll',
  11. ALLOW_SPECIFIED: 'allowSpecified',
  12. };
  13. const CommandUsageTypes = {
  14. BROADCAST_USE: 'broadcastUse',
  15. SINGLE_USE: 'singleUse',
  16. };
  17. const EventTypes = {
  18. LINK_SHARING: 'linkSharing',
  19. };
  20. // A utility function that returns the new state but identical to the previous state
  21. const getUpdatedChannelsList = (prevState, commandName, value) => {
  22. // string to array
  23. const allowedChannelsArray = value.split(',');
  24. // trim whitespace from all elements
  25. const trimedAllowedChannelsArray = allowedChannelsArray.map(channelName => channelName.trim());
  26. prevState[commandName] = trimedAllowedChannelsArray;
  27. return prevState;
  28. };
  29. // A utility function that returns the new state
  30. const getUpdatedPermissionSettings = (prevState, commandName, value) => {
  31. const newState = { ...prevState };
  32. switch (value) {
  33. case PermissionTypes.ALLOW_ALL:
  34. newState[commandName] = true;
  35. break;
  36. case PermissionTypes.DENY_ALL:
  37. newState[commandName] = false;
  38. break;
  39. case PermissionTypes.ALLOW_SPECIFIED:
  40. newState[commandName] = [];
  41. break;
  42. default:
  43. logger.error('Not implemented');
  44. break;
  45. }
  46. return newState;
  47. };
  48. // A utility function that returns the permission type from the permission value
  49. const getPermissionTypeFromValue = (value) => {
  50. if (Array.isArray(value)) {
  51. return PermissionTypes.ALLOW_SPECIFIED;
  52. }
  53. if (typeof value === 'boolean') {
  54. return value ? PermissionTypes.ALLOW_ALL : PermissionTypes.DENY_ALL;
  55. }
  56. logger.error('The value type must be boolean or string[]');
  57. };
  58. const PermissionSettingForEachPermissionTypeComponent = ({
  59. keyName, onUpdatePermissions, onUpdateChannels, singleCommandDescription, allowedChannelsDescription, currentPermissionType, permissionSettings,
  60. }) => {
  61. const { t } = useTranslation();
  62. const hiddenClass = currentPermissionType === PermissionTypes.ALLOW_SPECIFIED ? '' : 'd-none';
  63. const permission = permissionSettings[keyName];
  64. if (permission === undefined) logger.error('Must be implemented');
  65. const textareaDefaultValue = Array.isArray(permission) ? permission.join(',') : '';
  66. return (
  67. <div className="my-1 mb-2">
  68. <div className="row align-items-center mb-3">
  69. <p className="col-md-5 text-md-right mb-2">
  70. <strong className="text-capitalize">{keyName}</strong>
  71. {singleCommandDescription && (
  72. <small className="form-text text-muted small">
  73. { singleCommandDescription }
  74. </small>
  75. )}
  76. </p>
  77. <div className="col dropdown">
  78. <button
  79. className="btn btn-outline-secondary dropdown-toggle text-right col-12 col-md-auto"
  80. type="button"
  81. id="dropdownMenuButton"
  82. data-toggle="dropdown"
  83. aria-haspopup="true"
  84. aria-expanded="true"
  85. >
  86. <span className="float-left">
  87. {currentPermissionType === PermissionTypes.ALLOW_ALL
  88. && t('admin:slack_integration.accordion.allow_all')}
  89. {currentPermissionType === PermissionTypes.DENY_ALL
  90. && t('admin:slack_integration.accordion.deny_all')}
  91. {currentPermissionType === PermissionTypes.ALLOW_SPECIFIED
  92. && t('admin:slack_integration.accordion.allow_specified')}
  93. </span>
  94. </button>
  95. <div className="dropdown-menu">
  96. <button
  97. className="dropdown-item"
  98. type="button"
  99. name={keyName}
  100. value={PermissionTypes.ALLOW_ALL}
  101. onClick={onUpdatePermissions}
  102. >
  103. {t('admin:slack_integration.accordion.allow_all_long')}
  104. </button>
  105. <button
  106. className="dropdown-item"
  107. type="button"
  108. name={keyName}
  109. value={PermissionTypes.DENY_ALL}
  110. onClick={onUpdatePermissions}
  111. >
  112. {t('admin:slack_integration.accordion.deny_all_long')}
  113. </button>
  114. <button
  115. className="dropdown-item"
  116. type="button"
  117. name={keyName}
  118. value={PermissionTypes.ALLOW_SPECIFIED}
  119. onClick={onUpdatePermissions}
  120. >
  121. {t('admin:slack_integration.accordion.allow_specified_long')}
  122. </button>
  123. </div>
  124. </div>
  125. </div>
  126. <div className={`row ${hiddenClass}`}>
  127. <div className="col-md-7 offset-md-5">
  128. <textarea
  129. className="form-control"
  130. type="textarea"
  131. name={keyName}
  132. defaultValue={textareaDefaultValue}
  133. onChange={onUpdateChannels}
  134. />
  135. <p className="form-text text-muted small">
  136. {t(allowedChannelsDescription, { keyName })}
  137. </p>
  138. </div>
  139. </div>
  140. </div>
  141. );
  142. };
  143. PermissionSettingForEachPermissionTypeComponent.propTypes = {
  144. keyName: PropTypes.string,
  145. usageType: PropTypes.string,
  146. currentPermissionType: PropTypes.string,
  147. singleCommandDescription: PropTypes.string,
  148. onUpdatePermissions: PropTypes.func,
  149. onUpdateChannels: PropTypes.func,
  150. allowedChannelsDescription: PropTypes.string,
  151. permissionSettings: PropTypes.object,
  152. };
  153. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  154. const ManageCommandsProcess = ({
  155. apiv3Put, slackAppIntegrationId, permissionsForBroadcastUseCommands, permissionsForSingleUseCommands, permissionsForSlackEventActions,
  156. }) => {
  157. const { t } = useTranslation();
  158. const [permissionsForBroadcastUseCommandsState, setPermissionsForBroadcastUseCommandsState] = useState({
  159. search: permissionsForBroadcastUseCommands.search,
  160. });
  161. const [permissionsForSingleUseCommandsState, setPermissionsForSingleUseCommandsState] = useState({
  162. note: permissionsForSingleUseCommands.note,
  163. keep: permissionsForSingleUseCommands.keep,
  164. });
  165. const [permissionsForEventsState, setPermissionsForEventsState] = useState({
  166. unfurl: permissionsForSlackEventActions.unfurl,
  167. });
  168. const [currentPermissionTypes, setCurrentPermissionTypes] = useState(() => {
  169. const initialState = {};
  170. Object.entries(permissionsForBroadcastUseCommandsState).forEach((entry) => {
  171. const [commandName, value] = entry;
  172. initialState[commandName] = getPermissionTypeFromValue(value);
  173. });
  174. Object.entries(permissionsForSingleUseCommandsState).forEach((entry) => {
  175. const [commandName, value] = entry;
  176. initialState[commandName] = getPermissionTypeFromValue(value);
  177. });
  178. Object.entries(permissionsForEventsState).forEach((entry) => {
  179. const [commandName, value] = entry;
  180. initialState[commandName] = getPermissionTypeFromValue(value);
  181. });
  182. return initialState;
  183. });
  184. const handleUpdateSingleUsePermissions = useCallback((e) => {
  185. const { target } = e;
  186. const { name: commandName, value } = target;
  187. setPermissionsForSingleUseCommandsState(prev => getUpdatedPermissionSettings(prev, commandName, value));
  188. setCurrentPermissionTypes((prevState) => {
  189. const newState = { ...prevState };
  190. newState[commandName] = value;
  191. return newState;
  192. });
  193. }, []);
  194. const handleUpdateBroadcastUsePermissions = useCallback((e) => {
  195. const { target } = e;
  196. const { name: commandName, value } = target;
  197. setPermissionsForBroadcastUseCommandsState(prev => getUpdatedPermissionSettings(prev, commandName, value));
  198. setCurrentPermissionTypes((prevState) => {
  199. const newState = { ...prevState };
  200. newState[commandName] = value;
  201. return newState;
  202. });
  203. }, []);
  204. const handleUpdateEventsPermissions = useCallback((e) => {
  205. const { target } = e;
  206. const { name: commandName, value } = target;
  207. setPermissionsForEventsState(prev => getUpdatedPermissionSettings(prev, commandName, value));
  208. setCurrentPermissionTypes((prevState) => {
  209. const newState = { ...prevState };
  210. newState[commandName] = value;
  211. return newState;
  212. });
  213. }, []);
  214. const handleUpdateSingleUseChannels = useCallback((e) => {
  215. const { target } = e;
  216. const { name: commandName, value } = target;
  217. setPermissionsForSingleUseCommandsState(prev => getUpdatedChannelsList(prev, commandName, value));
  218. }, []);
  219. const handleUpdateBroadcastUseChannels = useCallback((e) => {
  220. const { target } = e;
  221. const { name: commandName, value } = target;
  222. setPermissionsForBroadcastUseCommandsState(prev => getUpdatedChannelsList(prev, commandName, value));
  223. }, []);
  224. const handleUpdateEventsChannels = useCallback((e) => {
  225. const { target } = e;
  226. const { name: commandName, value } = target;
  227. setPermissionsForEventsState(prev => getUpdatedChannelsList(prev, commandName, value));
  228. }, []);
  229. const updateSettingsHandler = async(e) => {
  230. try {
  231. // TODO: add new attribute 78975
  232. await apiv3Put(`/slack-integration-settings/slack-app-integrations/${slackAppIntegrationId}/permissions`, {
  233. permissionsForBroadcastUseCommands: permissionsForBroadcastUseCommandsState,
  234. permissionsForSingleUseCommands: permissionsForSingleUseCommandsState,
  235. permissionsForSlackEventActions: permissionsForEventsState,
  236. });
  237. toastSuccess(t('toaster.update_successed', { target: 'Token' }));
  238. }
  239. catch (err) {
  240. toastError(err);
  241. logger.error(err);
  242. }
  243. };
  244. const PermissionSettingsForEachCategoryComponent = ({
  245. currentPermissionTypes,
  246. usageType,
  247. menuItem,
  248. }) => {
  249. const permissionMap = {
  250. broadcastUse: permissionsForBroadcastUseCommandsState,
  251. singleUse: permissionsForSingleUseCommandsState,
  252. linkSharing: permissionsForEventsState,
  253. };
  254. const {
  255. title,
  256. description,
  257. defaultCommandsName,
  258. singleCommandDescription,
  259. updatePermissionsHandler,
  260. updateChannelsHandler,
  261. allowedChannelsDescription,
  262. } = menuItem;
  263. return (
  264. <>
  265. {(title || description) && (
  266. <div className="row">
  267. <div className="col-md-7 offset-md-2">
  268. { title && <p className="font-weight-bold mb-1">{title}</p> }
  269. { description && <p className="text-muted">{description}</p> }
  270. </div>
  271. </div>
  272. )}
  273. <div className="custom-control custom-checkbox">
  274. <div className="row mb-5 d-block">
  275. {defaultCommandsName.map(keyName => (
  276. <PermissionSettingForEachPermissionTypeComponent
  277. key={`${keyName}-component`}
  278. keyName={keyName}
  279. usageType={usageType}
  280. permissionSettings={permissionMap[usageType]}
  281. currentPermissionType={currentPermissionTypes[keyName]}
  282. singleCommandDescription={singleCommandDescription}
  283. onUpdatePermissions={updatePermissionsHandler}
  284. onUpdateChannels={updateChannelsHandler}
  285. allowedChannelsDescription={allowedChannelsDescription}
  286. />
  287. ))}
  288. </div>
  289. </div>
  290. </>
  291. );
  292. };
  293. PermissionSettingsForEachCategoryComponent.propTypes = {
  294. currentPermissionTypes: PropTypes.object,
  295. usageType: PropTypes.string,
  296. menuItem: PropTypes.object,
  297. };
  298. // Using i18n in allowedChannelsDescription will cause interpolation error
  299. const menuMap = {
  300. broadcastUse: {
  301. title: 'Multiple GROWI',
  302. description: t('admin:slack_integration.accordion.multiple_growi_command'),
  303. defaultCommandsName: defaultSupportedCommandsNameForBroadcastUse,
  304. updatePermissionsHandler: handleUpdateBroadcastUsePermissions,
  305. updateChannelsHandler: handleUpdateBroadcastUseChannels,
  306. allowedChannelsDescription: 'admin:slack_integration.accordion.allowed_channels_description',
  307. },
  308. singleUse: {
  309. title: 'Single GROWI',
  310. description: t('admin:slack_integration.accordion.single_growi_command'),
  311. defaultCommandsName: defaultSupportedCommandsNameForSingleUse,
  312. updatePermissionsHandler: handleUpdateSingleUsePermissions,
  313. updateChannelsHandler: handleUpdateSingleUseChannels,
  314. allowedChannelsDescription: 'admin:slack_integration.accordion.allowed_channels_description',
  315. },
  316. linkSharing: {
  317. defaultCommandsName: defaultSupportedSlackEventActions,
  318. updatePermissionsHandler: handleUpdateEventsPermissions,
  319. updateChannelsHandler: handleUpdateEventsChannels,
  320. singleCommandDescription: t('admin:slack_integration.accordion.unfurl_description'),
  321. allowedChannelsDescription: 'admin:slack_integration.accordion.unfurl_allowed_channels_description',
  322. },
  323. };
  324. return (
  325. <div className="py-4 px-5">
  326. <p className="mb-4 font-weight-bold">{t('admin:slack_integration.accordion.growi_commands')}</p>
  327. <div className="row d-flex flex-column align-items-center">
  328. <div className="col-8">
  329. {Object.values(CommandUsageTypes).map(commandUsageType => (
  330. <PermissionSettingsForEachCategoryComponent
  331. key={commandUsageType}
  332. currentPermissionTypes={currentPermissionTypes}
  333. usageType={commandUsageType}
  334. menuItem={menuMap[commandUsageType]}
  335. />
  336. ))}
  337. </div>
  338. </div>
  339. <p className="mb-4 font-weight-bold">Events</p>
  340. <div className="row d-flex flex-column align-items-center">
  341. <div className="col-8">
  342. {Object.values(EventTypes).map(EventType => (
  343. <PermissionSettingsForEachCategoryComponent
  344. key={EventType}
  345. currentPermissionTypes={currentPermissionTypes}
  346. usageType={EventType}
  347. menuItem={menuMap[EventType]}
  348. />
  349. ))}
  350. </div>
  351. </div>
  352. <div className="row">
  353. <button
  354. type="submit"
  355. className="btn btn-primary mx-auto"
  356. onClick={updateSettingsHandler}
  357. >
  358. { t('Update') }
  359. </button>
  360. </div>
  361. </div>
  362. );
  363. };
  364. ManageCommandsProcess.propTypes = {
  365. apiv3Put: PropTypes.func,
  366. slackAppIntegrationId: PropTypes.string.isRequired,
  367. permissionsForBroadcastUseCommands: PropTypes.object.isRequired,
  368. permissionsForSingleUseCommands: PropTypes.object.isRequired,
  369. permissionsForSlackEventActions: PropTypes.object.isRequired,
  370. };
  371. export default ManageCommandsProcess;