AccessTokenForm.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import React from 'react';
  2. import { useTranslation } from 'next-i18next';
  3. import { useForm } from 'react-hook-form';
  4. import type { IAccessTokenInfo } from '~/interfaces/access-token';
  5. const MAX_DESCRIPTION_LENGTH = 200;
  6. type AccessTokenFormProps = {
  7. submitHandler: (info: IAccessTokenInfo) => Promise<void>;
  8. }
  9. type FormInputs = {
  10. expiredAt: string;
  11. description: string;
  12. scopes: string[];
  13. }
  14. export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Element => {
  15. const { submitHandler } = props;
  16. const { t } = useTranslation();
  17. const defaultExpiredAt = new Date();
  18. defaultExpiredAt.setMonth(defaultExpiredAt.getMonth() + 1);
  19. const defaultExpiredAtStr = defaultExpiredAt.toISOString().split('T')[0];
  20. const todayStr = new Date().toISOString().split('T')[0];
  21. const {
  22. register,
  23. handleSubmit,
  24. formState: { errors, isValid },
  25. } = useForm<FormInputs>({
  26. defaultValues: {
  27. expiredAt: defaultExpiredAtStr,
  28. description: '',
  29. scopes: [],
  30. },
  31. });
  32. const onSubmit = (data: FormInputs) => {
  33. const expiredAtDate = new Date(data.expiredAt);
  34. submitHandler({
  35. expiredAt: expiredAtDate,
  36. description: data.description,
  37. scope: data.scopes, // scope の値を正しく送信
  38. });
  39. };
  40. return (
  41. <div className="card mt-3 mb-4">
  42. <div className="card-header">{t('page_me_access_token.form.title')}</div>
  43. <div className="card-body">
  44. <form onSubmit={handleSubmit(onSubmit)}>
  45. <div className="mb-3">
  46. <label htmlFor="expiredAt" className="form-label">{t('page_me_access_token.expiredAt')}</label>
  47. <div className="row">
  48. <div className="col-16 col-sm-4 col-md-4 col-lg-3">
  49. <div className="input-group">
  50. <input
  51. type="date"
  52. className={`form-control ${errors.expiredAt ? 'is-invalid' : ''}`}
  53. data-testid="grw-accesstoken-input-expiredAt"
  54. min={todayStr}
  55. {...register('expiredAt', {
  56. required: t('input_validation.message.required', { param: t('page_me_access_token.expiredAt') }),
  57. validate: (value) => {
  58. const date = new Date(value);
  59. const now = new Date();
  60. return date > now || 'Expiration date must be in the future';
  61. },
  62. })}
  63. />
  64. </div>
  65. {errors.expiredAt && (
  66. <div className="invalid-feedback d-block">
  67. {errors.expiredAt.message}
  68. </div>
  69. )}
  70. </div>
  71. </div>
  72. <div className="form-text">{t('page_me_access_token.form.expiredAt_desc')}</div>
  73. </div>
  74. <div className="mb-3">
  75. <label htmlFor="description" className="form-label">{t('page_me_access_token.description')}</label>
  76. <textarea
  77. className={`form-control ${errors.description ? 'is-invalid' : ''}`}
  78. rows={3}
  79. data-testid="grw-accesstoken-textarea-description"
  80. {...register('description', {
  81. required: t('input_validation.message.required', { param: t('page_me_access_token.description') }),
  82. maxLength: {
  83. value: MAX_DESCRIPTION_LENGTH,
  84. message: t('page_me_access_token.form.description_max_length', { length: MAX_DESCRIPTION_LENGTH }),
  85. },
  86. })}
  87. />
  88. {errors.description && (
  89. <div className="invalid-feedback">
  90. {errors.description.message}
  91. </div>
  92. )}
  93. <div className="form-text">{t('page_me_access_token.form.description_desc')}</div>
  94. </div>
  95. <div className="mb-3">
  96. <label htmlFor="scopes" className="form-label">
  97. {t('page_me_access_token.scopes')}
  98. </label>
  99. <div className="border container rounded py-2 px-3">
  100. {[
  101. { id: 'admin', label: 'admin', desc: 'Access admin data' },
  102. { id: 'user', label: 'user', desc: 'Access user data' },
  103. ].map(({ id, label, desc }, index, array) => (
  104. <>
  105. <div className="row align-items-center" key={id}>
  106. <div className="col-md-3">
  107. <input
  108. className="form-check-input"
  109. type="checkbox"
  110. id={id}
  111. value={id}
  112. {...register('scopes')}
  113. />
  114. <label className="form-check-label fw-bold ms-2" htmlFor={id}>{label}</label>
  115. </div>
  116. <div className="col">
  117. <span className="text-muted">{desc}</span>
  118. </div>
  119. </div>
  120. {index < array.length - 1 && <hr className="my-1" />}
  121. </>
  122. ))}
  123. </div>
  124. <div className="form-text mb-2">{t('page_me_access_token.form.scope_desc')}</div>
  125. </div>
  126. <button
  127. type="submit"
  128. className="btn btn-primary"
  129. data-testid="grw-accesstoken-create-button"
  130. disabled={!isValid}
  131. >
  132. {t('page_me_access_token.create_token')}
  133. </button>
  134. </form>
  135. </div>
  136. </div>
  137. );
  138. });
  139. AccessTokenForm.displayName = 'AccessTokenForm';