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

Merge pull request #8202 from weseek/feat/132682-create-todays-page

feat: Create today's and template page
Yuki Takei 2 лет назад
Родитель
Сommit
0514bc0e9d

+ 13 - 0
apps/app/public/static/locales/en_US/commons.json

@@ -68,6 +68,19 @@
     "feedback": "Feedback"
   },
 
+  "create_page_dropdown": {
+    "new_page": "Create New Page",
+    "todays": {
+      "desc": "Create today's ...",
+      "memo": "memo"
+    },
+    "template": {
+      "desc": "Create/Edit template page",
+      "children": "Template for children",
+      "descendants": "Template for descendants"
+    }
+  },
+
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",

+ 0 - 2
apps/app/public/static/locales/en_US/translation.json

@@ -106,8 +106,6 @@
   "Disclose E-mail": "Disclose E-mail",
   "page exists": "this page already exists",
   "Error occurred": "Error occurred",
-  "Create today's": "Create today's ...",
-  "Memo": "memo",
   "Input page name": "Input page name",
   "Input page name (optional)": "Input page name (optional)",
   "New Page": "New page",

+ 13 - 0
apps/app/public/static/locales/ja_JP/commons.json

@@ -70,6 +70,19 @@
     "feedback": "ご意見・ご要望"
   },
 
+  "create_page_dropdown": {
+    "new_page": "新規ページ作成",
+    "todays": {
+      "desc": "今日の◯◯を作成",
+      "memo": "メモ"
+    },
+    "template": {
+      "desc": "テンプレートページの作成/編集",
+      "children": "同一階層テンプレート",
+      "decendants": "下位層テンプレート"
+    }
+  },
+
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",

+ 0 - 2
apps/app/public/static/locales/ja_JP/translation.json

@@ -105,8 +105,6 @@
   "Disclose E-mail": "メールアドレスの公開",
   "page exists": "このページはすでに存在しています",
   "Error occurred": "エラーが発生しました",
-  "Create today's": "今日の◯◯を作成",
-  "Memo": "メモ",
   "Input page name": "ページ名を入力",
   "Input page name (optional)": "ページ名を入力(空欄OK)",
   "New Page": "新規ページ",

+ 13 - 0
apps/app/public/static/locales/zh_CN/commons.json

@@ -71,6 +71,19 @@
     "feedback": "意见和要求"
   },
 
+  "create_page_dropdown": {
+    "new_page": "新页面",
+    "todays": {
+      "desc": "Create today's ...",
+      "memo": "memo"
+    },
+    "template": {
+      "desc": "创建/编辑模板页",
+      "children": "子模板",
+      "descendants": "子代模板"
+    }
+  },
+
 	"copy_to_clipboard": {
 		"Copy to clipboard": "复制到剪贴板",
 		"Page path": "页面路径",

+ 0 - 2
apps/app/public/static/locales/zh_CN/translation.json

@@ -111,8 +111,6 @@
 	"Disclose E-mail": "显示邮箱",
 	"page exists": "页面已存在",
 	"Error occurred": "Error occurred",
-	"Create today's": "Create today's ...",
-	"Memo": "memo",
 	"Input page name": "Input page name",
 	"Input page name (optional)": "Input page name (optional)",
 	"New Page": "新页面",

+ 17 - 1
apps/app/src/client/services/page-operation.ts

@@ -9,7 +9,7 @@ import { useCurrentPageId, useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stor
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 
-import { apiPost } from '../util/apiv1-client';
+import { apiGet, apiPost } from '../util/apiv1-client';
 import { apiv3Post, apiv3Put } from '../util/apiv3-client';
 import { toastError } from '../util/toastr';
 
@@ -203,3 +203,19 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
 export const unlink = async(path: string): Promise<void> => {
   await apiPost('/pages.unlink', { path });
 };
+
+
+interface PageExistRequest {
+  pagePaths: string;
+}
+
+interface PageExistResponse {
+  pages: Record<string, boolean>;
+  ok: boolean
+}
+
+export const exist = async(pagePaths: string): Promise<PageExistResponse> => {
+  const request: PageExistRequest = { pagePaths };
+  const res = await apiGet('/pages.exist', request);
+  return res as PageExistResponse;
+};

+ 0 - 147
apps/app/src/components/Sidebar/PageCreateButton.tsx

@@ -1,147 +0,0 @@
-import React, { useCallback, useState } from 'react';
-
-import { useRouter } from 'next/router';
-
-import { createPage } from '~/client/services/page-operation';
-import { toastError } from '~/client/util/toastr';
-import { useSWRxCurrentPage } from '~/stores/page';
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:cli:PageCreateButton');
-
-export const PageCreateButton = React.memo((): JSX.Element => {
-  const router = useRouter();
-  const { data: currentPage, isLoading } = useSWRxCurrentPage();
-
-  const [isHovered, setIsHovered] = useState(false);
-  const [isCreating, setIsCreating] = useState(false);
-
-  const onMouseEnterHandler = () => {
-    setIsHovered(true);
-  };
-
-  const onMouseLeaveHandler = () => {
-    setIsHovered(false);
-  };
-
-  const onCreateNewPageButtonHandler = useCallback(async() => {
-    if (isLoading) return;
-
-    try {
-      setIsCreating(true);
-
-      const parentPath = currentPage == null
-        ? '/'
-        : currentPage.path;
-
-      const params = {
-        isSlackEnabled: false,
-        slackChannels: '',
-        grant: currentPage?.grant || 1,
-        pageTags: [],
-        grantUserGroupId: currentPage?.grantedGroup?._id,
-        shouldGeneratePath: true,
-      };
-
-      const response = await createPage(parentPath, '', params);
-
-      router.push(`${response.page.id}#edit`);
-    }
-    catch (err) {
-      logger.warn(err);
-      toastError(err);
-    }
-    finally {
-      setIsCreating(false);
-    }
-  }, [currentPage, isLoading, router]);
-  const onCreateTodaysButtonHandler = useCallback(() => {
-    // router.push(`${router.pathname}#edit`);
-  }, [router]);
-  const onTemplateForChildrenButtonHandler = useCallback(() => {
-    // router.push(`${router.pathname}/_template#edit`);
-  }, [router]);
-  const onTemplateForDescendantsButtonHandler = useCallback(() => {
-    // router.push(`${router.pathname}/__template#edit`);
-  }, [router]);
-
-  // TODO: update button design
-  // https://redmine.weseek.co.jp/issues/132683
-  // TODO: i18n
-  // https://redmine.weseek.co.jp/issues/132681
-  return (
-    <div
-      className="d-flex flex-row"
-      onMouseEnter={onMouseEnterHandler}
-      onMouseLeave={onMouseLeaveHandler}
-    >
-      <div className="btn-group">
-        <button
-          className="d-block btn btn-primary"
-          onClick={onCreateNewPageButtonHandler}
-          type="button"
-          data-testid="grw-sidebar-nav-page-create-button"
-          disabled={isCreating}
-        >
-          <i className="material-symbols-outlined">edit</i>
-        </button>
-      </div>
-      {isHovered && (
-        <div className="btn-group dropend">
-          <button
-            className="btn btn-secondary dropdown-toggle dropdown-toggle-split position-absolute"
-            type="button"
-            data-bs-toggle="dropdown"
-            aria-expanded="false"
-          />
-          <ul className="dropdown-menu">
-            <li>
-              <button
-                className="dropdown-item"
-                onClick={onCreateNewPageButtonHandler}
-                type="button"
-                disabled={isCreating}
-              >
-                Create New Page
-              </button>
-            </li>
-            <li><hr className="dropdown-divider" /></li>
-            <li><span className="text-muted px-3">Create today&apos;s ...</span></li>
-            {/* TODO: show correct create today's page path */}
-            {/* https://redmine.weseek.co.jp/issues/132682 */}
-            <li>
-              <button
-                className="dropdown-item"
-                onClick={onCreateTodaysButtonHandler}
-                type="button"
-              >
-                Create today&apos;s
-              </button>
-            </li>
-            <li><hr className="dropdown-divider" /></li>
-            <li><span className="text-muted px-3">Child page template</span></li>
-            <li>
-              <button
-                className="dropdown-item"
-                onClick={onTemplateForChildrenButtonHandler}
-                type="button"
-              >
-                Template for children
-              </button>
-            </li>
-            <li>
-              <button
-                className="dropdown-item"
-                onClick={onTemplateForDescendantsButtonHandler}
-                type="button"
-              >
-                Template for descendants
-              </button>
-            </li>
-          </ul>
-        </div>
-      )}
-    </div>
-  );
-});
-PageCreateButton.displayName = 'PageCreateButton';

+ 210 - 0
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx

@@ -0,0 +1,210 @@
+import React, { useCallback, useState } from 'react';
+
+import { pagePathUtils } from '@growi/core/dist/utils';
+import { format } from 'date-fns';
+import { useRouter } from 'next/router';
+
+import { createPage, exist } from '~/client/services/page-operation';
+import { toastError } from '~/client/util/toastr';
+import { useCurrentUser } from '~/stores/context';
+import { useSWRxCurrentPage } from '~/stores/page';
+import loggerFactory from '~/utils/logger';
+
+import { PageCreateButtonDropdownMenu } from './PageCreateButtonDropdownMenu';
+
+const logger = loggerFactory('growi:cli:PageCreateButton');
+
+export const PageCreateButton = React.memo((): JSX.Element => {
+  const router = useRouter();
+  const { data: currentPage, isLoading } = useSWRxCurrentPage();
+  const { data: currentUser } = useCurrentUser();
+
+  const [isHovered, setIsHovered] = useState(false);
+  const [isCreating, setIsCreating] = useState(false);
+
+  const now = format(new Date(), 'yyyy/MM/dd');
+  const userHomepagePath = pagePathUtils.userHomepagePath(currentUser);
+  const todaysPath = `${userHomepagePath}/memo/${now}`;
+
+  const onMouseEnterHandler = () => {
+    setIsHovered(true);
+  };
+
+  const onMouseLeaveHandler = () => {
+    setIsHovered(false);
+  };
+
+  const onClickCreateNewPageButtonHandler = useCallback(async() => {
+    if (isLoading) return;
+
+    try {
+      setIsCreating(true);
+
+      const parentPath = currentPage == null
+        ? '/'
+        : currentPage.path;
+
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: currentPage?.grant || 1,
+        pageTags: [],
+        grantUserGroupId: currentPage?.grantedGroup?._id,
+        shouldGeneratePath: true,
+      };
+
+      const response = await createPage(parentPath, '', params);
+
+      router.push(`${response.page.id}#edit`);
+    }
+    catch (err) {
+      logger.warn(err);
+      toastError(err);
+    }
+    finally {
+      setIsCreating(false);
+    }
+  }, [currentPage, isLoading, router]);
+
+  const onClickCreateTodaysButtonHandler = useCallback(async() => {
+    if (currentUser == null) {
+      return;
+    }
+
+    try {
+      setIsCreating(true);
+
+      // TODO: get grant, grantUserGroupId data from parent page
+      // https://redmine.weseek.co.jp/issues/133892
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: 1,
+        pageTags: [],
+      };
+
+      const res = await exist(JSON.stringify([todaysPath]));
+      if (!res.pages[todaysPath]) {
+        await createPage(todaysPath, '', params);
+      }
+
+      router.push(`${todaysPath}#edit`);
+    }
+    catch (err) {
+      logger.warn(err);
+      toastError(err);
+    }
+    finally {
+      setIsCreating(false);
+    }
+  }, [currentUser, router, todaysPath]);
+
+  const onClickTemplateForChildrenButtonHandler = useCallback(async() => {
+    if (isLoading) return;
+
+    try {
+      setIsCreating(true);
+
+      const path = currentPage == null || currentPage.path === '/'
+        ? '/_template'
+        : `${currentPage.path}/_template`;
+
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: currentPage?.grant || 1,
+        pageTags: [],
+        grantUserGroupId: currentPage?.grantedGroup?._id,
+      };
+
+      const res = await exist(JSON.stringify([path]));
+      if (!res.pages[path]) {
+        await createPage(path, '', params);
+      }
+
+      router.push(`${path}#edit`);
+    }
+    catch (err) {
+      logger.warn(err);
+      toastError(err);
+    }
+    finally {
+      setIsCreating(false);
+    }
+  }, [currentPage, isLoading, router]);
+
+  const onClickTemplateForDescendantsButtonHandler = useCallback(async() => {
+    if (isLoading) return;
+
+    try {
+      setIsCreating(true);
+
+      const path = currentPage == null || currentPage.path === '/'
+        ? '/__template'
+        : `${currentPage.path}/__template`;
+
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: currentPage?.grant || 1,
+        pageTags: [],
+        grantUserGroupId: currentPage?.grantedGroup?._id,
+      };
+
+      const res = await exist(JSON.stringify([path]));
+      if (!res.pages[path]) {
+        await createPage(path, '', params);
+      }
+
+      router.push(`${path}#edit`);
+    }
+    catch (err) {
+      logger.warn(err);
+      toastError(err);
+    }
+    finally {
+      setIsCreating(false);
+    }
+  }, [currentPage, isLoading, router]);
+
+  // TODO: update button design
+  // https://redmine.weseek.co.jp/issues/132683
+  return (
+    <div
+      className="d-flex flex-row"
+      onMouseEnter={onMouseEnterHandler}
+      onMouseLeave={onMouseLeaveHandler}
+    >
+      <div className="btn-group">
+        <button
+          className="d-block btn btn-primary"
+          onClick={onClickCreateNewPageButtonHandler}
+          type="button"
+          data-testid="grw-sidebar-nav-page-create-button"
+          disabled={isCreating}
+        >
+          <i className="material-symbols-outlined">edit</i>
+        </button>
+      </div>
+      {isHovered && (
+        <div className="btn-group dropend">
+          <button
+            className="btn btn-secondary dropdown-toggle dropdown-toggle-split position-absolute"
+            type="button"
+            data-bs-toggle="dropdown"
+            aria-expanded="false"
+            disabled={isCreating}
+          />
+          <PageCreateButtonDropdownMenu
+            todaysPath={todaysPath}
+            onClickCreateNewPageButtonHandler={onClickCreateNewPageButtonHandler}
+            onClickCreateTodaysButtonHandler={onClickCreateTodaysButtonHandler}
+            onClickTemplateForChildrenButtonHandler={onClickTemplateForChildrenButtonHandler}
+            onClickTemplateForDescendantsButtonHandler={onClickTemplateForDescendantsButtonHandler}
+          />
+        </div>
+      )}
+    </div>
+  );
+});
+PageCreateButton.displayName = 'PageCreateButton';

+ 69 - 0
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButtonDropdownMenu.tsx

@@ -0,0 +1,69 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+type PageCreateButtonDropdownMenuProps = {
+  todaysPath: string,
+  onClickCreateNewPageButtonHandler: () => Promise<void>
+  onClickCreateTodaysButtonHandler: () => Promise<void>
+  onClickTemplateForChildrenButtonHandler: () => Promise<void>
+  onClickTemplateForDescendantsButtonHandler: () => Promise<void>
+}
+
+export const PageCreateButtonDropdownMenu = React.memo((props: PageCreateButtonDropdownMenuProps): JSX.Element => {
+  const {
+    todaysPath,
+    onClickCreateNewPageButtonHandler,
+    onClickCreateTodaysButtonHandler,
+    onClickTemplateForChildrenButtonHandler,
+    onClickTemplateForDescendantsButtonHandler,
+  } = props;
+
+  const { t } = useTranslation('commons');
+
+  return (
+    <ul className="dropdown-menu">
+      <li>
+        <button
+          className="dropdown-item"
+          onClick={onClickCreateNewPageButtonHandler}
+          type="button"
+        >
+          {t('create_page_dropdown.new_page')}
+        </button>
+      </li>
+      <li><hr className="dropdown-divider" /></li>
+      <li><span className="text-muted px-3">{t('create_page_dropdown.todays.desc')}</span></li>
+      <li>
+        <button
+          className="dropdown-item"
+          onClick={onClickCreateTodaysButtonHandler}
+          type="button"
+        >
+          {todaysPath}
+        </button>
+      </li>
+      <li><hr className="dropdown-divider" /></li>
+      <li><span className="text-muted text-nowrap px-3">{t('create_page_dropdown.template.desc')}</span></li>
+      <li>
+        <button
+          className="dropdown-item"
+          onClick={onClickTemplateForChildrenButtonHandler}
+          type="button"
+        >
+          {t('create_page_dropdown.template.children')}
+        </button>
+      </li>
+      <li>
+        <button
+          className="dropdown-item"
+          onClick={onClickTemplateForDescendantsButtonHandler}
+          type="button"
+        >
+          {t('create_page_dropdown.template.decendants')}
+        </button>
+      </li>
+    </ul>
+  );
+});
+PageCreateButtonDropdownMenu.displayName = 'PageCreateButtonDropdownMenu';

+ 1 - 0
apps/app/src/components/Sidebar/PageCreateButton/index.ts

@@ -0,0 +1 @@
+export * from './PageCreateButton';

+ 2 - 4
apps/app/src/server/routes/apiv3/pages.js

@@ -240,11 +240,9 @@ module.exports = (crowi) => {
   }
 
   async function generateUniquePath(basePath, index = 1) {
-    const Page = mongoose.model('Page');
     const path = basePath + index;
-    const response = await Page.findByPath(path);
-    const isPathExists = response != null;
-    if (isPathExists) {
+    const existingPageId = await Page.exists({ path, isEmpty: false });
+    if (existingPageId != null) {
       return generateUniquePath(basePath, index + 1);
     }
     return path;