AccessTokenScopeList.tsx 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import type React from 'react';
  2. import type { Scope } from '@growi/core/dist/interfaces';
  3. import { useTranslation } from 'next-i18next';
  4. import type { UseFormRegisterReturn } from 'react-hook-form';
  5. import { useDeviceLargerThanMd } from '~/states/ui/device';
  6. import styles from './AccessTokenScopeList.module.scss';
  7. const moduleClass = styles['access-token-scope-list'] ?? '';
  8. interface scopeObject {
  9. [key: string]: Scope | scopeObject;
  10. }
  11. interface AccessTokenScopeListProps {
  12. scopeObject: scopeObject;
  13. register: UseFormRegisterReturn<'scopes'>;
  14. disabledScopes: Set<Scope>;
  15. level?: number;
  16. }
  17. /**
  18. * Renders the permission object recursively as nested checkboxes.
  19. */
  20. export const AccessTokenScopeList: React.FC<AccessTokenScopeListProps> = ({
  21. scopeObject,
  22. register,
  23. disabledScopes,
  24. level = 1,
  25. }) => {
  26. const [isDeviceLargerThanMd] = useDeviceLargerThanMd();
  27. // Convert object into an array to determine "first vs. non-first" elements
  28. const entries = Object.entries(scopeObject);
  29. const { t } = useTranslation('commons');
  30. return (
  31. <>
  32. {entries.map(([scopeKey, scopeValue], idx) => {
  33. const showHr = (level === 1 || level === 2) && idx !== 0;
  34. if (typeof scopeValue === 'object') {
  35. return (
  36. <div key={scopeKey} className={moduleClass}>
  37. {showHr && <hr className="my-1" />}
  38. <div className="my-1 row">
  39. <div className="col-md-5 ">
  40. <span
  41. className={`form-check-label fw-bold indentation indentation-level-${level}`}
  42. >
  43. {scopeKey}
  44. </span>
  45. </div>
  46. </div>
  47. {/* Render recursively */}
  48. <AccessTokenScopeList
  49. scopeObject={scopeValue as scopeObject}
  50. register={register}
  51. level={level + 1}
  52. disabledScopes={disabledScopes}
  53. />
  54. </div>
  55. );
  56. }
  57. // If it's a string, render a checkbox
  58. return (
  59. <div key={scopeKey} className={`row my-1 ${moduleClass}`}>
  60. <div className="col-md-5 indentation">
  61. <input
  62. data-testid={`grw-accesstoken-checkbox-${scopeValue}`}
  63. className={`form-check-input indentation indentation-level-${level}`}
  64. type="checkbox"
  65. id={scopeValue as string}
  66. disabled={disabledScopes.has(scopeValue)}
  67. value={scopeValue as string}
  68. {...register}
  69. />
  70. <label
  71. className="form-check-label ms-2"
  72. htmlFor={scopeValue as string}
  73. >
  74. {scopeKey}
  75. </label>
  76. </div>
  77. <div
  78. className={`col form-text ${isDeviceLargerThanMd ? '' : 'text-end'}`}
  79. >
  80. {t(`accesstoken_scopes_desc.${scopeKey.replace(/:/g, '.')}`)}
  81. </div>
  82. </div>
  83. );
  84. })}
  85. </>
  86. );
  87. };