فهرست منبع

Merge pull request #2196 from weseek/support/reactify-create-page

Support/reactify create page
Yuki Takei 5 سال پیش
والد
کامیت
aa68087802

+ 1 - 1
resource/locales/en-US/translation.json

@@ -337,7 +337,7 @@
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "Create/Edit template page",
-      "Create template under": "Create template page under:<br /><code>%s</code>"
+      "Create template under": "Create template page under:"
     },
     "option_label": {
       "create/edit": "Create/Edit template page..",

+ 1 - 1
resource/locales/ja/translation.json

@@ -335,7 +335,7 @@
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
-      "Create template under": "<code>%s</code><br />にテンプレートページを作成"
+      "Create template under": "以下のパスにテンプレートページを作成"
     },
     "option_label": {
       "select": "テンプレートタイプを選択してください",

+ 0 - 2
src/client/js/app.jsx

@@ -68,8 +68,6 @@ Object.assign(componentMappings, {
   // 'revision-history': <PageHistory pageId={pageId} />,
   'tags-page': <TagsList crowi={appContainer} />,
 
-  'create-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} addTrailingSlash />,
-
   'page-editor': <PageEditor />,
   'page-editor-path-nav': <PagePathNavForEditor />,
   'page-editor-options-selector': <OptionsSelector crowi={appContainer} />,

+ 6 - 0
src/client/js/bootstrap.jsx

@@ -11,6 +11,8 @@ import StaffCredit from './components/StaffCredit/StaffCredit';
 
 import AppContainer from './services/AppContainer';
 import WebsocketContainer from './services/WebsocketContainer';
+import PageCreateButton from './components/Navbar/PageCreateButton';
+import PageCreateModal from './components/PageCreateModal';
 
 const logger = loggerFactory('growi:app');
 
@@ -44,6 +46,10 @@ const componentMappings = {
   'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
   'personal-dropdown': <PersonalDropdown />,
 
+  'create-page-button': <PageCreateButton />,
+  'create-page-button-icon': <PageCreateButton isIcon />,
+  'page-create-modal': <PageCreateModal />,
+
   'grw-sidebar-wrapper': <Sidebar />,
 
   'staff-credit': <StaffCredit />,

+ 43 - 0
src/client/js/components/Navbar/PageCreateButton.jsx

@@ -0,0 +1,43 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+
+const PageCreateButton = (props) => {
+  const { t, appContainer, isIcon } = props;
+
+  if (isIcon) {
+    return (
+      <button className="btn btn-lg btn-primary rounded-circle waves-effect waves-light" type="button" onClick={appContainer.openPageCreateModal}>
+        <i className="icon-pencil"></i>
+      </button>
+    );
+  }
+
+  return (
+    <a className="nav-link create-page" type="button" onClick={appContainer.openPageCreateModal}>
+      <i className="icon-pencil mr-2"></i>
+      <span>{ t('New') }</span>
+    </a>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageCreateButtonWrapper = (props) => {
+  return createSubscribedElement(PageCreateButton, props, [AppContainer]);
+};
+
+
+PageCreateButton.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  isIcon: PropTypes.bool,
+};
+
+export default withTranslation()(PageCreateButtonWrapper);

+ 229 - 0
src/client/js/components/PageCreateModal.jsx

@@ -0,0 +1,229 @@
+
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+
+import { withTranslation } from 'react-i18next';
+import { format } from 'date-fns';
+import urljoin from 'url-join';
+
+import { userPageRoot } from '@commons/util/path-utils';
+import { pathUtils } from 'growi-commons';
+import { createSubscribedElement } from './UnstatedUtils';
+
+import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
+import PagePathAutoComplete from './PagePathAutoComplete';
+
+const PageCreateModal = (props) => {
+  const { t, appContainer, pageContainer } = props;
+
+  const config = appContainer.getConfig();
+  const isReachable = config.isSearchServiceReachable;
+  const { path } = pageContainer.state;
+  const userPageRootPath = userPageRoot(appContainer.currentUser);
+  const parentPath = pathUtils.addTrailingSlash(path);
+  const now = format(new Date(), 'yyyy/MM/dd');
+
+  const [todayInput1, setTodayInput1] = useState(t('Memo'));
+  const [todayInput2, setTodayInput2] = useState('');
+  const [pageNameInput, setPageNameInput] = useState(parentPath);
+  const [template, setTemplate] = useState(null);
+
+  /**
+   * change todayInput1
+   * @param {string} value
+   */
+  function onChangeTodayInput1Handler(value) {
+    setTodayInput1(value);
+  }
+
+  /**
+   * change todayInput2
+   * @param {string} value
+   */
+  function onChangeTodayInput2Handler(value) {
+    setTodayInput2(value);
+  }
+
+  /**
+   * change pageNameInput
+   * @param {string} value
+   */
+  function onChangePageNameInputHandler(value) {
+    setPageNameInput(value);
+  }
+
+  /**
+   * change template
+   * @param {string} value
+   */
+  function onChangeTemplateHandler(value) {
+    setTemplate(value);
+  }
+
+  /**
+   * access today page
+   */
+  function createTodayPage() {
+    let tmpTodayInput1 = todayInput1;
+    if (tmpTodayInput1 === '') {
+      tmpTodayInput1 = t('Memo');
+    }
+    window.location.href = encodeURI(urljoin(userPageRootPath, tmpTodayInput1, now, todayInput2, '#edit'));
+  }
+
+  /**
+   * access input page
+   */
+  function createInputPage() {
+    window.location.href = encodeURI(urljoin(pageNameInput, '#edit'));
+  }
+
+  /**
+   * access template page
+   */
+  function createTemplatePage() {
+    const pageName = (template === 'children') ? '_template' : '__template';
+    window.location.href = encodeURI(urljoin(parentPath, pageName, '#edit'));
+  }
+
+  function renderCreateTodayForm() {
+    return (
+      <div className="row form-group">
+        <fieldset className="col-12 mb-4">
+          <h3 className="grw-modal-head pb-2">{ t("Create today's") }</h3>
+          <div className="d-flex">
+            <div className="create-page-input-row d-flex align-items-center">
+              <span>{userPageRootPath}/</span>
+              <input
+                type="text"
+                className="page-today-input1 form-control text-center"
+                value={todayInput1}
+                onChange={e => onChangeTodayInput1Handler(e.target.value)}
+              />
+              <span className="page-today-suffix">/{now}/</span>
+              <input
+                type="text"
+                className="page-today-input2 form-control"
+                id="page-today-input2"
+                placeholder={t('Input page name (optional)')}
+                value={todayInput2}
+                onChange={e => onChangeTodayInput2Handler(e.target.value)}
+              />
+            </div>
+            <div className="create-page-button-container">
+              <button type="button" className="btn btn-outline-primary rounded-pill" onClick={createTodayPage}>
+                <i className="icon-fw icon-doc"></i>{ t('Create') }
+              </button>
+            </div>
+          </div>
+        </fieldset>
+      </div>
+    );
+  }
+
+  function renderInputPageForm() {
+    return (
+      <div className="row form-group">
+        <fieldset className="col-12 mb-4">
+          <h3 className="grw-modal-head pb-2">{ t('Create under') }</h3>
+          <div className="d-flex create-page-input-container">
+            <div className="create-page-input-row d-flex align-items-center">
+              {isReachable
+                // GW-2355 refactor typeahead
+                ? <PagePathAutoComplete crowi={appContainer} initializedPath={path} addTrailingSlash />
+                : (
+                  <input
+                    type="text"
+                    value={pageNameInput}
+                    className="page-name-input form-control"
+                    placeholder={t('Input page name')}
+                    onChange={e => onChangePageNameInputHandler(e.target.value)}
+                    required
+                  />
+                )}
+            </div>
+            <div className="create-page-button-container">
+              <button type="submit" className="btn btn-outline-primary rounded-pill" onClick={createInputPage}>
+                <i className="icon-fw icon-doc"></i>{ t('Create') }
+              </button>
+            </div>
+          </div>
+        </fieldset>
+      </div>
+    );
+  }
+
+  function renderTemplatePageForm() {
+    return (
+      <div className="row form-group">
+        <fieldset className="col-12">
+          <h3 className="grw-modal-head pb-2">{ t('template.modal_label.Create template under')}<br />
+            <code>{path}</code>
+          </h3>
+          <div className="d-flex create-page-input-container">
+            <div className="create-page-input-row d-flex align-items-center">
+
+              <div id="dd-template-type" className="dropdown w-100">
+                <button id="template-type" type="button" className="btn btn-secondary btn dropdown-toggle" data-toggle="dropdown">
+                  {template == null && t('template.option_label.select') }
+                  {template === 'children' && t('template.children.label')}
+                  {template === 'decendants' && t('template.decendants.label')}
+                </button>
+                <div className="dropdown-menu" aria-labelledby="userMenu">
+                  <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('children')}>
+                    { t('template.children.label') } (_template)<br className="d-block d-md-none" />
+                    <small className="text-muted text-wrap">- { t('template.children.desc') }</small>
+                  </a>
+                  <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('decendants')}>
+                    { t('template.decendants.label') } (__template) <br className="d-block d-md-none" />
+                    <small className="text-muted">- { t('template.decendants.desc') }</small>
+                  </a>
+                </div>
+              </div>
+
+            </div>
+            <div className="create-page-button-container">
+              <button type="button" className={`btn btn-outline-primary rounded-pill ${template == null && 'disabled'}`} onClick={createTemplatePage}>
+                <i className="icon-fw icon-doc"></i>
+                <span>{ t('Edit') }</span>
+              </button>
+            </div>
+          </div>
+        </fieldset>
+      </div>
+    );
+  }
+  return (
+    <Modal size="lg" isOpen={appContainer.state.isPageCreateModalShown} toggle={appContainer.closePageCreateModal}>
+      <ModalHeader tag="h4" toggle={appContainer.closePageCreateModal} className="bg-primary text-light">
+        { t('New Page') }
+      </ModalHeader>
+      <ModalBody>
+        {renderCreateTodayForm}
+        {renderInputPageForm}
+        {renderTemplatePageForm}
+      </ModalBody>
+    </Modal>
+
+  );
+};
+
+
+/**
+ * Wrapper component for using unstated
+ */
+const ModalControlWrapper = (props) => {
+  return createSubscribedElement(PageCreateModal, props, [AppContainer, PageContainer]);
+};
+
+
+PageCreateModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(ModalControlWrapper);

+ 1 - 0
src/client/js/legacy/crowi.js

@@ -219,6 +219,7 @@ $(() => {
   });
 
 
+  // TODO GW-2355 remove this after refactoring
   $('#create-page').on('shown.bs.modal', (e) => {
     // quick hack: replace from server side rendering "date" to client side "date"
     const today = new Date();

+ 13 - 0
src/client/js/services/AppContainer.js

@@ -35,6 +35,8 @@ export default class AppContainer extends Container {
       preferDarkModeByUser: null,
       isDrawerOpened: false,
 
+      isPageCreateModalShown: false,
+
       recentlyUpdatedPages: [],
     };
 
@@ -93,6 +95,9 @@ export default class AppContainer extends Container {
       put: this.apiv3Put.bind(this),
       delete: this.apiv3Delete.bind(this),
     };
+
+    this.openPageCreateModal = this.openPageCreateModal.bind(this);
+    this.closePageCreateModal = this.closePageCreateModal.bind(this);
   }
 
   /**
@@ -465,4 +470,12 @@ export default class AppContainer extends Container {
     return this.apiv3Request('delete', path, { params });
   }
 
+  openPageCreateModal() {
+    this.setState({ isPageCreateModalShown: true });
+  }
+
+  closePageCreateModal() {
+    this.setState({ isPageCreateModalShown: false });
+  }
+
 }

+ 3 - 9
src/server/views/layout/layout.html

@@ -107,12 +107,7 @@
       {% endif %}
 
       {% if user %}
-      <li class="nav-item d-none d-md-block">
-        <a class="nav-link create-page" href="#" data-target="#create-page" data-toggle="modal">
-          <i class="icon-pencil mr-2"></i>
-          <span>{{ t('New') }}</span>
-        </a>
-      </li>
+      <li id="create-page-button" class="nav-item d-none d-md-block"></li>
       <li class="nav-item d-none d-md-block">
         <a class="nav-link" href="https://docs.growi.org/" target="_blank">
           <i class="icon-question mr-2"></i><span class="mr-2">{{ t('Help') }}</span><span class="small"><i class="icon-share-alt"></i></span>
@@ -148,15 +143,14 @@
 
 <div class="grw-fixed-controls-container d-md-none d-edit-none animated fadeInUp faster">
   <div class="grw-fixed-controls-button-container rounded-circle">
-    <button class="btn btn-lg btn-primary rounded-circle waves-effect waves-light" type="button" data-target="#create-page" data-toggle="modal">
-      <i class="icon-pencil"></i>
-    </button>
+    <div id='create-page-button-icon'></div>
   </div>
 </div>
 
 <!-- /#staff-credit -->
 <div id="staff-credit"></div>
 
+<div id="page-create-modal"></div>
 {% include '../modal/shortcuts.html' %}
 
 {% block body_end %}

+ 1 - 0
src/server/views/modal/create_page.html

@@ -1,3 +1,4 @@
+<!-- TODO GW-2362 remove after adjust layout -->
 <div class="modal create-page" id="create-page">
   <div class="modal-dialog modal-lg">
     <div class="modal-content">