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

Merge pull request #934 from weseek/feat/list-drafts-in-localstorage

Feat/list drafts in localstorage
Yuki Takei 7 лет назад
Родитель
Сommit
a3fd0c2bb5

+ 6 - 3
resource/locales/en-US/translation.json

@@ -3,6 +3,8 @@
   "Edit": "Edit",
   "Delete": "Delete",
   "Duplicate": "Duplicate",
+  "Copy": "Copy",
+  "Click to copy": "Click to copy",
   "Move": "Move",
   "Moved": "Moved",
   "Unlinked": "Unlinked",
@@ -78,7 +80,7 @@
   "Hide": "Hide",
   "Disclose E-mail": "Disclose E-mail",
 
-
+  "page exists": "this page already exists",
 
   "Create today's": "Create today's ...",
   "Memo": "memo",
@@ -101,6 +103,7 @@
   "Load latest": "Load latest",
   "edited this page": "edited this page.",
 
+  "Resume Drafts": "Your Drafts",
   "Deleted Pages": "Deleted Pages",
   "Sign out": "Logout",
 
@@ -623,8 +626,8 @@
     "write_java": "You can write Javascript that is applied to whole system.",
     "attach_title_header": "Add h1 section when create new page automatically",
     "attach_title_header_desc": "Add page path to the first line as h1 section when create new page",
-    "recent_created_page_num": "Recent Created Paging num",
-    "recent_created_page_num_desc": "The number of pages to show in Recent Created Page List on the user's home"
+    "recent_created__n_draft_num_desc": "Number of Recently Created Pages & Drafts Displayed",
+    "recently_created_n_draft_num_desc": "Number of recently created pages and drafts displayed on user page"
   },
 
   "user_management": {

+ 6 - 4
resource/locales/ja/translation.json

@@ -3,6 +3,8 @@
   "Edit": "編集",
   "Delete": "削除",
   "Duplicate": "複製",
+  "Copy": "コピー",
+  "Click to copy": "クリックでコピー",
   "Move": "移動",
   "Moved": "移動しました",
   "Unlinked": "リダイレクト削除",
@@ -76,8 +78,7 @@
   "Hide": "非公開",
   "Disclose E-mail": "メールアドレスの公開",
 
-
-
+  "page exists": "このページはすでに存在しています",
 
   "Create today's": "今日の◯◯を作成",
   "Memo": "メモ",
@@ -120,6 +121,7 @@
   "Load latest": "最新版を読み込む",
   "edited this page": "さんがこのページを編集しました。",
 
+  "Resume Drafts": "下書きを続ける",
   "Deleted Pages": "削除済みページ",
   "Sign out": "ログアウト",
 
@@ -644,8 +646,8 @@
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
     "attach_title_header": "新規ページ作成時の h1 セクション自動挿入",
     "attach_title_header_desc": "新規作成したページの1行目に、ページのパスを h1 セクションとして挿入します。",
-    "recent_created_page_num": "Recent Created ページングサイズ",
-    "recent_created_page_num_desc": "ホーム画面の Recent Created で、1ページに表示する件数を設定します。"
+    "recent_created__n_draft_num_desc": "最近作成したページと下書きの表示数",
+    "recently_created_n_draft_num_desc": "ホーム画面の Recently Created での、1ページの表示数を設定します。"
   },
 
   "user_management": {

+ 20 - 0
src/client/js/app.js

@@ -38,6 +38,7 @@ import BookmarkButton from './components/BookmarkButton';
 import LikeButton from './components/LikeButton';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
+import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
 
 import CustomCssEditor from './components/Admin/CustomCssEditor';
@@ -403,6 +404,25 @@ if (recentCreatedControlsElem) {
   );
 }
 
+const myDraftControlsElem = document.getElementById('user-draft-list');
+if (myDraftControlsElem) {
+  let limit = crowi.getConfig().recentCreatedLimit;
+  if (limit == null) {
+    limit = 10;
+  }
+
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <MyDraftList
+        limit={limit}
+        crowi={crowi}
+        crowiOriginRenderer={crowiRenderer}
+      />
+    </I18nextProvider>,
+    myDraftControlsElem,
+  );
+}
+
 /*
  * HackMD Editor
  */

+ 240 - 0
src/client/js/components/MyDraftList/MyDraftList.jsx

@@ -0,0 +1,240 @@
+import React from 'react';
+
+import PropTypes from 'prop-types';
+import Pagination from 'react-bootstrap/lib/Pagination';
+import Draft from '../PageList/Draft';
+
+export default class MyDraftList extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      drafts: [],
+      currentDrafts: [],
+      activePage: 1,
+      paginationNumbers: {},
+    };
+
+    this.getDraftsFromLocalStorage = this.getDraftsFromLocalStorage.bind(this);
+    this.getCurrentDrafts = this.getCurrentDrafts.bind(this);
+    this.clearDraft = this.clearDraft.bind(this);
+    this.clearAllDrafts = this.clearAllDrafts.bind(this);
+    this.calculatePagination = this.calculatePagination.bind(this);
+  }
+
+  async componentWillMount() {
+    await this.getDraftsFromLocalStorage();
+    this.getCurrentDrafts(1);
+  }
+
+  async getDraftsFromLocalStorage() {
+    const draftsAsObj = JSON.parse(this.props.crowi.localStorage.getItem('draft') || '{}');
+
+    const res = await this.props.crowi.apiGet('/pages.exist', {
+      pages: draftsAsObj,
+    });
+
+    // {'/a': '#a', '/b': '#b'} => [{path: '/a', markdown: '#a'}, {path: '/b', markdown: '#b'}]
+    const drafts = Object.entries(draftsAsObj).map((d) => {
+      const path = d[0];
+      return {
+        path,
+        markdown: d[1],
+        isExist: res.pages[path],
+      };
+    });
+
+    this.setState({ drafts });
+  }
+
+  getCurrentDrafts(selectPageNumber) {
+    const limit = this.props.limit;
+    const totalCount = this.state.drafts.length;
+    const activePage = selectPageNumber;
+    const paginationNumbers = this.calculatePagination(limit, totalCount, activePage);
+
+    const currentDrafts = this.state.drafts.slice((activePage - 1) * limit, activePage * limit);
+
+    this.setState({
+      currentDrafts,
+      activePage,
+      paginationNumbers,
+    });
+  }
+
+  /**
+   * generate Elements of Draft
+   *
+   * @param {any} drafts Array of pages Model Obj
+   *
+   */
+  generateDraftList(drafts) {
+    return drafts.map((draft) => {
+      return (
+        <Draft
+          key={draft.path}
+          crowi={this.props.crowi}
+          crowiOriginRenderer={this.props.crowiOriginRenderer}
+          path={draft.path}
+          markdown={draft.markdown}
+          isExist={draft.isExist}
+          clearDraft={this.clearDraft}
+        />
+      );
+    });
+  }
+
+  clearDraft(path) {
+    this.props.crowi.clearDraft(path);
+
+    this.setState((prevState) => {
+      return {
+        drafts: prevState.drafts.filter((draft) => { return draft.path !== path }),
+        currentDrafts: prevState.drafts.filter((draft) => { return draft.path !== path }),
+      };
+    });
+  }
+
+  clearAllDrafts() {
+    this.props.crowi.clearAllDrafts();
+
+    this.setState({
+      drafts: [],
+      currentDrafts: [],
+      activePage: 1,
+      paginationNumbers: {},
+    });
+  }
+
+  calculatePagination(limit, totalCount, activePage) {
+    // calc totalPageNumber
+    const totalPage = Math.floor(totalCount / limit) + (totalCount % limit === 0 ? 0 : 1);
+
+    let paginationStart = activePage - 2;
+    let maxViewPageNum = activePage + 2;
+    // pagiNation Number area size = 5 , pageNuber calculate in here
+    // activePage Position calculate ex. 4 5 [6] 7 8 (Page8 over is Max), 3 4 5 [6] 7 (Page7 is Max)
+    if (paginationStart < 1) {
+      const diff = 1 - paginationStart;
+      paginationStart += diff;
+      maxViewPageNum = Math.min(totalPage, maxViewPageNum + diff);
+    }
+    if (maxViewPageNum > totalPage) {
+      const diff = maxViewPageNum - totalPage;
+      maxViewPageNum -= diff;
+      paginationStart = Math.max(1, paginationStart - diff);
+    }
+
+    return {
+      totalPage,
+      paginationStart,
+      maxViewPageNum,
+    };
+  }
+
+  /**
+   * generate Elements of Pagination First Prev
+   * ex.  <<   <   1  2  3  >  >>
+   * this function set << & <
+   */
+  generateFirstPrev(activePage) {
+    const paginationItems = [];
+    if (activePage !== 1) {
+      paginationItems.push(
+        <Pagination.First key="first" onClick={() => { return this.getCurrentDrafts(1) }} />,
+      );
+      paginationItems.push(
+        <Pagination.Prev key="prev" onClick={() => { return this.getCurrentDrafts(this.state.activePage - 1) }} />,
+      );
+    }
+    else {
+      paginationItems.push(
+        <Pagination.First key="first" disabled />,
+      );
+      paginationItems.push(
+        <Pagination.Prev key="prev" disabled />,
+      );
+
+    }
+    return paginationItems;
+  }
+
+  /**
+   * generate Elements of Pagination First Prev
+   *  ex. << < 4 5 6 7 8 > >>, << < 1 2 3 4 > >>
+   * this function set  numbers
+   */
+  generatePaginations(activePage, paginationStart, maxViewPageNum) {
+    const paginationItems = [];
+    for (let number = paginationStart; number <= maxViewPageNum; number++) {
+      paginationItems.push(
+        <Pagination.Item key={number} active={number === activePage} onClick={() => { return this.getCurrentDrafts(number) }}>{number}</Pagination.Item>,
+      );
+    }
+    return paginationItems;
+  }
+
+  /**
+   * generate Elements of Pagination First Prev
+   * ex.  <<   <   1  2  3  >  >>
+   * this function set > & >>
+   */
+  generateNextLast(activePage, totalPage) {
+    const paginationItems = [];
+    if (totalPage !== activePage) {
+      paginationItems.push(
+        <Pagination.Next key="next" onClick={() => { return this.getCurrentDrafts(this.state.activePage + 1) }} />,
+      );
+      paginationItems.push(
+        <Pagination.Last key="last" onClick={() => { return this.getCurrentDrafts(totalPage) }} />,
+      );
+    }
+    else {
+      paginationItems.push(
+        <Pagination.Next key="next" disabled />,
+      );
+      paginationItems.push(
+        <Pagination.Last key="last" disabled />,
+      );
+
+    }
+    return paginationItems;
+
+  }
+
+  render() {
+    const draftList = this.generateDraftList(this.state.currentDrafts);
+
+    const paginationItems = [];
+
+    const activePage = this.state.activePage;
+    const totalPage = this.state.paginationNumbers.totalPage;
+    const paginationStart = this.state.paginationNumbers.paginationStart;
+    const maxViewPageNum = this.state.paginationNumbers.maxViewPageNum;
+    const firstPrevItems = this.generateFirstPrev(activePage);
+    paginationItems.push(firstPrevItems);
+    const paginations = this.generatePaginations(activePage, paginationStart, maxViewPageNum);
+    paginationItems.push(paginations);
+    const nextLastItems = this.generateNextLast(activePage, totalPage);
+    paginationItems.push(nextLastItems);
+
+    return (
+      <div className="page-list-container-create">
+        <button type="button" className="btn-danger mb-3" onClick={this.clearAllDrafts}>Delete All</button>
+        <div className="tab-pane m-t-30 accordion" id="draft-list">
+          {draftList}
+        </div>
+        <Pagination bsSize="small">{paginationItems}</Pagination>
+      </div>
+    );
+  }
+
+}
+
+
+MyDraftList.propTypes = {
+  limit: PropTypes.number,
+  crowi: PropTypes.object.isRequired,
+  crowiOriginRenderer: PropTypes.object.isRequired,
+};

+ 23 - 3
src/client/js/components/PageEditor.js

@@ -38,6 +38,7 @@ export default class PageEditor extends React.Component {
     this.setCaretLine = this.setCaretLine.bind(this);
     this.focusToEditor = this.focusToEditor.bind(this);
     this.onMarkdownChanged = this.onMarkdownChanged.bind(this);
+    this.onSave = this.onSave.bind(this);
     this.onUpload = this.onUpload.bind(this);
     this.onEditorScroll = this.onEditorScroll.bind(this);
     this.onEditorScrollCursorIntoView = this.onEditorScrollCursorIntoView.bind(this);
@@ -45,6 +46,7 @@ export default class PageEditor extends React.Component {
     this.saveDraft = this.saveDraft.bind(this);
     this.clearDraft = this.clearDraft.bind(this);
     this.apiErrorHandler = this.apiErrorHandler.bind(this);
+    this.showUnsavedWarning = this.showUnsavedWarning.bind(this);
 
     // for scrolling
     this.lastScrolledDateWithCursor = null;
@@ -62,6 +64,20 @@ export default class PageEditor extends React.Component {
   componentWillMount() {
     // initial rendering
     this.renderPreview(this.state.markdown);
+
+    this.props.crowi.window.addEventListener('beforeunload', this.showUnsavedWarning);
+  }
+
+  componentWillUnmount() {
+    this.props.crowi.window.removeEventListener('beforeunload', this.showUnsavedWarning);
+  }
+
+  showUnsavedWarning(e) {
+    if (!this.props.crowi.getIsDocSaved()) {
+      // display browser default message
+      e.returnValue = '';
+      return '';
+    }
   }
 
   getMarkdown() {
@@ -111,6 +127,12 @@ export default class PageEditor extends React.Component {
   onMarkdownChanged(value) {
     this.renderPreviewWithDebounce(value);
     this.saveDraftWithDebounce();
+    this.props.crowi.setIsDocSaved(false);
+  }
+
+  onSave() {
+    this.props.onSaveWithShortcut(this.state.markdown);
+    this.props.crowi.setIsDocSaved(true);
   }
 
   /**
@@ -330,9 +352,7 @@ export default class PageEditor extends React.Component {
             onScrollCursorIntoView={this.onEditorScrollCursorIntoView}
             onChange={this.onMarkdownChanged}
             onUpload={this.onUpload}
-            onSave={() => {
-              this.props.onSaveWithShortcut(this.state.markdown);
-            }}
+            onSave={this.onSave}
           />
         </div>
         <div className="col-md-6 hidden-sm hidden-xs page-editor-preview-container">

+ 165 - 0
src/client/js/components/PageList/Draft.jsx

@@ -0,0 +1,165 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import GrowiRenderer from '../../util/GrowiRenderer';
+
+import RevisionBody from '../Page/RevisionBody';
+
+class Draft extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      html: '',
+      isOpen: false,
+    };
+
+    this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiOriginRenderer, { mode: 'draft' });
+
+    this.renderHtml = this.renderHtml.bind(this);
+    this.toggleContent = this.toggleContent.bind(this);
+    this.copyMarkdownToClipboard = this.copyMarkdownToClipboard.bind(this);
+    this.renderAccordionTitle = this.renderAccordionTitle.bind(this);
+  }
+
+  copyMarkdownToClipboard() {
+    navigator.clipboard.writeText(this.props.markdown);
+  }
+
+  async toggleContent(e) {
+    const target = e.currentTarget.getAttribute('data-target');
+
+    if (!this.state.html) {
+      await this.renderHtml();
+    }
+
+    if (this.state.isOpen) {
+      $(target).collapse('hide');
+      this.setState({ isOpen: false });
+    }
+    else {
+      $(target).collapse('show');
+      this.setState({ isOpen: true });
+    }
+  }
+
+  async renderHtml() {
+    const context = {
+      markdown: this.props.markdown,
+    };
+
+    const growiRenderer = this.growiRenderer;
+    const interceptorManager = this.props.crowi.interceptorManager;
+    await interceptorManager.process('prePreProcess', context)
+      .then(() => {
+        context.markdown = growiRenderer.preProcess(context.markdown);
+      })
+      .then(() => { return interceptorManager.process('postPreProcess', context) })
+      .then(() => {
+        const parsedHTML = growiRenderer.process(context.markdown);
+        context.parsedHTML = parsedHTML;
+      })
+      .then(() => { return interceptorManager.process('prePostProcess', context) })
+      .then(() => {
+        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+      })
+      .then(() => { return interceptorManager.process('postPostProcess', context) })
+      .then(() => {
+        this.setState({ html: context.parsedHTML });
+      });
+  }
+
+  renderAccordionTitle(isExist) {
+    if (isExist) {
+      return (
+        <Fragment>
+          <span>{this.props.path}</span>
+          <span className="mx-2">({this.props.t('page exists')})</span>
+        </Fragment>
+      );
+    }
+
+    return (
+      <Fragment>
+        <a href={`${this.props.path}#edit`} target="_blank" rel="noopener noreferrer">{this.props.path}</a>
+        <span className="mx-2">
+          <span className="label-draft label label-default">draft</span>
+        </span>
+      </Fragment>
+    );
+  }
+
+  render() {
+    const { t } = this.props;
+    const id = this.props.path.replace('/', '-');
+
+    return (
+      <div className="timeline-body">
+        <div className="panel panel-timeline">
+          <div className="panel-heading d-flex justify-content-between">
+            <div className="panel-title" onClick={this.toggleContent} data-target={`#${id}`}>
+              {this.renderAccordionTitle(this.props.isExist)}
+            </div>
+            <div>
+              {this.props.isExist
+                ? null
+                : (
+                  <a
+                    href={`${this.props.path}#edit`}
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    className="draft-edit"
+                    data-toggle="tooltip"
+                    data-placement="bottom"
+                    title={this.props.t('Edit')}
+                  >
+                    <i className="icon-note" />
+                  </a>
+                )
+              }
+              <a
+                className="draft-copy"
+                data-toggle="tooltip"
+                data-placement="bottom"
+                title={this.props.t('Copy')}
+                onClick={this.copyMarkdownToClipboard}
+              >
+                <i className="icon-doc" />
+              </a>
+              <a
+                className="text-danger draft-delete"
+                data-toggle="tooltip"
+                data-placement="top"
+                title={t('Delete')}
+                onClick={() => { return this.props.clearDraft(this.props.path) }}
+              >
+                <i className="icon-trash" />
+              </a>
+            </div>
+          </div>
+          <div className="panel-body collapse" id={id} aria-labelledby={id} data-parent="#draft-list">
+            <div className="revision-body wiki">
+              <RevisionBody html={this.state.html} />
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+Draft.propTypes = {
+  t: PropTypes.func.isRequired,
+  crowi: PropTypes.object.isRequired,
+  crowiOriginRenderer: PropTypes.object.isRequired,
+  path: PropTypes.string.isRequired,
+  markdown: PropTypes.string.isRequired,
+  isExist: PropTypes.bool.isRequired,
+  clearDraft: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(Draft);

+ 1 - 0
src/client/js/components/SavePageControls.jsx

@@ -47,6 +47,7 @@ class SavePageControls extends React.PureComponent {
   }
 
   submit() {
+    this.props.crowi.setIsDocSaved(true);
     this.props.onSubmit();
   }
 

+ 13 - 0
src/client/js/util/Crowi.js

@@ -30,6 +30,7 @@ export default class Crowi {
     this.socketClientId = Math.floor(Math.random() * 100000);
     this.page = undefined;
     this.pageEditor = undefined;
+    this.isDocSaved = true;
 
     this.fetchUsers = this.fetchUsers.bind(this);
     this.apiGet = this.apiGet.bind(this);
@@ -79,6 +80,14 @@ export default class Crowi {
     this.pageEditor = pageEditor;
   }
 
+  setIsDocSaved(isSaved) {
+    this.isDocSaved = isSaved;
+  }
+
+  getIsDocSaved() {
+    return this.isDocSaved;
+  }
+
   getWebSocket() {
     return this.socket;
   }
@@ -164,6 +173,10 @@ export default class Crowi {
     this.localStorage.setItem('draft', JSON.stringify(this.draft));
   }
 
+  clearAllDrafts() {
+    this.localStorage.removeItem('draft');
+  }
+
   saveDraft(path, body) {
     this.draft[path] = body;
     this.localStorage.setItem('draft', JSON.stringify(this.draft));

+ 32 - 4
src/client/styles/scss/_user.scss

@@ -24,17 +24,19 @@
 
       ul {
         padding-left: 0;
+
         li {
           list-style: none;
         }
       }
+
       .user-page-username {
         font-weight: bold;
       }
-      .user-page-email {
-      }
-      .user-page-introduction {
-      }
+
+      .user-page-email {}
+
+      .user-page-introduction {}
     }
 
     .btn-like,
@@ -62,4 +64,30 @@
       height: 48px;
     }
   }
+
+  .user-page-content {
+    #user-draft-list {
+      #draft-list {
+        .panel-title {
+          width: 100%;
+
+          .label-draft {
+            padding: 1px 5px;
+            margin: 0 0 0 4px;
+            font-weight: normal;
+          }
+        }
+
+        a {
+
+          .icon-copy,
+          .draft-delete,
+          .icon-edit {
+            margin: 0 0 0 4px;
+            cursor: pointer;
+          }
+        }
+      }
+    }
+  }
 }

+ 1 - 0
src/server/routes/index.js

@@ -193,6 +193,7 @@ module.exports = function(crowi, app) {
   app.post('/_api/pages.create'       , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.create);
   app.post('/_api/pages.update'       , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.update);
   app.get('/_api/pages.get'           , accessTokenParser , loginRequired(crowi, app, false) , page.api.get);
+  app.get('/_api/pages.exist'         , accessTokenParser , loginRequired(crowi, app, false) , page.api.exist);
   app.get('/_api/pages.updatePost', accessTokenParser, loginRequired(crowi, app, false), page.api.getUpdatePost);
   app.get('/_api/pages.getPageTag'    , accessTokenParser , loginRequired(crowi, app, false) , page.api.getPageTag);
   // allow posting to guests because the client doesn't know whether the user logged in

+ 23 - 0
src/server/routes/page.js

@@ -722,6 +722,29 @@ module.exports = function(crowi, app) {
     return res.json(ApiResponse.success(result));
   };
 
+  /**
+   * @api {get} /pages.exist Get if page exists
+   * @apiName GetPage
+   * @apiGroup Page
+   *
+   * @apiParam {String} pages (stringified JSON)
+   */
+  api.exist = async function(req, res) {
+    const pagesAsObj = JSON.parse(req.query.pages || '{}');
+    const pagePaths = Object.keys(pagesAsObj);
+
+    await Promise.all(pagePaths.map(async(path) => {
+      // check page existence
+      const isExist = await Page.count({ path }) > 0;
+      pagesAsObj[path] = isExist;
+      return;
+    }));
+
+    const result = { pages: pagesAsObj };
+
+    return res.json(ApiResponse.success(result));
+  };
+
   /**
    * @api {get} /pages.getPageTag get page tags
    * @apiName GetPageTag

+ 2 - 2
src/server/views/admin/customize.html

@@ -282,7 +282,7 @@
           </div>
 
           <div class="form-group">
-            <label for="settingForm[customize:showRecentCreatedNumber]" class="col-xs-3 control-label">{{ t("customize_page.recent_created_page_num") }}</label>
+            <label for="settingForm[customize:showRecentCreatedNumber]" class="col-xs-3 control-label">{{ t("customize_page.recent_created__n_draft_num_desc") }}</label>
             <div class="col-xs-5">
               <select class="form-control selectpicker" name="settingForm[customize:showRecentCreatedNumber]" value="{{ settingForm['customize:showRecentCreatedNumber'] }}">
                 <option value="10" {% if 10 == settingForm['customize:showRecentCreatedNumber'] %}selected{% endif %}>10</option>
@@ -291,7 +291,7 @@
               </select>
 
               <p class="help-block">
-                {{ t("customize_page.recent_created_page_num_desc") }}
+                {{ t("customize_page.recently_created_n_draft_num_desc") }}
               </p>
             </div>
           </div>

+ 2 - 1
src/server/views/layout/layout.html

@@ -189,7 +189,8 @@
             <li><a href="/user/{{ user.username }}"><i class="icon-fw icon-home"></i>{{ t('Home') }}</a></li>
             <li><a href="/me"><i class="icon-fw icon-wrench"></i>{{ t('User Settings') }}</a></li>
             <li role="separator" class="divider"></li>
-            <li><a href="/trash"><i class="icon-fw icon-trash"></i>{{ t('Deleted Pages') }}</a></li>
+            <li><a href="/user/{{ user.username }}#user-draft-list"><i class="icon-fw icon-docs"></i>{{ t('Resume Drafts') }}</a></li>
+            <li><a href="/user"><i class="icon-fw icon-trash"></i>{{ t('Deleted Pages') }}</a></li>
             <li role="separator" class="divider"></li>
             <li><a href="/logout"><i class="icon-fw icon-power"></i>{{ t('Sign out') }}</a></li>
           </ul>

+ 24 - 1
src/server/views/widget/user_page_content.html

@@ -4,7 +4,10 @@
       <a href="#user-bookmark-list" data-toggle="tab"><i class="icon-star"></i> Bookmarks</a>
     </li>
     <li>
-      <a href="#user-created-list" data-toggle="tab"><i class="icon-clock"></i> Recent Created</a>
+      <a href="#user-created-list" data-toggle="tab"><i class="icon-clock"></i> Recently Created</a>
+    </li>
+    <li>
+      <a href="#user-draft-list" data-toggle="tab"><i class="icon-docs"></i> My Drafts</a>
     </li>
     {% if user._id.toString() == pageUser._id.toString() %}
     <li>
@@ -29,5 +32,25 @@
       <div class="page-list-container">
       </div>
     </div>
+
+    <div class="tab-pane user-draft-list page-list" id="user-draft-list">
+      <div class="page-list-container">
+      </div>
+    </div>
   </div>
 </div>
+
+<script>
+  function activateTab(tab){
+    $('.nav-tabs a[href="#' + tab + '"]').tab('show');
+  };
+
+  window.addEventListener('load', function(e) {
+    // hash on page
+    if (location.hash) {
+      if (location.hash == '#user-draft-list') {
+        activateTab('user-draft-list');
+      }
+    }
+  });
+</script>