AdminNavigation.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import React, { type JSX, useCallback } from 'react';
  2. import Link from 'next/link';
  3. import { pathUtils } from '@growi/core/dist/utils';
  4. import { useTranslation } from 'next-i18next';
  5. import urljoin from 'url-join';
  6. import {
  7. useGrowiAppIdForGrowiCloud,
  8. useGrowiCloudUri,
  9. } from '~/stores-universal/context';
  10. import styles from './AdminNavigation.module.scss';
  11. const moduleClass = styles['admin-navigation'];
  12. // eslint-disable-next-line react/prop-types
  13. const MenuLabel = ({ menu }: { menu: string }) => {
  14. const { t } = useTranslation(['admin', 'commons']);
  15. switch (menu) {
  16. /* eslint-disable no-multi-spaces, max-len */
  17. case 'app':
  18. return (
  19. <>
  20. <span className="material-symbols-outlined me-1">settings</span>
  21. {t('headers.app_settings', { ns: 'commons' })}
  22. </>
  23. );
  24. case 'security':
  25. return (
  26. <>
  27. <span className="material-symbols-outlined me-1">shield</span>
  28. {t('security_settings.security_settings')}
  29. </>
  30. );
  31. case 'markdown':
  32. return (
  33. <>
  34. <span className="material-symbols-outlined me-1">note</span>
  35. {t('markdown_settings.markdown_settings')}
  36. </>
  37. );
  38. case 'customize':
  39. return (
  40. <>
  41. <span className="material-symbols-outlined me-1">construction</span>
  42. {t('customize_settings.customize_settings')}
  43. </>
  44. );
  45. case 'importer':
  46. return (
  47. <>
  48. <span className="material-symbols-outlined me-1">cloud_upload</span>
  49. {t('importer_management.import_data')}
  50. </>
  51. );
  52. case 'export':
  53. return (
  54. <>
  55. <span className="material-symbols-outlined me-1">cloud_download</span>
  56. {t('export_management.export_archive_data')}
  57. </>
  58. );
  59. case 'data-transfer':
  60. return (
  61. <>
  62. <span className="material-symbols-outlined me-1">flight</span>
  63. {t('g2g_data_transfer.data_transfer', { ns: 'commons' })}
  64. </>
  65. );
  66. case 'notification':
  67. return (
  68. <>
  69. <span className="material-symbols-outlined me-1">notifications</span>
  70. {t('external_notification.external_notification')}
  71. </>
  72. );
  73. case 'slack-integration':
  74. return (
  75. <>
  76. <span className="material-symbols-outlined me-1">shuffle</span>
  77. {t('slack_integration.slack_integration')}
  78. </>
  79. );
  80. case 'slack-integration-legacy':
  81. return (
  82. <>
  83. <span className="material-symbols-outlined me-1">shuffle</span>
  84. {t('slack_integration_legacy.slack_integration_legacy')}
  85. </>
  86. );
  87. case 'users':
  88. return (
  89. <>
  90. <span className="material-symbols-outlined me-1">person</span>
  91. {t('user_management.user_management')}
  92. </>
  93. );
  94. case 'user-groups':
  95. return (
  96. <>
  97. <span className="material-symbols-outlined me-1">group</span>
  98. {t('user_group_management.user_group_management')}
  99. </>
  100. );
  101. case 'audit-log':
  102. return (
  103. <>
  104. <span className="material-symbols-outlined me-1">feed</span>
  105. {t('audit_log_management.audit_log')}
  106. </>
  107. );
  108. case 'plugins':
  109. return (
  110. <>
  111. <span className="material-symbols-outlined me-1">extension</span>
  112. {t('plugins.plugins')}
  113. </>
  114. );
  115. // Temporarily hiding
  116. // case 'ai-integration': return (
  117. // <>{/* TODO: unify sizing of growi-custom-icons so that simplify code -- 2024.10.09 Yuki Takei */}
  118. // <span
  119. // className="growi-custom-icons d-inline-block me-1"
  120. // style={{
  121. // fontSize: '18px', width: '24px', height: '24px', lineHeight: '24px', verticalAlign: 'bottom', paddingLeft: '2px',
  122. // }}
  123. // >
  124. // growi_ai
  125. // </span>
  126. // {t('ai_integration.ai_integration')}
  127. // </>
  128. // );
  129. case 'search':
  130. return (
  131. <>
  132. <span className="material-symbols-outlined me-1">search</span>
  133. {t('full_text_search_management.full_text_search_management')}
  134. </>
  135. );
  136. case 'cloud':
  137. return (
  138. <>
  139. <span className="material-symbols-outlined me-1">share</span>
  140. {t('cloud_setting_management.to_cloud_settings')}{' '}
  141. </>
  142. );
  143. default:
  144. return (
  145. <>
  146. <span className="material-symbols-outlined me-1">home</span>
  147. {t('wiki_management_homepage')}
  148. </>
  149. );
  150. /* eslint-enable no-multi-spaces, max-len */
  151. }
  152. };
  153. type MenuLinkProps = {
  154. menu: string;
  155. isListGroupItems: boolean;
  156. isRoot?: boolean;
  157. isActive?: boolean;
  158. };
  159. const MenuLink = ({
  160. menu,
  161. isRoot,
  162. isListGroupItems,
  163. isActive,
  164. }: MenuLinkProps) => {
  165. const pageTransitionClassName = isListGroupItems
  166. ? 'list-group-item list-group-item-action rounded border-0'
  167. : 'dropdown-item px-3 py-2';
  168. const href = isRoot ? '/admin' : urljoin('/admin', menu);
  169. return (
  170. <Link
  171. href={href}
  172. className={`${pageTransitionClassName} ${isActive ? 'active' : ''}`}
  173. >
  174. <MenuLabel menu={menu} />
  175. </Link>
  176. );
  177. };
  178. export const AdminNavigation = (): JSX.Element => {
  179. const pathname = window.location.pathname;
  180. const { data: growiCloudUri } = useGrowiCloudUri();
  181. const { data: growiAppIdForGrowiCloud } = useGrowiAppIdForGrowiCloud();
  182. const isActiveMenu = useCallback(
  183. (path: string | string[]) => {
  184. const paths = Array.isArray(path) ? path : [path];
  185. return paths.some((path) => {
  186. const basisPath = pathUtils.normalizePath(urljoin('/admin', path));
  187. const basisParentPath = pathUtils.addTrailingSlash(basisPath);
  188. return pathname === basisPath || pathname.startsWith(basisParentPath);
  189. });
  190. },
  191. [pathname],
  192. );
  193. const getListGroupItemOrDropdownItemList = useCallback(
  194. (isListGroupItems: boolean) => {
  195. return (
  196. <>
  197. {/* eslint-disable no-multi-spaces */}
  198. <MenuLink
  199. menu="home"
  200. isListGroupItems={isListGroupItems}
  201. isActive={pathname === '/admin'}
  202. isRoot
  203. />
  204. <MenuLink
  205. menu="app"
  206. isListGroupItems={isListGroupItems}
  207. isActive={isActiveMenu('/app')}
  208. />
  209. <MenuLink
  210. menu="security"
  211. isListGroupItems={isListGroupItems}
  212. isActive={isActiveMenu('/security')}
  213. />
  214. <MenuLink
  215. menu="markdown"
  216. isListGroupItems={isListGroupItems}
  217. isActive={isActiveMenu('/markdown')}
  218. />
  219. <MenuLink
  220. menu="customize"
  221. isListGroupItems={isListGroupItems}
  222. isActive={isActiveMenu('/customize')}
  223. />
  224. <MenuLink
  225. menu="importer"
  226. isListGroupItems={isListGroupItems}
  227. isActive={isActiveMenu('/importer')}
  228. />
  229. <MenuLink
  230. menu="export"
  231. isListGroupItems={isListGroupItems}
  232. isActive={isActiveMenu('/export')}
  233. />
  234. <MenuLink
  235. menu="data-transfer"
  236. isListGroupItems={isListGroupItems}
  237. isActive={isActiveMenu('/data-transfer')}
  238. />
  239. <MenuLink
  240. menu="notification"
  241. isListGroupItems={isListGroupItems}
  242. isActive={isActiveMenu(['/notification', '/global-notification'])}
  243. />
  244. <MenuLink
  245. menu="slack-integration"
  246. isListGroupItems={isListGroupItems}
  247. isActive={isActiveMenu('/slack-integration')}
  248. />
  249. <MenuLink
  250. menu="slack-integration-legacy"
  251. isListGroupItems={isListGroupItems}
  252. isActive={isActiveMenu('/slack-integration-legacy')}
  253. />
  254. <MenuLink
  255. menu="users"
  256. isListGroupItems={isListGroupItems}
  257. isActive={isActiveMenu('/users')}
  258. />
  259. <MenuLink
  260. menu="user-groups"
  261. isListGroupItems={isListGroupItems}
  262. isActive={isActiveMenu(['/user-groups', 'user-group-detail'])}
  263. />
  264. <MenuLink
  265. menu="audit-log"
  266. isListGroupItems={isListGroupItems}
  267. isActive={isActiveMenu('/audit-log')}
  268. />
  269. <MenuLink
  270. menu="plugins"
  271. isListGroupItems={isListGroupItems}
  272. isActive={isActiveMenu('/plugins')}
  273. />
  274. {/* Temporarily hiding */}
  275. {/* <MenuLink menu="ai-integration" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/aai-integration')} /> */}
  276. <MenuLink
  277. menu="search"
  278. isListGroupItems={isListGroupItems}
  279. isActive={isActiveMenu('/search')}
  280. />
  281. {growiCloudUri != null && growiAppIdForGrowiCloud != null && (
  282. <a
  283. href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}
  284. className="list-group-item list-group-item-action border-0 round-corner"
  285. >
  286. <MenuLabel menu="cloud" />
  287. </a>
  288. )}
  289. {/* eslint-enable no-multi-spaces */}
  290. </>
  291. );
  292. },
  293. [growiAppIdForGrowiCloud, growiCloudUri, isActiveMenu, pathname],
  294. );
  295. return (
  296. <React.Fragment>
  297. {/* List group */}
  298. <div className={`list-group ${moduleClass} sticky-top d-none d-lg-block`}>
  299. {getListGroupItemOrDropdownItemList(true)}
  300. </div>
  301. {/* Dropdown */}
  302. <div className="dropdown d-block d-lg-none mb-5">
  303. <button
  304. className="btn btn-outline-primary btn-lg dropdown-toggle col-12 text-end"
  305. type="button"
  306. id="dropdown-admin-navigation"
  307. data-display="static"
  308. data-bs-toggle="dropdown"
  309. aria-haspopup="true"
  310. aria-expanded="false"
  311. >
  312. <span className="float-start">
  313. {/* eslint-disable no-multi-spaces */}
  314. {pathname === '/admin' && <MenuLabel menu="home" />}
  315. {isActiveMenu('/app') && <MenuLabel menu="app" />}
  316. {isActiveMenu('/security') && <MenuLabel menu="security" />}
  317. {isActiveMenu('/markdown') && <MenuLabel menu="markdown" />}
  318. {isActiveMenu('/customize') && <MenuLabel menu="customize" />}
  319. {isActiveMenu('/importer') && <MenuLabel menu="importer" />}
  320. {isActiveMenu('/export') && <MenuLabel menu="export" />}
  321. {isActiveMenu(['/notification', '/global-notification']) && (
  322. <MenuLabel menu="notification" />
  323. )}
  324. {isActiveMenu('/slack-integration') && (
  325. <MenuLabel menu="slack-integration" />
  326. )}
  327. {isActiveMenu('/users') && <MenuLabel menu="users" />}
  328. {isActiveMenu(['/user-groups', 'user-group-detail']) && (
  329. <MenuLabel menu="user-groups" />
  330. )}
  331. {isActiveMenu('/search') && <MenuLabel menu="search" />}
  332. {isActiveMenu('/audit-log') && <MenuLabel menu="audit-log" />}
  333. {isActiveMenu('/plugins') && <MenuLabel menu="plugins" />}
  334. {isActiveMenu('/data-transfer') && (
  335. <MenuLabel menu="data-transfer" />
  336. )}
  337. {/* Temporarily hiding */}
  338. {/* {isActiveMenu('/ai-integration') && <MenuLabel menu="ai-integration" />} */}
  339. {/* eslint-enable no-multi-spaces */}
  340. </span>
  341. </button>
  342. <div
  343. className="dropdown-menu"
  344. role="menu"
  345. aria-labelledby="dropdown-admin-navigation"
  346. >
  347. {getListGroupItemOrDropdownItemList(false)}
  348. </div>
  349. </div>
  350. </React.Fragment>
  351. );
  352. };