AccessTokenForm.tsx 5.2 KB

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