TemplateModal.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import React, {
  2. useCallback, useEffect, useState,
  3. } from 'react';
  4. import assert from 'assert';
  5. import { Lang } from '@growi/core';
  6. import type { TemplateSummary, TemplateStatus } from '@growi/pluginkit/dist/v4';
  7. import { useTranslation } from 'next-i18next';
  8. import {
  9. Modal,
  10. ModalHeader,
  11. ModalBody,
  12. ModalFooter,
  13. UncontrolledDropdown,
  14. DropdownToggle,
  15. DropdownMenu,
  16. DropdownItem,
  17. } from 'reactstrap';
  18. import { useSWRxTemplate, useSWRxTemplates } from '~/features/templates/stores';
  19. import { useTemplateModal } from '~/stores/modal';
  20. import { usePersonalSettings } from '~/stores/personal-settings';
  21. import { usePreviewOptions } from '~/stores/renderer';
  22. import loggerFactory from '~/utils/logger';
  23. import Preview from '../PageEditor/Preview';
  24. import { useFormatter } from './use-formatter';
  25. const logger = loggerFactory('growi:components:TemplateModal');
  26. function constructTemplateId(templateSummary: TemplateSummary): string {
  27. const defaultTemplate = templateSummary.default;
  28. return `${defaultTemplate.pluginId ?? ''}_${defaultTemplate.id}`;
  29. }
  30. type TemplateItemProps = {
  31. templateId: string,
  32. template: TemplateStatus,
  33. locales: Set<string>,
  34. onClick: () => void,
  35. isSelected: boolean,
  36. }
  37. const TemplateItem: React.FC<TemplateItemProps> = ({
  38. templateId,
  39. template,
  40. locales,
  41. onClick,
  42. isSelected,
  43. }) => {
  44. assert(template.isValid);
  45. return (
  46. <a
  47. key={templateId}
  48. className={`list-group-item list-group-item-action ${isSelected ? 'active' : ''}`}
  49. onClick={onClick}
  50. aria-current="true"
  51. >
  52. <h4 className="mb-1">{template.title}</h4>
  53. <p className="mb-2">{template.desc}</p>
  54. { Array.from(locales).map(locale => (
  55. <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
  56. ))}
  57. </a>
  58. );
  59. };
  60. type TemplateMenuProps = {
  61. templateSummaries: TemplateSummary[],
  62. onClickHandler: (templateSummary: TemplateSummary, template: TemplateStatus, locales: Set<string>) => void,
  63. usersDefaultLang: Lang,
  64. selectedTemplateSummary?: TemplateSummary,
  65. }
  66. const TemplateMenu: React.FC<TemplateMenuProps> = ({
  67. templateSummaries,
  68. onClickHandler,
  69. usersDefaultLang,
  70. selectedTemplateSummary,
  71. }) => {
  72. return (
  73. <>
  74. {templateSummaries.map((templateSummary) => {
  75. const templateId = constructTemplateId(templateSummary);
  76. const locales = new Set(Object.values(templateSummary).map(s => s.locale));
  77. const template = usersDefaultLang in templateSummary
  78. ? templateSummary[usersDefaultLang]
  79. : templateSummary.default;
  80. const isSelected = selectedTemplateSummary != null && constructTemplateId(selectedTemplateSummary) === templateId;
  81. const onClick = () => {
  82. onClickHandler(templateSummary, template, locales);
  83. };
  84. return (
  85. <TemplateItem
  86. key={templateId}
  87. templateId={templateId}
  88. template={template}
  89. locales={locales}
  90. onClick={onClick}
  91. isSelected={isSelected}
  92. />
  93. );
  94. })}
  95. </>
  96. );
  97. };
  98. export const TemplateModal = (): JSX.Element => {
  99. const { t } = useTranslation(['translation', 'commons']);
  100. const { data: templateModalStatus, close } = useTemplateModal();
  101. const { data: personalSettingsInfo } = usePersonalSettings();
  102. const { data: rendererOptions } = usePreviewOptions();
  103. const { data: templateSummaries } = useSWRxTemplates();
  104. const [selectedTemplateSummary, setSelectedTemplateSummary] = useState<TemplateSummary>();
  105. const [selectedTemplateLocales, setSelectedTemplateLocales] = useState<Set<string>>();
  106. const [selectedTemplateLocale, setSelectedTemplateLocale] = useState<string>();
  107. const [selectedTemplate, setSelectedTemplate] = useState<TemplateStatus>();
  108. const { data: selectedTemplateMarkdown } = useSWRxTemplate(selectedTemplateSummary, selectedTemplateLocale);
  109. const { format } = useFormatter();
  110. const usersDefaultLang = personalSettingsInfo?.lang;
  111. const submitHandler = useCallback((markdown?: string) => {
  112. if (templateModalStatus == null || markdown == null) {
  113. return;
  114. }
  115. if (templateModalStatus.onSubmit == null) {
  116. close();
  117. return;
  118. }
  119. templateModalStatus.onSubmit(format(selectedTemplateMarkdown));
  120. close();
  121. }, [close, format, selectedTemplateMarkdown, templateModalStatus]);
  122. const onClickHandler = useCallback((
  123. templateSummary: TemplateSummary,
  124. template: TemplateStatus,
  125. locales: Set<string>,
  126. ) => {
  127. if (usersDefaultLang == null) {
  128. return;
  129. }
  130. if (selectedTemplateLocale != null && selectedTemplateLocale in templateSummary) {
  131. setSelectedTemplateLocale(selectedTemplateLocale);
  132. }
  133. else {
  134. setSelectedTemplateLocale(usersDefaultLang in templateSummary ? usersDefaultLang : undefined);
  135. }
  136. setSelectedTemplateSummary(templateSummary);
  137. setSelectedTemplate(template);
  138. setSelectedTemplateLocales(locales);
  139. }, [selectedTemplateLocale, usersDefaultLang]);
  140. useEffect(() => {
  141. if (!templateModalStatus?.isOpened) {
  142. setSelectedTemplateSummary(undefined);
  143. setSelectedTemplateLocale(undefined);
  144. }
  145. }, [templateModalStatus?.isOpened]);
  146. if (templateSummaries == null || templateModalStatus == null || usersDefaultLang == null) {
  147. return <></>;
  148. }
  149. return (
  150. <Modal className="link-edit-modal" isOpen={templateModalStatus.isOpened} toggle={close} size="xl" autoFocus={false}>
  151. <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
  152. {t('template.modal_label.Select template')}
  153. </ModalHeader>
  154. <ModalBody className="container">
  155. <div className="row">
  156. {/* List Group */}
  157. <div className="d-none d-lg-block col-lg-4">
  158. <div className="list-group">
  159. <TemplateMenu
  160. templateSummaries={templateSummaries}
  161. onClickHandler={onClickHandler}
  162. usersDefaultLang={usersDefaultLang}
  163. selectedTemplateSummary={selectedTemplateSummary}
  164. />
  165. </div>
  166. </div>
  167. {/* Dropdown */}
  168. <div className='d-lg-none col mb-3'>
  169. <UncontrolledDropdown>
  170. <DropdownToggle caret type="button" outline className='w-100 text-right'>
  171. <span className="float-left">{(selectedTemplate != null && selectedTemplate.isValid) ? selectedTemplate.title : t('Select template')}</span>
  172. </DropdownToggle>
  173. <DropdownMenu role="menu" className='p-0'>
  174. <TemplateMenu
  175. templateSummaries={templateSummaries}
  176. onClickHandler={onClickHandler}
  177. usersDefaultLang={usersDefaultLang}
  178. selectedTemplateSummary={selectedTemplateSummary}
  179. />
  180. </DropdownMenu>
  181. </UncontrolledDropdown>
  182. </div>
  183. <div className="col-12 col-lg-8">
  184. <div className='row mb-2 mb-lg-0'>
  185. <div className="col-6">
  186. <h3>{t('preview')}</h3>
  187. </div>
  188. <div className="col-6 d-flex justify-content-end">
  189. <UncontrolledDropdown>
  190. <DropdownToggle caret type="button" outline className='float-right'>
  191. <span className="float-left">{selectedTemplateLocale != null ? selectedTemplateLocale : t('Language')}</span>
  192. </DropdownToggle>
  193. <DropdownMenu className="dropdown-menu" role="menu">
  194. { selectedTemplateLocales != null && Array.from(selectedTemplateLocales).map((locale) => {
  195. return (
  196. <DropdownItem
  197. key={locale}
  198. onClick={() => setSelectedTemplateLocale(locale)}>
  199. <span>{locale}</span>
  200. </DropdownItem>
  201. );
  202. }) }
  203. </DropdownMenu>
  204. </UncontrolledDropdown>
  205. </div>
  206. </div>
  207. <div className='card'>
  208. <div className="card-body" style={{ height: '400px', overflowY: 'auto' }}>
  209. { rendererOptions != null && selectedTemplateSummary != null && (
  210. <Preview rendererOptions={rendererOptions} markdown={format(selectedTemplateMarkdown)}/>
  211. ) }
  212. </div>
  213. </div>
  214. </div>
  215. </div>
  216. </ModalBody>
  217. <ModalFooter>
  218. <button type="button" className="btn btn-outline-secondary mx-1" onClick={close}>
  219. {t('Cancel')}
  220. </button>
  221. <button
  222. type="submit"
  223. className="btn btn-primary mx-1"
  224. onClick={() => submitHandler(selectedTemplateMarkdown)}
  225. disabled={selectedTemplateSummary == null}>
  226. {t('commons:Insert')}
  227. </button>
  228. </ModalFooter>
  229. </Modal>
  230. );
  231. };