jam411 3 yıl önce
ebeveyn
işleme
28f9602873

+ 1 - 1
packages/app/src/client/services/AdminHomeContainer.js

@@ -66,7 +66,7 @@ export default class AdminHomeContainer extends Container {
         nodeVersion: adminHomeParams.nodeVersion,
         nodeVersion: adminHomeParams.nodeVersion,
         npmVersion: adminHomeParams.npmVersion,
         npmVersion: adminHomeParams.npmVersion,
         yarnVersion: adminHomeParams.yarnVersion,
         yarnVersion: adminHomeParams.yarnVersion,
-        installedPlugins: adminHomeParams.installedPlugins,
+        // installedPlugins: adminHomeParams.installedPlugins,
         envVars: adminHomeParams.envVars,
         envVars: adminHomeParams.envVars,
         isV5Compatible: adminHomeParams.isV5Compatible,
         isV5Compatible: adminHomeParams.isV5Compatible,
         isMaintenanceMode: adminHomeParams.isMaintenanceMode,
         isMaintenanceMode: adminHomeParams.isMaintenanceMode,

+ 3 - 3
packages/app/src/components/Admin/AdminHome/InstalledPluginTable.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 
 
@@ -27,7 +27,7 @@ const InstalledPluginTable = (props) => {
         </tr>
         </tr>
       </thead>
       </thead>
       <tbody>
       <tbody>
-        {adminHomeContainer.state.installedPlugins.map((plugin) => {
+        {/* {adminHomeContainer.state.installedPlugins.map((plugin) => {
           return (
           return (
             <tr key={plugin.name}>
             <tr key={plugin.name}>
               <td>{plugin.name}</td>
               <td>{plugin.name}</td>
@@ -35,7 +35,7 @@ const InstalledPluginTable = (props) => {
               <td data-hide-in-vrt className="text-center">{plugin.installedVersion}</td>
               <td data-hide-in-vrt className="text-center">{plugin.installedVersion}</td>
             </tr>
             </tr>
           );
           );
-        })}
+        })} */}
       </tbody>
       </tbody>
     </table>
     </table>
   );
   );

+ 6 - 0
packages/app/src/components/Page.tsx

@@ -37,6 +37,8 @@ declare const globalEmitter: EventEmitter;
 const GridEditModal = dynamic(() => import('./PageEditor/GridEditModal'), { ssr: false });
 const GridEditModal = dynamic(() => import('./PageEditor/GridEditModal'), { ssr: false });
 // const HandsontableModal = dynamic(() => import('./PageEditor/HandsontableModal'), { ssr: false });
 // const HandsontableModal = dynamic(() => import('./PageEditor/HandsontableModal'), { ssr: false });
 const LinkEditModal = dynamic(() => import('./PageEditor/LinkEditModal'), { ssr: false });
 const LinkEditModal = dynamic(() => import('./PageEditor/LinkEditModal'), { ssr: false });
+const TemplateModal = dynamic(() => import('./TemplateModal'), { ssr: false });
+
 
 
 const logger = loggerFactory('growi:Page');
 const logger = loggerFactory('growi:Page');
 
 
@@ -57,6 +59,8 @@ class PageSubstance extends React.Component<PageSubstanceProps> {
 
 
   linkEditModal: any;
   linkEditModal: any;
 
 
+  templateModal: any;
+
   handsontableModal: any;
   handsontableModal: any;
 
 
   drawioModal: any;
   drawioModal: any;
@@ -71,6 +75,7 @@ class PageSubstance extends React.Component<PageSubstanceProps> {
 
 
     this.gridEditModal = React.createRef();
     this.gridEditModal = React.createRef();
     this.linkEditModal = React.createRef();
     this.linkEditModal = React.createRef();
+    this.templateModal = React.createRef();
     this.handsontableModal = React.createRef();
     this.handsontableModal = React.createRef();
     this.drawioModal = React.createRef();
     this.drawioModal = React.createRef();
 
 
@@ -185,6 +190,7 @@ class PageSubstance extends React.Component<PageSubstanceProps> {
           <>
           <>
             <GridEditModal ref={this.gridEditModal} />
             <GridEditModal ref={this.gridEditModal} />
             <LinkEditModal ref={this.linkEditModal} />
             <LinkEditModal ref={this.linkEditModal} />
+            <TemplateModal ref={this.templateModal} />
             {/* <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} /> */}
             {/* <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} /> */}
             {/* TODO: use global DrawioModal https://redmine.weseek.co.jp/issues/105981 */}
             {/* TODO: use global DrawioModal https://redmine.weseek.co.jp/issues/105981 */}
             {/* <DrawioModal
             {/* <DrawioModal

+ 20 - 0
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -14,6 +14,7 @@ import InterceptorManager from '~/services/interceptor-manager';
 import { useDrawioModal } from '~/stores/modal';
 import { useDrawioModal } from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import TemplateModal from '../TemplateModal';
 import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 
 
 import AbstractEditor from './AbstractEditor';
 import AbstractEditor from './AbstractEditor';
@@ -115,6 +116,7 @@ class CodeMirrorEditor extends AbstractEditor {
     this.cm = React.createRef();
     this.cm = React.createRef();
     this.gridEditModal = React.createRef();
     this.gridEditModal = React.createRef();
     this.linkEditModal = React.createRef();
     this.linkEditModal = React.createRef();
+    this.templateModal = React.createRef();
     this.handsontableModal = React.createRef();
     this.handsontableModal = React.createRef();
     this.drawioModal = React.createRef();
     this.drawioModal = React.createRef();
 
 
@@ -159,6 +161,8 @@ class CodeMirrorEditor extends AbstractEditor {
     this.foldDrawioSection = this.foldDrawioSection.bind(this);
     this.foldDrawioSection = this.foldDrawioSection.bind(this);
     this.onSaveForDrawio = this.onSaveForDrawio.bind(this);
     this.onSaveForDrawio = this.onSaveForDrawio.bind(this);
 
 
+    this.showTemplateModal = this.showTemplateModal.bind(this);
+
   }
   }
 
 
   init() {
   init() {
@@ -870,6 +874,9 @@ class CodeMirrorEditor extends AbstractEditor {
     // this.handsontableModal.current.show(mtu.getMarkdownTable(this.getCodeMirror()));
     // this.handsontableModal.current.show(mtu.getMarkdownTable(this.getCodeMirror()));
   }
   }
 
 
+  showTemplateModal() {
+    this.templateModal.current.show(markdownLinkUtil.getMarkdownLink(this.getCodeMirror()));
+  }
 
 
   // fold draw.io section (::: drawio ~ :::)
   // fold draw.io section (::: drawio ~ :::)
   foldDrawioSection() {
   foldDrawioSection() {
@@ -1034,6 +1041,15 @@ class CodeMirrorEditor extends AbstractEditor {
       >
       >
         <EditorIcon icon="Emoji" />
         <EditorIcon icon="Emoji" />
       </Button>,
       </Button>,
+      <Button
+        key="nav-item-template"
+        color={null}
+        bssize="small"
+        title="Template"
+        onClick={() => this.showTemplateModal()}
+      >
+        <EditorIcon icon="Template" />
+      </Button>,
     ];
     ];
   }
   }
 
 
@@ -1127,6 +1143,10 @@ class CodeMirrorEditor extends AbstractEditor {
           ref={this.linkEditModal}
           ref={this.linkEditModal}
           onSave={(linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
           onSave={(linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
         />
         />
+        <TemplateModal
+          ref={this.templateModal}
+          onSave={(templateText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), templateText) }}
+        />
         {/* <HandsontableModal
         {/* <HandsontableModal
           ref={this.handsontableModal}
           ref={this.handsontableModal}
           onSave={(table) => { return mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }}
           onSave={(table) => { return mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }}

+ 9 - 0
packages/app/src/components/PageEditor/EditorIcon.jsx

@@ -1,5 +1,6 @@
 /* eslint-disable max-len */
 /* eslint-disable max-len */
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 const EditorIcon = (props) => {
 const EditorIcon = (props) => {
@@ -139,6 +140,14 @@ const EditorIcon = (props) => {
           </g>
           </g>
         </svg>
         </svg>
       );
       );
+    case 'Template':
+      // TODO: Fix template icon
+      return (
+        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
+          <rect fillOpacity="0" width="30" height="30" />
+          <path d="M9.71,22.5a2.57,2.57,0,0,1-1.85-.79,2.79,2.79,0,0,1,0-4l9-9.23a3.21,3.21,0,0,1,1.59-.87,3.39,3.39,0,0,1,1.81.1,4.38,4.38,0,0,1,1.7,1.05,4.15,4.15,0,0,1,.46.56,3.73,3.73,0,0,1,.35.65,4.25,4.25,0,0,1,.2.72,3.91,3.91,0,0,1,.07.76,3.71,3.71,0,0,1-1.12,2.67l-6.79,7a.48.48,0,0,1-.34.16.51.51,0,0,1-.35-.13.48.48,0,0,1,0-.7l6.78-7a2.8,2.8,0,0,0,.84-2,2.58,2.58,0,0,0-.79-2,3.63,3.63,0,0,0-1.11-.75,2.41,2.41,0,0,0-1.31-.17,2.19,2.19,0,0,0-1.25.62l-9,9.22A1.8,1.8,0,0,0,8,19.69,1.78,1.78,0,0,0,8.58,21a1.81,1.81,0,0,0,.57.39,1.48,1.48,0,0,0,.66.1,2,2,0,0,0,1.28-.62l7.12-7.35.15-.16a1.15,1.15,0,0,0,.15-.2.9.9,0,0,0,.12-.24,1.17,1.17,0,0,0,.07-.25.52.52,0,0,0-.05-.27.75.75,0,0,0-.19-.26.73.73,0,0,0-.58-.27,1.29,1.29,0,0,0-.67.38l-5.36,5.53a.5.5,0,0,1-.22.13.46.46,0,0,1-.26,0,.48.48,0,0,1-.22-.12A.41.41,0,0,1,11,17.5a.5.5,0,0,1,.14-.35L16.5,11.6a2.19,2.19,0,0,1,1.29-.67,1.69,1.69,0,0,1,1.37.55,1.54,1.54,0,0,1,.53,1.31,2.26,2.26,0,0,1-.76,1.42L11.8,21.58a3.06,3.06,0,0,1-2,.91H9.71Z" />
+        </svg>
+      );
   }
   }
 
 
 
 

+ 495 - 0
packages/app/src/components/TemplateModal.jsx

@@ -0,0 +1,495 @@
+import React from 'react';
+
+import path from 'path';
+
+import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
+import {
+  Modal,
+  ModalHeader,
+  ModalBody,
+  ModalFooter,
+  Popover,
+  PopoverBody,
+} from 'reactstrap';
+import validator from 'validator';
+
+
+import Linker from '~/client/models/Linker';
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { useCurrentPagePath } from '~/stores/context';
+
+import PagePreviewIcon from './Icons/PagePreviewIcon';
+import Preview from './PageEditor/Preview';
+import SearchTypeahead from './SearchTypeahead';
+
+
+import styles from './PageEditor/LinkEditPreview.module.scss';
+
+const presetA = {
+  name: 'presetA',
+  value: '## Preset',
+};
+
+const presetB = {
+  name: 'presetB',
+  value: '### Preset',
+};
+
+const presetC = {
+  name: 'presetC',
+  value: '#### Preset',
+};
+
+const templates = [presetA, presetB, presetC];
+
+class TemplateModal extends React.PureComponent {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      show: false,
+      isUseRelativePath: false,
+      isUsePermanentLink: false,
+      linkInputValue: '',
+      labelInputValue: '',
+      linkerType: '',
+      markdown: null,
+      pagePath: null,
+      previewError: '',
+      permalink: '',
+      isPreviewOpen: false,
+    };
+
+    // this.isApplyPukiwikiLikeLinkerPlugin = window.growiRenderer.preProcessors.some(process => process.constructor.name === 'PukiwikiLikeLinker');
+
+    this.show = this.show.bind(this);
+    this.hide = this.hide.bind(this);
+    this.cancel = this.cancel.bind(this);
+    this.handleChangeTypeahead = this.handleChangeTypeahead.bind(this);
+    this.handleChangeLabelInput = this.handleChangeLabelInput.bind(this);
+    this.handleChangeLinkInput = this.handleChangeLinkInput.bind(this);
+    // this.handleSelecteLinkerType = this.handleSelecteLinkerType.bind(this);
+    this.toggleIsUseRelativePath = this.toggleIsUseRelativePath.bind(this);
+    this.toggleIsUsePamanentLink = this.toggleIsUsePamanentLink.bind(this);
+    this.save = this.save.bind(this);
+    this.generateLink = this.generateLink.bind(this);
+    this.getRootPath = this.getRootPath.bind(this);
+    this.toggleIsPreviewOpen = this.toggleIsPreviewOpen.bind(this);
+    this.setMarkdown = this.setMarkdown.bind(this);
+  }
+
+  // defaultMarkdownLink is an instance of Linker
+  show(defaultMarkdownLink = null) {
+    // if defaultMarkdownLink is null, set default value in inputs.
+    const { label = '', link = '' } = defaultMarkdownLink;
+    let { type = Linker.types.markdownLink } = defaultMarkdownLink;
+
+    // if type of defaultMarkdownLink is pukiwikiLink when pukiwikiLikeLinker plugin is disable, change type(not change label and link)
+    if (type === Linker.types.pukiwikiLink && !this.isApplyPukiwikiLikeLinkerPlugin) {
+      type = Linker.types.markdownLink;
+    }
+
+    this.parseLinkAndSetState(link, type);
+
+    this.setState({
+      show: true,
+      labelInputValue: label,
+      isUsePermanentLink: false,
+      permalink: '',
+      linkerType: 'type',
+    });
+  }
+
+  // getCodeMirror() {
+  //   return this.cm.current?.editor;
+  // }
+
+  // parse link, link is ...
+  // case-1. url of this growi's page (ex. 'http://localhost:3000/hoge/fuga')
+  // case-2. absolute path of this growi's page (ex. '/hoge/fuga')
+  // case-3. relative path of this growi's page (ex. '../fuga', 'hoge')
+  // case-4. external link (ex. 'https://growi.org')
+  // case-5. the others (ex. '')
+  parseLinkAndSetState(link, type) {
+    // create url from link, add dummy origin if link is not valid url.
+    // ex-1. link = 'https://growi.org/' -> url = 'https://growi.org/' (case-1,4)
+    // ex-2. link = 'hoge' -> url = 'http://example.com/hoge' (case-2,3,5)
+    const url = new URL(link, 'http://example.com');
+    const isUrl = url.origin !== 'http://example.com';
+
+    let isUseRelativePath = false;
+    let reshapedLink = link;
+
+    // if case-1, reshapedLink becomes page path
+    reshapedLink = this.convertUrlToPathIfPageUrl(reshapedLink, url);
+
+    // case-3
+    if (!isUrl && !reshapedLink.startsWith('/') && reshapedLink !== '') {
+      isUseRelativePath = true;
+      const rootPath = this.getRootPath(type);
+      reshapedLink = path.resolve(rootPath, reshapedLink);
+    }
+
+    this.setState({
+      linkInputValue: reshapedLink,
+      isUseRelativePath,
+    });
+  }
+
+  // return path name of link if link is this growi page url, else return original link.
+  convertUrlToPathIfPageUrl(link, url) {
+    // when link is this growi's page url, url.origin === window.location.origin and return path name
+    return url.origin === window.location.origin ? decodeURI(url.pathname) : link;
+  }
+
+  cancel() {
+    this.hide();
+  }
+
+  hide() {
+    this.setState({
+      show: false,
+    });
+  }
+
+  toggleIsUseRelativePath() {
+    if (!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink) {
+      return;
+    }
+
+    // User can't use both relativePath and permalink at the same time
+    this.setState({ isUseRelativePath: !this.state.isUseRelativePath, isUsePermanentLink: false });
+  }
+
+  toggleIsUsePamanentLink() {
+    if (this.state.permalink === '' || this.state.linkerType === Linker.types.growiLink) {
+      return;
+    }
+
+    // User can't use both relativePath and permalink at the same time
+    this.setState({ isUsePermanentLink: !this.state.isUsePermanentLink, isUseRelativePath: false });
+  }
+
+  async setMarkdown() {
+    const { t } = this.props;
+    const path = this.state.linkInputValue;
+    let markdown = null;
+    let pagePath = null;
+    let permalink = '';
+    let previewError = '';
+
+    if (path.startsWith('/')) {
+      const pathWithoutFragment = new URL(path, 'http://dummy').pathname;
+      const isPermanentLink = validator.isMongoId(pathWithoutFragment.slice(1));
+      const pageId = isPermanentLink ? pathWithoutFragment.slice(1) : null;
+
+      try {
+        const { data } = await apiv3Get('/page', { path: pathWithoutFragment, page_id: pageId });
+        const { page } = data;
+        markdown = page.revision.body;
+        pagePath = page.path;
+        permalink = page.id;
+      }
+      catch (err) {
+        previewError = err.message;
+      }
+    }
+    else {
+      previewError = t('link_edit.page_not_found_in_preview', { path });
+    }
+    this.setState({
+      markdown, pagePath, previewError, permalink,
+    });
+  }
+
+  renderLinkPreview() {
+    const linker = this.generateLink();
+    return (
+      <div className="d-flex justify-content-between mb-3 flex-column flex-sm-row">
+        <div className="card card-disabled w-100 p-1 mb-0">
+          <p className="text-left text-muted mb-1 small">Markdown</p>
+          <p className="text-center text-truncate text-muted">{linker.generateMarkdownText()}</p>
+        </div>
+        <div className="d-flex align-items-center justify-content-center">
+          <span className="lead mx-3">
+            <i className="d-none d-sm-block fa fa-caret-right"></i>
+            <i className="d-sm-none fa fa-caret-down"></i>
+          </span>
+        </div>
+        <div className="card w-100 p-1 mb-0">
+          <p className="text-left text-muted mb-1 small">HTML</p>
+          <p className="text-center text-truncate">
+            <a href={linker.link}>{linker.label}</a>
+          </p>
+        </div>
+      </div>
+    );
+  }
+
+  handleChangeTypeahead(selected) {
+    const pageWithMeta = selected[0];
+    if (pageWithMeta != null) {
+      const page = pageWithMeta.data;
+      const permalink = `${window.location.origin}/${page.id}`;
+      this.setState({ linkInputValue: page.path, permalink });
+    }
+  }
+
+  handleChangeLabelInput(label) {
+    this.setState({ labelInputValue: label });
+  }
+
+  handleChangeLinkInput(link) {
+    let isUseRelativePath = this.state.isUseRelativePath;
+    if (!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink) {
+      isUseRelativePath = false;
+    }
+    this.setState({
+      linkInputValue: link, isUseRelativePath, isUsePermanentLink: false, permalink: '',
+    });
+  }
+
+  handleSelecteLinkerType(linkerType) {
+    // let { isUseRelativePath, isUsePermanentLink } = this.state;
+    // if (linkerType === Linker.types.growiLink) {
+    //   isUseRelativePath = false;
+    //   isUsePermanentLink = false;
+    // }
+
+    this.setState({ linkerType });
+  }
+
+  save() {
+    const { linkerType } = this.state;
+
+    if (this.props.onSave != null) {
+      this.props.onSave(linkerType);
+    }
+
+    this.hide();
+  }
+
+  generateLink() {
+    const {
+      linkInputValue, labelInputValue, linkerType, isUseRelativePath, isUsePermanentLink, permalink,
+    } = this.state;
+
+    // let reshapedLink = linkInputValue;
+    // if (isUseRelativePath) {
+    //   const rootPath = this.getRootPath(linkerType);
+    //   reshapedLink = rootPath === linkInputValue ? '.' : path.relative(rootPath, linkInputValue);
+    // }
+
+    // if (isUsePermanentLink && permalink != null) {
+    //   reshapedLink = permalink;
+    // }
+
+    return linkerType;
+  }
+
+  getRootPath(type) {
+    const { pagePath } = this.props;
+    // rootPaths of md link and pukiwiki link are different
+    return type === Linker.types.markdownLink ? path.dirname(pagePath) : pagePath;
+  }
+
+  async toggleIsPreviewOpen() {
+    // open popover
+    if (this.state.isPreviewOpen === false) {
+      this.setMarkdown();
+    }
+    this.setState({ isPreviewOpen: !this.state.isPreviewOpen });
+  }
+
+  renderLinkAndLabelForm() {
+    const { t } = this.props;
+    const { pagePath } = this.state;
+
+    return (
+      <>
+        <h3 className="grw-modal-head">{t('link_edit.set_link_and_label')}</h3>
+        <form className="form-group">
+          <div className="form-gorup my-3">
+            <div className="input-group flex-nowrap">
+              <div className="input-group-prepend">
+                <span className="input-group-text">{t('link_edit.link')}</span>
+              </div>
+              <SearchTypeahead
+                onChange={this.handleChangeTypeahead}
+                onInputChange={this.handleChangeLinkInput}
+                inputName="link"
+                placeholder={t('link_edit.placeholder_of_link_input')}
+                keywordOnInit={this.state.linkInputValue}
+                autoFocus
+              />
+              <div className="d-none d-sm-block input-group-append">
+                <button type="button" id="preview-btn" className={`btn btn-info btn-page-preview ${styles['btn-page-preview']}`}>
+                  <PagePreviewIcon />
+                </button>
+                <Popover trigger="focus" placement="right" isOpen={this.state.isPreviewOpen} target="preview-btn" toggle={this.toggleIsPreviewOpen}>
+                  <PopoverBody>
+                    {this.state.markdown != null && pagePath != null
+                    && <div className={`linkedit-preview ${styles['linkedit-preview']}`}>
+                      <Preview markdown={this.state.markdown} pagePath={pagePath} />
+                    </div>
+                    }
+                  </PopoverBody>
+                </Popover>
+              </div>
+            </div>
+          </div>
+          <div className="form-gorup my-3">
+            <div className="input-group flex-nowrap">
+              <div className="input-group-prepend">
+                <span className="input-group-text">{t('link_edit.label')}</span>
+              </div>
+              <input
+                type="text"
+                className="form-control"
+                id="label"
+                value={this.state.labelInputValue}
+                onChange={e => this.handleChangeLabelInput(e.target.value)}
+                disabled={this.state.linkerType === Linker.types.growiLink}
+                placeholder={this.state.linkInputValue}
+              />
+            </div>
+          </div>
+        </form>
+      </>
+    );
+  }
+
+  // /**
+  //  * return a function to add prefix to selected each lines
+  //  *
+  //  * The cursor after editing is inserted between the end of the selection.
+  //  */
+  // createAddPrefixToEachLinesHandler(prefix) {
+  //   return () => {
+  //     const cm = this.getCodeMirror();
+  //     const startLineNum = cm.getCursor('from').line;
+  //     const endLineNum = cm.getCursor('to').line;
+
+  //     const lines = [];
+  //     for (let i = startLineNum; i <= endLineNum; i++) {
+  //       lines.push(prefix + cm.getDoc().getLine(i));
+  //     }
+  //     const replacement = `${lines.join('\n')}\n`;
+  //     cm.getDoc().replaceRange(replacement, { line: startLineNum, ch: 0 }, { line: endLineNum + 1, ch: 0 });
+
+  //     cm.setCursor(endLineNum, cm.getDoc().getLine(endLineNum).length);
+  //     cm.focus();
+  //   };
+  // }
+
+  element(template) {
+    return (
+      <div key={template.name} className="custom-control custom-radio">
+        <input
+          type="radio"
+          className="custom-control-input"
+          id="string"
+          value={template.value}
+          // checked={this.state.linkerType === template.value}
+          onChange={this.handleSelecteLinkerType(template.value)}
+        />
+        <label className="custom-control-label" htmlFor="string">
+          {template.name}
+        </label>
+      </div>
+    );
+  }
+
+  renderPathFormatForm() {
+    const { t } = this.props;
+    return (
+      <div className="card well pt-3">
+        {/* <form className="form-group mb-0">
+          <div className="form-group row mb-0"> */}
+        <label className="col-sm-3">Templates</label>
+        <div className="col-sm-9">
+          {/* <div key={templates[0].name} className="custom-control custom-radio custom-control-inline">
+            <input
+              type="radio"
+              className="custom-control-input"
+              id="markdownType"
+              value={templates[0].value}
+              // checked={this.state.linkerType === Linker.types.markdownLink}
+              onChange={e => this.handleSelecteLinkerType(e.target.value)}
+            />
+            <label className="custom-control-label" htmlFor="markdownType">
+              {templates[0].name}
+            </label>
+          </div>
+          <div key={templates[1].name} className="custom-control custom-radio custom-control-inline">
+            <input
+              type="radio"
+              className="custom-control-input"
+              id="markdownType"
+              value={templates[1].value}
+              // checked={this.state.linkerType === Linker.types.markdownLink}
+              onChange={e => this.handleSelecteLinkerType(e.target.value)}
+            />
+            <label className="custom-control-label" htmlFor="markdownType">
+              {templates[1].name}
+            </label>
+          </div> */}
+
+          { templates.map((template) => {
+            return (
+              this.element(template)
+            );
+          })}
+
+        </div>
+        {/* </div>
+        </form> */}
+      </div>
+    );
+  }
+
+  render() {
+    const { t } = this.props;
+    return (
+      <Modal className="link-edit-modal" isOpen={this.state.show} toggle={this.cancel} size="lg" autoFocus={false}>
+        <ModalHeader tag="h4" toggle={this.cancel} className="bg-primary text-light">
+          Template
+        </ModalHeader>
+
+        <ModalBody className="container">
+          <div className="row">
+            <div className="col-12">
+              {this.renderPathFormatForm()}
+            </div>
+          </div>
+        </ModalBody>
+        <ModalFooter>
+          <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={this.hide}>
+            {t('Cancel')}
+          </button>
+          <button type="submit" className="btn btn-sm btn-primary mx-1" onClick={this.save}>
+            {t('Done')}
+          </button>
+        </ModalFooter>
+      </Modal>
+    );
+  }
+
+}
+
+const TemplateModalFc = React.forwardRef((props, ref) => {
+  const { t } = useTranslation();
+  const { data: currentPath } = useCurrentPagePath();
+  return <TemplateModal t={t} ref={ref} pagePath={currentPath} {...props} />;
+});
+
+TemplateModal.propTypes = {
+  t: PropTypes.func.isRequired,
+  pagePath: PropTypes.string,
+  onSave: PropTypes.func,
+};
+
+
+export default TemplateModalFc;

+ 30 - 0
packages/app/src/components/TemplateTab.tsx

@@ -0,0 +1,30 @@
+import React from 'react';
+
+type Props = {
+  template: any,
+  onChangeHandler: any,
+}
+
+// const onChangeHandler = () => {
+
+// }
+
+export const TemplateTab = (props: Props): JSX.Element => {
+  const { template, onChangeHandler } = props;
+
+  return (
+    <div key={template.name} className="custom-control custom-radio">
+      <input
+        type="radio"
+        className="custom-control-input"
+        id="string"
+        value={template.value}
+        // checked={this.state.linkerType === template.value}
+        onChange={onChangeHandler}
+      />
+      <label className="custom-control-label" htmlFor="string">
+        {template.name}
+      </label>
+    </div>
+  );
+};

+ 2 - 2
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -107,7 +107,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
         nodeVersion={props.nodeVersion}
         nodeVersion={props.nodeVersion}
         npmVersion={props.npmVersion}
         npmVersion={props.npmVersion}
         yarnVersion={props.yarnVersion}
         yarnVersion={props.yarnVersion}
-        installedPlugins={props.installedPlugins}
+        // installedPlugins={props.installedPlugins}
       />,
       />,
     },
     },
     app: {
     app: {
@@ -284,7 +284,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   props.nodeVersion = crowi.runtimeVersions.versions.node ? crowi.runtimeVersions.versions.node.version.version : null;
   props.nodeVersion = crowi.runtimeVersions.versions.node ? crowi.runtimeVersions.versions.node.version.version : null;
   props.npmVersion = crowi.runtimeVersions.versions.npm ? crowi.runtimeVersions.versions.npm.version.version : null;
   props.npmVersion = crowi.runtimeVersions.versions.npm ? crowi.runtimeVersions.versions.npm.version.version : null;
   props.yarnVersion = crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version.version : null;
   props.yarnVersion = crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version.version : null;
-  props.installedPlugins = crowi.pluginService.listPlugins();
+  // props.installedPlugins = crowi.pluginService.listPlugins();
   props.envVars = await ConfigLoader.getEnvVarsForDisplay(true);
   props.envVars = await ConfigLoader.getEnvVarsForDisplay(true);
   props.isAclEnabled = aclService.isAclEnabled();
   props.isAclEnabled = aclService.isAclEnabled();