Просмотр исходного кода

Merge pull request #15 from weseek/support/create-template-dropdown

feat: TemplateModal
Yuki Takei 3 лет назад
Родитель
Сommit
32589ef146

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

@@ -39,6 +39,7 @@ const GridEditModal = dynamic(() => import('./PageEditor/GridEditModal'), { ssr:
 // const HandsontableModal = dynamic(() => import('./PageEditor/HandsontableModal'), { ssr: false });
 const LinkEditModal = dynamic(() => import('./PageEditor/LinkEditModal'), { ssr: false });
 
+
 const logger = loggerFactory('growi:Page');
 
 type PageSubstanceProps = {

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

@@ -14,6 +14,7 @@ import InterceptorManager from '~/services/interceptor-manager';
 import { useDrawioModal } from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 
+import { TemplateModal } from '../TemplateModal';
 import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 
 import AbstractEditor from './AbstractEditor';
@@ -110,6 +111,7 @@ class CodeMirrorEditor extends AbstractEditor {
       emojiSearchText: '',
       startPosWithEmojiPickerModeTurnedOn: null,
       isEmojiPickerMode: false,
+      isTemplateModalOpened: false,
     };
 
     this.cm = React.createRef();
@@ -159,6 +161,8 @@ class CodeMirrorEditor extends AbstractEditor {
     this.foldDrawioSection = this.foldDrawioSection.bind(this);
     this.onSaveForDrawio = this.onSaveForDrawio.bind(this);
 
+    this.showTemplateModal = this.showTemplateModal.bind(this);
+
   }
 
   init() {
@@ -870,6 +874,9 @@ class CodeMirrorEditor extends AbstractEditor {
     // this.handsontableModal.current.show(mtu.getMarkdownTable(this.getCodeMirror()));
   }
 
+  showTemplateModal() {
+    this.setState({ isTemplateModalOpened: true });
+  }
 
   // fold draw.io section (::: drawio ~ :::)
   foldDrawioSection() {
@@ -1034,6 +1041,15 @@ class CodeMirrorEditor extends AbstractEditor {
       >
         <EditorIcon icon="Emoji" />
       </Button>,
+      <Button
+        key="nav-item-template"
+        color={null}
+        bssize="small"
+        title="Template"
+        onClick={() => this.showTemplateModal()}
+      >
+        <EditorIcon icon="Template" />
+      </Button>,
     ];
   }
 
@@ -1127,6 +1143,11 @@ class CodeMirrorEditor extends AbstractEditor {
           ref={this.linkEditModal}
           onSave={(linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
         />
+        <TemplateModal
+          isOpen={this.state.isTemplateModalOpened}
+          onClose={() => this.setState({ isTemplateModalOpened: false })}
+          onSubmit={templateText => this.setValue(templateText) }
+        />
         {/* <HandsontableModal
           ref={this.handsontableModal}
           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 */
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
 const EditorIcon = (props) => {
@@ -139,6 +140,14 @@ const EditorIcon = (props) => {
           </g>
         </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>
+      );
   }
 
 

+ 155 - 0
packages/app/src/components/TemplateModal.tsx

@@ -0,0 +1,155 @@
+import React, { useCallback, useState } from 'react';
+
+import { useTranslation } from 'next-i18next';
+import {
+  Modal,
+  ModalHeader,
+  ModalBody,
+  ModalFooter,
+} from 'reactstrap';
+
+import { usePreviewOptions } from '~/stores/renderer';
+
+import Preview from './PageEditor/Preview';
+
+
+type ITemplate = {
+  name: string,
+  markdown: string,
+}
+
+const templates: ITemplate[] = [
+  {
+    name: 'WESEEK Inner Wiki Style',
+    markdown: `# 関連ページ
+
+$lsx()
+
+# `,
+  },
+  {
+    name: 'Qiita Style',
+    markdown: `# <会議体名>
+## 日時
+yyyy/mm/dd hh:mm〜hh:mm
+
+## 場所
+
+## 出席者
+-
+
+## 議題
+1. [議題1](#link)
+2.
+3.
+
+## 議事内容
+### <a name="link"></a>議題1
+
+## 決定事項
+- 決定事項1
+
+## アクション事項
+- [ ] アクション
+
+## 次回
+yyyy/mm/dd (予定、時間は追って連絡)`,
+  },
+];
+
+
+type TemplateRadioButtonProps = {
+  template: ITemplate,
+  onChange: (selectedTemplate: ITemplate) => void,
+  isSelected?: boolean,
+}
+
+const TemplateRadioButton = ({ template, onChange, isSelected }: TemplateRadioButtonProps): JSX.Element => {
+  const radioButtonId = `rb-${template.name}`;
+
+  return (
+    <div key={template.name} 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>
+  );
+};
+
+
+type Props = {
+  isOpen: boolean,
+  onClose: () => void,
+  onSubmit?: (markdown: string) => void,
+}
+
+export const TemplateModal = (props: Props): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { isOpen, onClose, onSubmit } = props;
+
+  const { data: rendererOptions } = usePreviewOptions();
+
+  const [selectedTemplate, setSelectedTemplate] = useState<ITemplate>();
+
+  const submitHandler = useCallback((template?: ITemplate) => {
+    if (onSubmit == null || template == null) {
+      onClose();
+      return;
+    }
+
+    onSubmit(template.markdown);
+    onClose();
+  }, [onClose, onSubmit]);
+
+  return (
+    <Modal className="link-edit-modal" isOpen={isOpen} toggle={onClose} size="lg" autoFocus={false}>
+      <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light">
+        Template
+      </ModalHeader>
+
+      <ModalBody className="container">
+        <div className="row">
+          <div className="col-12">
+            { templates.map(template => (
+              <TemplateRadioButton
+                key={template.name}
+                template={template}
+                onChange={t => setSelectedTemplate(t)}
+                isSelected={template.name === selectedTemplate?.name}
+              />
+            )) }
+          </div>
+        </div>
+
+        { rendererOptions != null && (
+          <>
+            <hr />
+            <h3>Preview</h3>
+            <div className='card'>
+              <div className="card-body" style={{ maxHeight: '60vh', overflowY: 'auto' }}>
+                <Preview rendererOptions={rendererOptions} markdown={selectedTemplate?.markdown}/>
+              </div>
+            </div>
+          </>
+        ) }
+
+      </ModalBody>
+      <ModalFooter>
+        <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={onClose}>
+          {t('Cancel')}
+        </button>
+        <button type="submit" className="btn btn-sm btn-primary mx-1" onClick={() => submitHandler(selectedTemplate)} disabled={selectedTemplate == null}>
+          {t('Update')}
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};

+ 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>
+  );
+};

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

@@ -290,7 +290,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   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.yarnVersion = crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version.version : null;
-  // props.installedPlugins = crowi.pluginService.listPlugins(crowi.rootDir);
+  // props.installedPlugins = crowi.pluginService.listPlugins();
   props.envVars = await ConfigLoader.getEnvVarsForDisplay(true);
   props.isAclEnabled = aclService.isAclEnabled();