AccessTokenScopeList.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  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. // biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
  9. // @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
  10. interface scopeObject {
  11. [key: string]: Scope | scopeObject;
  12. }
  13. interface AccessTokenScopeListProps {
  14. scopeObject: scopeObject;
  15. register: UseFormRegisterReturn<'scopes'>;
  16. disabledScopes: Set<Scope>;
  17. level?: number;
  18. }
  19. /**
  20. * Renders the permission object recursively as nested checkboxes.
  21. */
  22. export const AccessTokenScopeList: React.FC<AccessTokenScopeListProps> = ({
  23. scopeObject,
  24. register,
  25. disabledScopes,
  26. level = 1,
  27. }) => {
  28. const [isDeviceLargerThanMd] = useDeviceLargerThanMd();
  29. // Convert object into an array to determine "first vs. non-first" elements
  30. const entries = Object.entries(scopeObject);
  31. const { t } = useTranslation('commons');
  32. return (
  33. <>
  34. {entries.map(([scopeKey, scopeValue], idx) => {
  35. const showHr = (level === 1 || level === 2) && idx !== 0;
  36. if (typeof scopeValue === 'object') {
  37. return (
  38. <div key={scopeKey} className={moduleClass}>
  39. {showHr && <hr className="my-1" />}
  40. <div className="my-1 row">
  41. <div className="col-md-5 ">
  42. <span
  43. className={`form-check-label fw-bold indentation indentation-level-${level}`}
  44. >
  45. {scopeKey}
  46. </span>
  47. </div>
  48. </div>
  49. {/* Render recursively */}
  50. <AccessTokenScopeList
  51. scopeObject={scopeValue as scopeObject}
  52. register={register}
  53. level={level + 1}
  54. disabledScopes={disabledScopes}
  55. />
  56. </div>
  57. );
  58. }
  59. // If it's a string, render a checkbox
  60. return (
  61. <div key={scopeKey} className={`row my-1 ${moduleClass}`}>
  62. <div className="col-md-5 indentation">
  63. <input
  64. data-testid={`grw-accesstoken-checkbox-${scopeValue}`}
  65. className={`form-check-input indentation indentation-level-${level}`}
  66. type="checkbox"
  67. id={scopeValue as string}
  68. disabled={disabledScopes.has(scopeValue)}
  69. value={scopeValue as string}
  70. {...register}
  71. />
  72. <label
  73. className="form-check-label ms-2"
  74. htmlFor={scopeValue as string}
  75. >
  76. {scopeKey}
  77. </label>
  78. </div>
  79. <div
  80. className={`col form-text ${isDeviceLargerThanMd ? '' : 'text-end'}`}
  81. >
  82. {t(`accesstoken_scopes_desc.${scopeKey.replace(/:/g, '.')}`)}
  83. </div>
  84. </div>
  85. );
  86. })}
  87. </>
  88. );
  89. };