ManageCommandsProcess.jsx 14 KB

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