|
|
@@ -2,18 +2,28 @@ import React, {
|
|
|
useCallback, useEffect, useState,
|
|
|
} from 'react';
|
|
|
|
|
|
-import type { ITemplate } from '@growi/core/dist/interfaces/template';
|
|
|
+import assert from 'assert';
|
|
|
+
|
|
|
+import { Lang } from '@growi/core';
|
|
|
+import {
|
|
|
+ extractSupportedLocales, getLocalizedTemplate, type TemplateSummary,
|
|
|
+} from '@growi/pluginkit/dist/v4';
|
|
|
import { useTranslation } from 'next-i18next';
|
|
|
import {
|
|
|
Modal,
|
|
|
ModalHeader,
|
|
|
ModalBody,
|
|
|
ModalFooter,
|
|
|
+ UncontrolledDropdown,
|
|
|
+ DropdownToggle,
|
|
|
+ DropdownMenu,
|
|
|
+ DropdownItem,
|
|
|
} from 'reactstrap';
|
|
|
|
|
|
-import { useTemplateModal } from '~/stores/modal';
|
|
|
+import { useSWRxTemplate, useSWRxTemplates } from '~/features/templates/stores';
|
|
|
+import { useTemplateModal, type TemplateModalStatus } from '~/stores/modal';
|
|
|
+import { usePersonalSettings } from '~/stores/personal-settings';
|
|
|
import { usePreviewOptions } from '~/stores/renderer';
|
|
|
-import { useTemplates } from '~/stores/template';
|
|
|
import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
import Preview from '../PageEditor/Preview';
|
|
|
@@ -23,108 +33,239 @@ import { useFormatter } from './use-formatter';
|
|
|
const logger = loggerFactory('growi:components:TemplateModal');
|
|
|
|
|
|
|
|
|
-type TemplateRadioButtonProps = {
|
|
|
- template: ITemplate,
|
|
|
- onChange: (selectedTemplate: ITemplate) => void,
|
|
|
+function constructTemplateId(templateSummary: TemplateSummary): string {
|
|
|
+ const defaultTemplate = templateSummary.default;
|
|
|
+
|
|
|
+ return `${defaultTemplate.pluginId ?? ''}_${defaultTemplate.id}`;
|
|
|
+}
|
|
|
+
|
|
|
+type TemplateItemProps = {
|
|
|
+ templateSummary: TemplateSummary,
|
|
|
+ selectedLocale?: string,
|
|
|
+ onClick?: () => void,
|
|
|
isSelected?: boolean,
|
|
|
+ usersDefaultLang?: Lang,
|
|
|
}
|
|
|
|
|
|
-const TemplateRadioButton = ({ template, onChange, isSelected }: TemplateRadioButtonProps): JSX.Element => {
|
|
|
- const radioButtonId = `rb-${template.id}`;
|
|
|
+const TemplateItem: React.FC<TemplateItemProps> = ({
|
|
|
+ templateSummary,
|
|
|
+ onClick,
|
|
|
+ isSelected,
|
|
|
+ usersDefaultLang,
|
|
|
+}) => {
|
|
|
+ const localizedTemplate = getLocalizedTemplate(templateSummary, usersDefaultLang);
|
|
|
+ const templateLocales = extractSupportedLocales(templateSummary);
|
|
|
+
|
|
|
+ assert(localizedTemplate?.isValid);
|
|
|
|
|
|
return (
|
|
|
- <div key={template.id} className="custom-control custom-radio mb-2">
|
|
|
- <input
|
|
|
- id={radioButtonId}
|
|
|
- type="radio"
|
|
|
- className="custom-control-input"
|
|
|
- checked={isSelected}
|
|
|
- onChange={() => onChange(template)}
|
|
|
- />
|
|
|
- <label className="custom-control-label" htmlFor={radioButtonId}>
|
|
|
- {template.name}
|
|
|
- </label>
|
|
|
- </div>
|
|
|
+ <a
|
|
|
+ className={`list-group-item list-group-item-action ${isSelected ? 'active' : ''}`}
|
|
|
+ onClick={onClick}
|
|
|
+ aria-current="true"
|
|
|
+ >
|
|
|
+ <h4 className="mb-1">{localizedTemplate.title}</h4>
|
|
|
+ <p className="mb-2">{localizedTemplate.desc}</p>
|
|
|
+ { templateLocales != null && Array.from(templateLocales).map(locale => (
|
|
|
+ <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
|
|
|
+ ))}
|
|
|
+ </a>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
-export const TemplateModal = (): JSX.Element => {
|
|
|
- const { t } = useTranslation(['translation', 'commons']);
|
|
|
+type TemplateModalSubstanceProps = {
|
|
|
+ templateModalStatus: TemplateModalStatus,
|
|
|
+ close: () => void,
|
|
|
+}
|
|
|
|
|
|
+const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element => {
|
|
|
+ const { templateModalStatus, close } = props;
|
|
|
|
|
|
- const { data: templateModalStatus, close } = useTemplateModal();
|
|
|
+ const { t } = useTranslation(['translation', 'commons']);
|
|
|
|
|
|
+ const { data: personalSettingsInfo } = usePersonalSettings();
|
|
|
const { data: rendererOptions } = usePreviewOptions();
|
|
|
- const { data: templates } = useTemplates();
|
|
|
+ const { data: templateSummaries } = useSWRxTemplates();
|
|
|
+
|
|
|
+ const [selectedTemplateSummary, setSelectedTemplateSummary] = useState<TemplateSummary>();
|
|
|
+ const [selectedTemplateLocale, setSelectedTemplateLocale] = useState<string>();
|
|
|
|
|
|
- const [selectedTemplate, setSelectedTemplate] = useState<ITemplate>();
|
|
|
+ const { data: selectedTemplateMarkdown } = useSWRxTemplate(selectedTemplateSummary, selectedTemplateLocale);
|
|
|
|
|
|
const { format } = useFormatter();
|
|
|
|
|
|
- const submitHandler = useCallback((template?: ITemplate) => {
|
|
|
- if (templateModalStatus == null || selectedTemplate == null) {
|
|
|
+ const usersDefaultLang = personalSettingsInfo?.lang;
|
|
|
+ const selectedLocalizedTemplate = getLocalizedTemplate(selectedTemplateSummary, usersDefaultLang);
|
|
|
+ const selectedTemplateLocales = extractSupportedLocales(selectedTemplateSummary);
|
|
|
+
|
|
|
+ const submitHandler = useCallback((markdown?: string) => {
|
|
|
+ if (markdown == null) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- if (templateModalStatus.onSubmit == null || template == null) {
|
|
|
+ if (templateModalStatus.onSubmit == null) {
|
|
|
close();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- templateModalStatus.onSubmit(format(selectedTemplate));
|
|
|
+ templateModalStatus.onSubmit(format(selectedTemplateMarkdown));
|
|
|
close();
|
|
|
- }, [close, format, selectedTemplate, templateModalStatus]);
|
|
|
+ }, [close, format, selectedTemplateMarkdown, templateModalStatus]);
|
|
|
+
|
|
|
+ const onClickHandler = useCallback((
|
|
|
+ templateSummary: TemplateSummary,
|
|
|
+ ) => {
|
|
|
+ let localeToSet: string | Lang | undefined;
|
|
|
+
|
|
|
+ if (selectedTemplateLocale != null && selectedTemplateLocale in templateSummary) {
|
|
|
+ localeToSet = selectedTemplateLocale;
|
|
|
+ }
|
|
|
+ else if (usersDefaultLang != null && usersDefaultLang in templateSummary) {
|
|
|
+ localeToSet = usersDefaultLang;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ localeToSet = undefined;
|
|
|
+ }
|
|
|
+
|
|
|
+ setSelectedTemplateLocale(localeToSet);
|
|
|
+ setSelectedTemplateSummary(templateSummary);
|
|
|
+ }, [selectedTemplateLocale, usersDefaultLang]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
- if (!templateModalStatus?.isOpened) {
|
|
|
- setSelectedTemplate(undefined);
|
|
|
+ if (!templateModalStatus.isOpened) {
|
|
|
+ setSelectedTemplateSummary(undefined);
|
|
|
+ setSelectedTemplateLocale(undefined);
|
|
|
}
|
|
|
- }, [templateModalStatus?.isOpened]);
|
|
|
+ }, [templateModalStatus.isOpened]);
|
|
|
|
|
|
- if (templates == null || templateModalStatus == null) {
|
|
|
+ if (templateSummaries == null) {
|
|
|
return <></>;
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
- <Modal className="link-edit-modal" isOpen={templateModalStatus.isOpened} toggle={close} size="lg" autoFocus={false}>
|
|
|
+ <>
|
|
|
<ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
|
|
|
{t('template.modal_label.Select template')}
|
|
|
</ModalHeader>
|
|
|
-
|
|
|
<ModalBody className="container">
|
|
|
<div className="row">
|
|
|
- <div className="col-12">
|
|
|
- { templates.map(template => (
|
|
|
- <TemplateRadioButton
|
|
|
- key={template.id}
|
|
|
- template={template}
|
|
|
- onChange={selected => setSelectedTemplate(selected)}
|
|
|
- isSelected={template.id === selectedTemplate?.id}
|
|
|
- />
|
|
|
- )) }
|
|
|
+ {/* List Group */}
|
|
|
+ <div className="d-none d-lg-block col-lg-4">
|
|
|
+ <div className="list-group">
|
|
|
+ {templateSummaries.map((templateSummary) => {
|
|
|
+ const templateId = constructTemplateId(templateSummary);
|
|
|
+ const isSelected = selectedTemplateSummary != null && constructTemplateId(selectedTemplateSummary) === templateId;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <TemplateItem
|
|
|
+ key={templateId}
|
|
|
+ templateSummary={templateSummary}
|
|
|
+ onClick={() => onClickHandler(templateSummary)}
|
|
|
+ isSelected={isSelected}
|
|
|
+ usersDefaultLang={usersDefaultLang}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ {/* Dropdown */}
|
|
|
+ <div className='d-lg-none col mb-3'>
|
|
|
+ <UncontrolledDropdown>
|
|
|
+ <DropdownToggle caret type="button" outline className='w-100 text-right'>
|
|
|
+ <span className="float-left">
|
|
|
+ {selectedLocalizedTemplate != null && selectedLocalizedTemplate.isValid
|
|
|
+ ? selectedLocalizedTemplate.title
|
|
|
+ : t('Select template')}
|
|
|
+ </span>
|
|
|
+ </DropdownToggle>
|
|
|
+ <DropdownMenu role="menu" className='p-0'>
|
|
|
+ {templateSummaries.map((templateSummary, index) => {
|
|
|
+ const templateId = constructTemplateId(templateSummary);
|
|
|
+ const localizedTemplate = getLocalizedTemplate(templateSummary, usersDefaultLang);
|
|
|
+ const templateLocales = extractSupportedLocales(templateSummary);
|
|
|
|
|
|
- <hr />
|
|
|
+ assert(localizedTemplate?.isValid);
|
|
|
|
|
|
- <h3>{t('Preview')}</h3>
|
|
|
- <div className='card'>
|
|
|
- <div className="card-body" style={{ height: '400px', overflowY: 'auto' }}>
|
|
|
- { rendererOptions != null && selectedTemplate != null && (
|
|
|
- <Preview rendererOptions={rendererOptions} markdown={format(selectedTemplate)}/>
|
|
|
- ) }
|
|
|
+ return (
|
|
|
+ <DropdownItem
|
|
|
+ key={templateId}
|
|
|
+ onClick={() => onClickHandler(templateSummary)}
|
|
|
+ className={`px-4 py-3 ${index === 0 ? '' : 'border-top'}`}
|
|
|
+ >
|
|
|
+ <h4 className="mb-1 text-wrap">{localizedTemplate.title}</h4>
|
|
|
+ <p className="mb-1 text-wrap">{localizedTemplate.desc}</p>
|
|
|
+ { templateLocales != null && Array.from(templateLocales).map(locale => (
|
|
|
+ <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
|
|
|
+ ))}
|
|
|
+ </DropdownItem>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </DropdownMenu>
|
|
|
+ </UncontrolledDropdown>
|
|
|
+ </div>
|
|
|
+ <div className="col-12 col-lg-8">
|
|
|
+ <div className='row mb-2 mb-lg-0'>
|
|
|
+ <div className="col-6">
|
|
|
+ <h3>{t('preview')}</h3>
|
|
|
+ </div>
|
|
|
+ <div className="col-6 d-flex justify-content-end">
|
|
|
+ <UncontrolledDropdown>
|
|
|
+ <DropdownToggle caret type="button" outline className='float-right' disabled={selectedTemplateSummary == null}>
|
|
|
+ <span className="float-left">{selectedTemplateLocale != null ? selectedTemplateLocale : t('Language')}</span>
|
|
|
+ </DropdownToggle>
|
|
|
+ <DropdownMenu className="dropdown-menu" role="menu">
|
|
|
+ { selectedTemplateLocales != null && Array.from(selectedTemplateLocales).map((locale) => {
|
|
|
+ return (
|
|
|
+ <DropdownItem
|
|
|
+ key={locale}
|
|
|
+ onClick={() => setSelectedTemplateLocale(locale)}>
|
|
|
+ <span>{locale}</span>
|
|
|
+ </DropdownItem>
|
|
|
+ );
|
|
|
+ }) }
|
|
|
+ </DropdownMenu>
|
|
|
+ </UncontrolledDropdown>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className='card'>
|
|
|
+ <div className="card-body" style={{ height: '400px', overflowY: 'auto' }}>
|
|
|
+ { rendererOptions != null && selectedTemplateSummary != null && (
|
|
|
+ <Preview rendererOptions={rendererOptions} markdown={format(selectedTemplateMarkdown)}/>
|
|
|
+ ) }
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
</ModalBody>
|
|
|
<ModalFooter>
|
|
|
- <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={close}>
|
|
|
+ <button type="button" className="btn btn-outline-secondary mx-1" onClick={close}>
|
|
|
{t('Cancel')}
|
|
|
</button>
|
|
|
- <button type="submit" className="btn btn-sm btn-primary mx-1" onClick={() => submitHandler(selectedTemplate)} disabled={selectedTemplate == null}>
|
|
|
+ <button
|
|
|
+ type="submit"
|
|
|
+ className="btn btn-primary mx-1"
|
|
|
+ onClick={() => submitHandler(selectedTemplateMarkdown)}
|
|
|
+ disabled={selectedTemplateSummary == null}>
|
|
|
{t('commons:Insert')}
|
|
|
</button>
|
|
|
</ModalFooter>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+export const TemplateModal = (): JSX.Element => {
|
|
|
+ const { data: templateModalStatus, close } = useTemplateModal();
|
|
|
+
|
|
|
+ if (templateModalStatus == null) {
|
|
|
+ return <></>;
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Modal className="link-edit-modal" isOpen={templateModalStatus.isOpened} toggle={close} size="xl" autoFocus={false}>
|
|
|
+ { templateModalStatus.isOpened && (
|
|
|
+ <TemplateModalSubstance templateModalStatus={templateModalStatus} close={close} />
|
|
|
+ ) }
|
|
|
</Modal>
|
|
|
);
|
|
|
};
|