ShareLinkForm.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import React, { FC, useState, useCallback } from 'react';
  2. import {
  3. format, parse, addDays, set,
  4. } from 'date-fns';
  5. import { useTranslation } from 'next-i18next';
  6. import { apiv3Post } from '~/client/util/apiv3-client';
  7. import { toastSuccess, toastError } from '~/client/util/toastr';
  8. import { useCurrentPageId } from '~/stores/page';
  9. const ExpirationType = {
  10. UNLIMITED: 'unlimited',
  11. CUSTOM: 'custom',
  12. NUMBER_OF_DAYS: 'numberOfDays',
  13. } as const;
  14. type ExpirationType = typeof ExpirationType[keyof typeof ExpirationType];
  15. type Props = {
  16. onCloseForm: () => void,
  17. }
  18. export const ShareLinkForm: FC<Props> = (props: Props) => {
  19. const { t } = useTranslation();
  20. const { onCloseForm } = props;
  21. const [expirationType, setExpirationType] = useState<ExpirationType>(ExpirationType.UNLIMITED);
  22. const [numberOfDays, setNumberOfDays] = useState<number>(7);
  23. const [description, setDescription] = useState<string>('');
  24. const [customExpirationDate, setCustomExpirationDate] = useState<Date>(new Date());
  25. const [customExpirationTime, setCustomExpirationTime] = useState<Date>(new Date());
  26. const { data: currentPageId } = useCurrentPageId();
  27. const handleChangeExpirationType = useCallback((expirationType: ExpirationType) => {
  28. setExpirationType(expirationType);
  29. }, []);
  30. const handleChangeNumberOfDays = useCallback((numberOfDays: number) => {
  31. setNumberOfDays(numberOfDays);
  32. }, []);
  33. const handleChangeDescription = useCallback((description: string) => {
  34. setDescription(description);
  35. }, []);
  36. const handleChangeCustomExpirationDate = useCallback((customExpirationDate: string) => {
  37. const parsedDate = parse(customExpirationDate, 'yyyy-MM-dd', new Date());
  38. setCustomExpirationDate(parsedDate);
  39. }, []);
  40. const handleChangeCustomExpirationTime = useCallback((customExpirationTime: string) => {
  41. const parsedTime = parse(customExpirationTime, 'HH:mm', new Date());
  42. setCustomExpirationTime(parsedTime);
  43. }, []);
  44. const generateExpired = useCallback(() => {
  45. if (expirationType === ExpirationType.UNLIMITED) {
  46. return null;
  47. }
  48. if (expirationType === ExpirationType.NUMBER_OF_DAYS) {
  49. if (!Number.isInteger(numberOfDays)) {
  50. throw new Error(t('share_links.Invalid_Number_of_Date'));
  51. }
  52. return addDays(new Date(), numberOfDays);
  53. }
  54. if (expirationType === ExpirationType.CUSTOM) {
  55. return set(customExpirationDate, { hours: customExpirationTime.getHours(), minutes: customExpirationTime.getMinutes() });
  56. }
  57. }, [t, customExpirationTime, customExpirationDate, expirationType, numberOfDays]);
  58. const closeForm = useCallback(() => {
  59. if (onCloseForm == null) {
  60. return;
  61. }
  62. onCloseForm();
  63. }, [onCloseForm]);
  64. const handleIssueShareLink = useCallback(async() => {
  65. let expiredAt;
  66. try {
  67. expiredAt = generateExpired();
  68. }
  69. catch (err) {
  70. return toastError(err);
  71. }
  72. try {
  73. await apiv3Post('/share-links/', { relatedPage: currentPageId, expiredAt, description });
  74. closeForm();
  75. toastSuccess(t('toaster.issue_share_link'));
  76. }
  77. catch (err) {
  78. toastError(err);
  79. }
  80. }, [t, currentPageId, description, closeForm, generateExpired]);
  81. return (
  82. <div className="share-link-form p-3">
  83. <h3 className="grw-modal-head pb-2"> { t('share_links.share_settings') }</h3>
  84. <div className=" p-3">
  85. {/* ExpirationTypeOptions */}
  86. <div className="form-group row">
  87. <label htmlFor="inputDesc" className="col-md-5 col-form-label">{t('share_links.expire')}</label>
  88. <div className="col-md-7">
  89. <div className="custom-control custom-radio form-group ">
  90. <input
  91. type="radio"
  92. className="custom-control-input"
  93. id="customRadio1"
  94. name="expirationType"
  95. value="customRadio1"
  96. checked={expirationType === ExpirationType.UNLIMITED}
  97. onChange={() => { handleChangeExpirationType(ExpirationType.UNLIMITED) }}
  98. />
  99. <label className="custom-control-label" htmlFor="customRadio1">{t('share_links.Unlimited')}</label>
  100. </div>
  101. <div className="custom-control custom-radio form-group">
  102. <input
  103. type="radio"
  104. className="custom-control-input"
  105. id="customRadio2"
  106. value="customRadio2"
  107. checked={expirationType === ExpirationType.NUMBER_OF_DAYS}
  108. onChange={() => { handleChangeExpirationType(ExpirationType.NUMBER_OF_DAYS) }}
  109. name="expirationType"
  110. />
  111. <label className="custom-control-label" htmlFor="customRadio2">
  112. <div className="row align-items-center m-0">
  113. <input
  114. type="number"
  115. min="1"
  116. className="col-4"
  117. name="expirationType"
  118. value={numberOfDays}
  119. onFocus={() => { handleChangeExpirationType(ExpirationType.NUMBER_OF_DAYS) }}
  120. onChange={e => handleChangeNumberOfDays(Number(e.target.value))}
  121. />
  122. <span className="col-auto">{t('share_links.Days')}</span>
  123. </div>
  124. </label>
  125. </div>
  126. <div className="custom-control custom-radio form-group text-nowrap mb-0">
  127. <input
  128. type="radio"
  129. className="custom-control-input"
  130. id="customRadio3"
  131. name="expirationType"
  132. value="customRadio3"
  133. checked={expirationType === ExpirationType.CUSTOM}
  134. onChange={() => { handleChangeExpirationType(ExpirationType.CUSTOM) }}
  135. />
  136. <label className="custom-control-label" htmlFor="customRadio3">
  137. {t('share_links.Custom')}
  138. </label>
  139. <div className="d-inline-flex flex-wrap">
  140. <input
  141. type="date"
  142. className="ml-3 mb-2"
  143. name="customExpirationDate"
  144. value={format(customExpirationDate, 'yyyy-MM-dd')}
  145. onFocus={() => { handleChangeExpirationType(ExpirationType.CUSTOM) }}
  146. onChange={e => handleChangeCustomExpirationDate(e.target.value)}
  147. />
  148. <input
  149. type="time"
  150. className="ml-3 mb-2"
  151. name="customExpiration"
  152. value={format(customExpirationTime, 'HH:mm')}
  153. onFocus={() => { handleChangeExpirationType(ExpirationType.CUSTOM) }}
  154. onChange={e => handleChangeCustomExpirationTime(e.target.value)}
  155. />
  156. </div>
  157. </div>
  158. </div>
  159. </div>
  160. {/* DescriptionForm */}
  161. <div className="form-group row">
  162. <label htmlFor="inputDesc" className="col-md-5 col-form-label">{t('share_links.description')}</label>
  163. <div className="col-md-4">
  164. <input
  165. type="text"
  166. className="form-control"
  167. id="inputDesc"
  168. placeholder={t('share_links.enter_desc')}
  169. value={description}
  170. onChange={e => handleChangeDescription(e.target.value)}
  171. />
  172. </div>
  173. </div>
  174. <button type="button" className="btn btn-primary d-block mx-auto px-5" onClick={handleIssueShareLink} data-testid="btn-sharelink-issue">
  175. {t('share_links.Issue')}
  176. </button>
  177. </div>
  178. </div>
  179. );
  180. };