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

Merge branch 'master' into imprv/show-unrelated-groups-and-disable-ungrantable-groups-in-grant-select

Futa Arai 2 лет назад
Родитель
Сommit
242ad2af00
36 измененных файлов с 193 добавлено и 193 удалено
  1. 2 2
      .github/workflows/ci-app-prod.yml
  2. 2 2
      .github/workflows/reusable-app-prod.yml
  3. 3 2
      .github/workflows/reusable-app-reg-suit.yml
  4. 8 2
      apps/app/public/static/locales/en_US/translation.json
  5. 8 2
      apps/app/public/static/locales/ja_JP/translation.json
  6. 8 2
      apps/app/public/static/locales/zh_CN/translation.json
  7. 4 1
      apps/app/src/components/PageComment.tsx
  8. 2 2
      apps/app/src/components/PageComment/CommentEditor.tsx
  9. 9 5
      apps/app/src/components/PageComment/DeleteCommentModal.tsx
  10. 1 1
      apps/app/src/components/PageComment/SwitchingButtonGroup.module.scss
  11. 2 2
      apps/app/src/components/PageComment/SwitchingButtonGroup.tsx
  12. 2 0
      apps/app/src/components/PageEditor/PageEditor.tsx
  13. 1 0
      apps/app/src/components/PageEditor/Preview.tsx
  14. 6 0
      apps/app/src/components/PageTags/TagsInput.tsx
  15. 10 33
      apps/app/src/components/SavePageControls.tsx
  16. 1 1
      apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx
  17. 0 1
      apps/app/src/components/Sidebar/PageCreateButton/CreateButton.tsx
  18. 1 0
      apps/app/src/components/Sidebar/PageCreateButton/DropendMenu.tsx
  19. 1 0
      apps/app/src/components/Sidebar/PageCreateButton/DropendToggle.tsx
  20. 1 0
      apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx
  21. 6 7
      apps/app/src/components/Sidebar/Tag.tsx
  22. 1 1
      apps/app/src/components/SlackNotification.tsx
  23. 1 0
      apps/app/src/interfaces/apiv3/page.ts
  24. 1 0
      apps/app/src/interfaces/page.ts
  25. 5 2
      apps/app/src/server/routes/apiv3/page/update-page.ts
  26. 10 3
      apps/app/src/server/service/page/index.ts
  27. 11 11
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--use-tools.cy.ts
  28. 1 1
      apps/app/test/cypress/e2e/22-sharelink/22-sharelink--access-to-sharelink.cy.ts
  29. 16 20
      apps/app/test/cypress/e2e/23-editor/23-editor--saving.cy.ts
  30. 13 36
      apps/app/test/cypress/e2e/23-editor/23-editor--template-modal.cy.ts
  31. 27 41
      apps/app/test/cypress/e2e/23-editor/23-editor--with-navigation.cy.ts
  32. 3 3
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts
  33. 1 1
      packages/editor/src/components/CodeMirrorEditor/Toolbar/TemplateButton.tsx
  34. 3 2
      packages/remark-lsx/src/client/components/Lsx.tsx
  35. 15 2
      packages/remark-lsx/src/client/components/LsxPageList/LsxListView.module.scss
  36. 7 5
      packages/remark-lsx/src/client/components/LsxPageList/LsxPage.tsx

+ 2 - 2
.github/workflows/ci-app-prod.yml

@@ -62,7 +62,7 @@ jobs:
     with:
       node-version: 20.x
       skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
-      cypress-report-artifact-name: Cypress report
+      cypress-report-artifact-name-prefix: cypress-report-
       cypress-config-video: ${{ inputs.cypress-config-video || false }}
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -78,7 +78,7 @@ jobs:
     with:
       node-version: 20.x
       skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
-      cypress-report-artifact-name: Cypress report
+      cypress-report-artifact-name-pattern: cypress-report-*
     secrets:
       REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
       AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}

+ 2 - 2
.github/workflows/reusable-app-prod.yml

@@ -8,7 +8,7 @@ on:
         type: string
       skip-cypress:
         type: boolean
-      cypress-report-artifact-name:
+      cypress-report-artifact-name-prefix:
         type: string
       cypress-config-video:
         type: boolean
@@ -325,7 +325,7 @@ jobs:
       if: always()
       uses: actions/upload-artifact@v4
       with:
-        name: ${{ inputs.cypress-report-artifact-name }}
+        name: ${{ inputs.cypress-report-artifact-name-prefix }}${{ matrix.spec-group }}
         path: |
           apps/app/test/cypress/screenshots
           apps/app/test/cypress/videos

+ 3 - 2
.github/workflows/reusable-app-reg-suit.yml

@@ -11,7 +11,7 @@ on:
         default: ${{ github.head_ref }}
       skip-reg-suit:
         type: boolean
-      cypress-report-artifact-name:
+      cypress-report-artifact-name-pattern:
         required: true
         type: string
     secrets:
@@ -88,8 +88,9 @@ jobs:
     - name: Download screenshots taken by cypress
       uses: actions/download-artifact@v4
       with:
-        name: ${{ inputs.cypress-report-artifact-name }}
         path: apps/app/test/cypress
+        pattern: ${{ inputs.cypress-report-artifact-name-pattern }}
+        merge-multiple: true
 
     - name: Run reg-suit
       working-directory: ./apps/app

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

@@ -330,8 +330,13 @@
   "page_comment": {
     "comments": "Commments",
     "comment": "Commment",
+    "preview": "Preview",
+    "write": "Write",
+    "add_a_comment": "Add a comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
-    "no_user_found": "No user found"
+    "no_user_found": "No user found",
+    "reply": "Reply",
+    "delete_comment": "Delete comment?"
   },
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
@@ -531,7 +536,8 @@
   },
   "slack_notification": {
     "popover_title": "Slack Notification",
-    "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
+    "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list.",
+    "input_channels": "Input channels"
   },
   "search_result": {
     "title": "Search",

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

@@ -363,8 +363,13 @@
   "page_comment": {
     "comments": "コメント",
     "comment": "コメント",
+    "preview": "プレビュー",
+    "write": "入力",
+    "add_a_comment": "コメントを追加",
     "display_the_page_when_posting_this_comment": "投稿時のページを表示する",
-    "no_user_found": "ユーザー名が見つかりません"
+    "no_user_found": "ユーザー名が見つかりません",
+    "reply": "返信",
+    "delete_comment": "コメントを削除しますか?"
   },
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
@@ -564,7 +569,8 @@
   },
   "slack_notification": {
     "popover_title": "Slack 通知",
-    "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
+    "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。",
+    "input_channels": "チャンネル名"
   },
   "search_result": {
     "title": "検索",

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

@@ -320,8 +320,13 @@
   "page_comment": {
     "comments": "评论",
     "comment": "评论",
+    "preview": "预览",
+    "write": "输入",
+    "add_a_comment": "Add a comment",
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
-    "no_user_found": "未找到用户名"
+    "no_user_found": "未找到用户名",
+    "reply": "Reply",
+    "delete_comment": "Delete comment?"
   },
   "page_api_error": {
     "notfound_or_forbidden": "未找到或禁止原始页。",
@@ -518,7 +523,8 @@
   },
   "slack_notification": {
     "popover_title": "Slack Notification",
-    "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
+    "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list.",
+    "input_channels": "Input channels"
   },
   "share_links": {
     "Shere this page link to public": "Shere this page link to public",

+ 4 - 1
apps/app/src/components/PageComment.tsx

@@ -5,6 +5,7 @@ import React, {
 
 import { isPopulated, getIdForRef, type IRevisionHasId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
+import { useTranslation } from 'next-i18next';
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { toastError } from '~/client/util/toastr';
@@ -50,6 +51,8 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
   const [errorMessageOnDelete, setErrorMessageOnDelete] = useState<string>('');
   const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
 
+  const { t } = useTranslation('');
+
   const commentsFromOldest = useMemo(() => (comments != null ? [...comments].reverse() : null), [comments]);
   const commentsExceptReply: ICommentHasIdList | undefined = useMemo(
     () => commentsFromOldest?.filter(comment => comment.replyTo == null), [commentsFromOldest],
@@ -178,7 +181,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
                           onClick={() => onReplyButtonClickHandler(comment._id)}
                         >
                           <UserPicture user={currentUser} noLink noTooltip additionalClassName="me-2" />
-                          <span className="material-symbols-outlined me-1 fs-5 pb-1">reply</span><small>Reply...</small>
+                          <span className="material-symbols-outlined me-1 fs-5 pb-1">reply</span><small>{t('page_comment.reply')}...</small>
                         </button>
                       </NotAvailableForReadOnlyUser>
                     </NotAvailableForGuest>

+ 2 - 2
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -239,7 +239,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
             >
               <UserPicture user={currentUser} noLink noTooltip additionalClassName="me-3" />
               <span className="material-symbols-outlined me-1 fs-5">add_comment</span>
-              <small>Add Comment in markdown...</small>
+              <small>{t('page_comment.add_a_comment')}...</small>
             </button>
           </NotAvailableForReadOnlyUser>
         </NotAvailableForGuest>
@@ -304,7 +304,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
           <div className="d-flex justify-content-between align-items-center mb-2">
             <div className="d-flex">
               <UserPicture user={currentUser} noLink noTooltip />
-              <p className="ms-2 mb-0">Add a comment</p>
+              <p className="ms-2 mb-0">{t('page_comment.add_a_comment')}</p>
             </div>
             <SwitchingButtonGroup showPreview={showPreview} onSelected={handleSelect} />
           </div>

+ 9 - 5
apps/app/src/components/PageComment/DeleteCommentModal.tsx

@@ -3,11 +3,13 @@ import React from 'react';
 import { isPopulated } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns';
+import { t } from 'i18next';
+import { useTranslation } from 'next-i18next';
 import {
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import { ICommentHasId } from '../../interfaces/comment';
+import type { ICommentHasId } from '../../interfaces/comment';
 import { Username } from '../User/Username';
 
 import styles from './DeleteCommentModal.module.scss';
@@ -26,6 +28,8 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
     isShown, comment, errorMessage, cancelToDelete, confirmToDelete,
   } = props;
 
+  const { t } = useTranslation();
+
   const headerContent = () => {
     if (comment == null || isShown === false) {
       return <></>;
@@ -33,7 +37,7 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
     return (
       <span>
         <span className="material-symbols-outlined">delete_forever</span>
-        Delete comment?
+        {t('page_comment.delete_comment')}
       </span>
     );
   };
@@ -58,7 +62,7 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
 
     return (
       <>
-        <UserPicture user={creator} size="xs" /> <strong><Username user={creator}></Username></strong> wrote on {commentDate}:
+        <UserPicture user={creator} size="xs" /> <strong className="me-2"><Username user={creator}></Username></strong>{commentDate}:
         <p className="card custom-card comment-body mt-2 p-2">{commentBodyElement}</p>
       </>
     );
@@ -71,10 +75,10 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
     return (
       <>
         <span className="text-danger">{errorMessage}</span>&nbsp;
-        <Button onClick={cancelToDelete}>Cancel</Button>
+        <Button onClick={cancelToDelete}>{t('Cancel')}</Button>
         <Button color="danger" onClick={confirmToDelete}>
           <span className="material-symbols-outlined">delete_forever</span>
-          Delete
+          {t('Delete')}
         </Button>
       </>
     );

+ 1 - 1
apps/app/src/components/PageComment/SwitchingButtonGroup.module.scss

@@ -9,7 +9,7 @@
     height: 38px;
 
     @include bs.media-breakpoint-up(sm) {
-      width: 90px;
+      width: auto;
       height: 30px;
     }
   }

+ 2 - 2
apps/app/src/components/PageComment/SwitchingButtonGroup.tsx

@@ -57,7 +57,7 @@ export const SwitchingButtonGroup = (props: Props): JSX.Element => {
         onClick={() => onSelected?.(true)}
       >
         <span className="material-symbols-outlined me-0">play_arrow</span>
-        <span className="d-none d-sm-inline">{t('Preview')}</span>
+        <span className="d-none d-sm-inline">{t('page_comment.preview')}</span>
       </SwitchingButton>
       <SwitchingButton
         active={!showPreview}
@@ -65,7 +65,7 @@ export const SwitchingButtonGroup = (props: Props): JSX.Element => {
         onClick={() => onSelected?.(false)}
       >
         <span className="material-symbols-outlined me-1">edit_square</span>
-        <span className="d-none d-sm-inline">{t('Write')}</span>
+        <span className="d-none d-sm-inline">{t('page_comment.write')}</span>
       </SwitchingButton>
     </div>
   );

+ 2 - 0
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -66,6 +66,7 @@ declare global {
 }
 
 export type SaveOptions = {
+  wip: boolean,
   slackChannels: string,
   overwriteScopesOfDescendants?: boolean
 }
@@ -179,6 +180,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       const { page } = await updatePage({
         pageId,
         revisionId,
+        wip: opts?.wip,
         body: markdown ?? '',
         grant: selectedGrant?.grant,
         origin: Origin.Editor,

+ 1 - 0
apps/app/src/components/PageEditor/Preview.tsx

@@ -32,6 +32,7 @@ const Preview = (props: Props): JSX.Element => {
 
   return (
     <div
+      data-testid="page-editor-preview-body"
       className={`${moduleClass} ${fluidLayoutClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
       style={style}
     >

+ 6 - 0
apps/app/src/components/PageTags/TagsInput.tsx

@@ -42,6 +42,12 @@ export const TagsInput: FC<Props> = (props: Props) => {
     if (event.key === ' ') {
       event.preventDefault();
 
+      // fix: https://redmine.weseek.co.jp/issues/140689
+      const isComposing = event.nativeEvent.isComposing;
+      if (isComposing) {
+        return;
+      }
+
       const initialItem = tagsInputRef?.current?.state?.initialItem;
       const handleMenuItemSelect = tagsInputRef?.current?._handleMenuItemSelect;
 

+ 10 - 33
apps/app/src/components/SavePageControls.tsx

@@ -10,23 +10,18 @@ import {
   DropdownToggle, DropdownMenu, DropdownItem, Modal,
 } from 'reactstrap';
 
-import { toastSuccess, toastError } from '~/client/util/toastr';
 import {
   useIsEditable, useIsAclEnabled,
   useIsSlackConfigured,
 } from '~/stores/context';
 import { useWaitingSaveProcessing, useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
-import { useSWRMUTxCurrentPage, useSWRxCurrentPage, useCurrentPagePath } from '~/stores/page';
-import { mutatePageTree } from '~/stores/page-listing';
+import { useSWRxCurrentPage, useCurrentPagePath } from '~/stores/page';
 import {
   useEditorMode, useIsDeviceLargerThanMd,
-  EditorMode,
+
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
-
-import { unpublish } from '../client/services/page-operation';
-
 import { GrantSelector } from './SavePageControls/GrantSelector';
 import { SlackNotification } from './SlackNotification';
 
@@ -43,10 +38,7 @@ const logger = loggerFactory('growi:SavePageControls');
 const SavePageButton = (props: {slackChannels: string, isDeviceLargerThanMd?: boolean}) => {
 
   const { t } = useTranslation();
-  const { data: currentPage } = useSWRxCurrentPage();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
-  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
-  const { mutate: mutateEditorMode } = useEditorMode();
   const [isSavePageModalShown, setIsSavePageModalShown] = useState<boolean>(false);
 
   const { slackChannels, isDeviceLargerThanMd } = props;
@@ -55,33 +47,18 @@ const SavePageButton = (props: {slackChannels: string, isDeviceLargerThanMd?: bo
 
   const save = useCallback(async(): Promise<void> => {
     // save
-    globalEmitter.emit('saveAndReturnToView', { slackChannels });
+    globalEmitter.emit('saveAndReturnToView', { wip: false, slackChannels });
   }, [slackChannels]);
 
   const saveAndOverwriteScopesOfDescendants = useCallback(() => {
     // save
-    globalEmitter.emit('saveAndReturnToView', { overwriteScopesOfDescendants: true, slackChannels });
+    globalEmitter.emit('saveAndReturnToView', { wip: false, overwriteScopesOfDescendants: true, slackChannels });
   }, [slackChannels]);
 
-  const clickUnpublishButtonHandler = useCallback(async() => {
-    const pageId = currentPage?._id;
-
-    if (pageId == null) {
-      return;
-    }
-
-    try {
-      await unpublish(pageId);
-      await mutateCurrentPage();
-      await mutatePageTree();
-      await mutateEditorMode(EditorMode.View);
-      toastSuccess(t('wip_page.success_save_as_wip'));
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(t('wip_page.fail_save_as_wip'));
-    }
-  }, [currentPage?._id, mutateCurrentPage, mutateEditorMode, t]);
+  const saveAndMakeWip = useCallback(() => {
+    // save
+    globalEmitter.emit('saveAndReturnToView', { wip: true, slackChannels });
+  }, [slackChannels]);
 
   const labelSubmitButton = t('Update');
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
@@ -111,7 +88,7 @@ const SavePageButton = (props: {slackChannels: string, isDeviceLargerThanMd?: bo
                 <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
                   {labelOverwriteScopes}
                 </DropdownItem>
-                <DropdownItem onClick={clickUnpublishButtonHandler}>
+                <DropdownItem onClick={saveAndMakeWip}>
                   {labelUnpublishPage}
                 </DropdownItem>
               </DropdownMenu>
@@ -128,7 +105,7 @@ const SavePageButton = (props: {slackChannels: string, isDeviceLargerThanMd?: bo
                   <button type="button" className="btn btn-primary" onClick={() => { setIsSavePageModalShown(false); saveAndOverwriteScopesOfDescendants() }}>
                     {labelOverwriteScopes}
                   </button>
-                  <button type="button" className="btn btn-primary" onClick={() => { setIsSavePageModalShown(false); clickUnpublishButtonHandler() }}>
+                  <button type="button" className="btn btn-primary" onClick={() => { setIsSavePageModalShown(false); saveAndMakeWip() }}>
                     {labelUnpublishPage}
                   </button>
                   <button type="button" className="btn btn-outline-neutral-secondary mx-auto mt-1" onClick={() => setIsSavePageModalShown(false)}>

+ 1 - 1
apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx

@@ -190,7 +190,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
           >
             {dropdownToggleLabelElm}
           </DropdownToggle>
-          <DropdownMenu container={openInModal ? '' : 'body'}>
+          <DropdownMenu data-testid="grw-grant-selector-dropdown-menu" container={openInModal ? '' : 'body'}>
             {dropdownMenuElems}
           </DropdownMenu>
         </UncontrolledDropdown>

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

@@ -15,7 +15,6 @@ export const CreateButton = (props: Props): JSX.Element => {
       type="button"
       {...props}
       className={`${moduleClass} btn btn-primary ${props.className ?? ''}`}
-      data-testid="grw-sidebar-nav-page-create-button"
     >
       <Hexagon />
       <span className="icon material-symbols-outlined position-absolute">edit</span>

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

@@ -26,6 +26,7 @@ export const DropendMenu = React.memo((props: DropendMenuProps): JSX.Element =>
   return (
     <DropdownMenu
       container="body"
+      data-testid="grw-page-create-button-dropend-menu"
     >
       <DropdownItem
         onClick={onClickCreateNewPage}

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

@@ -14,6 +14,7 @@ export const DropendToggle = (): JSX.Element => {
       color="primary"
       className={`position-absolute z-1 ${moduleClass}`}
       aria-expanded={false}
+      data-testid="grw-page-create-button-dropend-toggle"
     >
       <Hexagon />
       <div className="hitarea position-absolute" />

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

@@ -45,6 +45,7 @@ export const PageCreateButton = React.memo((): JSX.Element => {
       className="d-flex flex-row mt-2"
       onMouseEnter={onMouseEnterHandler}
       onMouseLeave={onMouseLeaveHandler}
+      data-testid="grw-page-create-button"
     >
       <div className="btn-group flex-grow-1">
         <CreateButton

+ 6 - 7
apps/app/src/components/Sidebar/Tag.tsx

@@ -2,7 +2,7 @@ import type { FC } from 'react';
 import React, { useState, useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
-import { useRouter } from 'next/router';
+import Link from 'next/link';
 
 import type { IDataTagCount } from '~/interfaces/tag';
 import { useSWRxTagsList } from '~/stores/tag';
@@ -19,8 +19,6 @@ const TAG_CLOUD_LIMIT = 20;
 
 const Tag: FC = () => {
 
-  const router = useRouter();
-
   const [activePage, setActivePage] = useState<number>(1);
   const [offset, setOffset] = useState<number>(0);
 
@@ -71,13 +69,14 @@ const Tag: FC = () => {
       }
 
       <div className="d-flex justify-content-center my-5">
-        <button
+        <Link
+          href="/tags"
           className="btn btn-primary rounded px-4"
-          type="button"
-          onClick={() => router.push('/tags')}
+          role="button"
+          prefetch={false}
         >
           {t('Check All tags')}
-        </button>
+        </Link>
       </div>
 
       <h6 className="my-3 pb-1 border-bottom">{t('popular_tags')}</h6>

+ 1 - 1
apps/app/src/components/SlackNotification.tsx

@@ -59,7 +59,7 @@ export const SlackNotification: FC<SlackNotificationProps> = ({
         id={idForSlackPopover}
         type="text"
         value={slackChannels}
-        placeholder="Input channels"
+        placeholder={`${t('slack_notification.input_channels')}`}
         onChange={updateSlackChannelsHandler}
       />
       <UncontrolledPopover trigger="focus" placement="top" target={idForSlackPopover}>

+ 1 - 0
apps/app/src/interfaces/apiv3/page.ts

@@ -32,6 +32,7 @@ export type IApiv3PageUpdateParams = IOptionsForUpdate & {
   origin?: Origin,
   isSlackEnabled?: boolean,
   slackChannels?: string,
+  wip?: boolean
 };
 
 export type IApiv3PageUpdateResponse = {

+ 1 - 0
apps/app/src/interfaces/page.ts

@@ -60,6 +60,7 @@ export type IDeleteManyPageApiv3Result = {
 
 export type IOptionsForUpdate = {
   origin?: Origin
+  wip?: boolean,
   grant?: PageGrant,
   userRelatedGrantUserGroupIds?: IGrantedGroup[],
   // isSyncRevisionToHackmd?: boolean,

+ 5 - 2
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -76,6 +76,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     body('isSlackEnabled').optional().isBoolean().withMessage('isSlackEnabled must be boolean'),
     body('slackChannels').optional().isString().withMessage('slackChannels must be string'),
     body('origin').optional().isIn(allOrigin).withMessage('origin must be "view" or "editor"'),
+    body('wip').optional().isBoolean().withMessage('wip must be boolean'),
   ];
 
 
@@ -158,8 +159,10 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
 
       let updatedPage;
       try {
-        const { grant, userRelatedGrantUserGroupIds, overwriteScopesOfDescendants } = req.body;
-        const options: IOptionsForUpdate = { overwriteScopesOfDescendants, origin };
+        const {
+          grant, userRelatedGrantUserGroupIds, overwriteScopesOfDescendants, wip,
+        } = req.body;
+        const options: IOptionsForUpdate = { overwriteScopesOfDescendants, origin, wip };
         if (grant != null) {
           options.grant = grant;
           options.userRelatedGrantUserGroupIds = userRelatedGrantUserGroupIds;

+ 10 - 3
apps/app/src/server/service/page/index.ts

@@ -4146,9 +4146,16 @@ class PageService implements IPageService {
     const clonedPageData = Page.hydrate(pageData.toObject());
     const newPageData = pageData;
 
-    // If updated at least once, publish
-    pageData.publish();
-
+    // Once updated it's exempt from automatic deletion
+    if (options.wip == null) {
+      newPageData.ttlTimestamp = undefined;
+    }
+    else if (options.wip) {
+      newPageData.unpublish();
+    }
+    else {
+      newPageData.publish();
+    }
 
     // use the previous data if absent
     const grant = options.grant ?? clonedPageData.grant;

+ 11 - 11
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--use-tools.cy.ts

@@ -10,7 +10,7 @@ context('Modal for page operation', () => {
   });
 
   it('Page Deletion and PutBack is executed successfully', { scrollBehavior: false }, () => {
-    cy.visit('/Sandbox/Bootstrap4');
+    cy.visit('/Sandbox/Bootstrap5');
 
     cy.waitUntil(() => {
       // do
@@ -30,7 +30,7 @@ context('Modal for page operation', () => {
 
     cy.getByTestid('trash-page-alert').should('be.visible');
     cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-bootstrap4-is-in-garbage-box`);
+    cy.screenshot(`${ssPrefix}-bootstrap5-is-in-garbage-box`);
 
     cy.getByTestid('put-back-button').click();
     cy.getByTestid('put-back-page-modal').should('be.visible').within(() => {
@@ -39,11 +39,11 @@ context('Modal for page operation', () => {
     });
 
     cy.collapseSidebar(true);
-    cy.screenshot(`${ssPrefix}-put-backed-bootstrap4-page`);
+    cy.screenshot(`${ssPrefix}-put-backed-bootstrap5-page`);
   });
 
   it('PageDuplicateModal is shown successfully', () => {
-    cy.visit('/Sandbox/Bootstrap4');
+    cy.visit('/Sandbox/5');
     cy.waitUntilSkeletonDisappear();
 
     cy.waitUntil(() => {
@@ -57,11 +57,11 @@ context('Modal for page operation', () => {
 
     cy.getByTestid('open-page-duplicate-modal-btn').filter(':visible').click({force: true});
 
-    cy.getByTestid('page-duplicate-modal').should('be.visible').screenshot(`${ssPrefix}-duplicate-bootstrap4`);
+    cy.getByTestid('page-duplicate-modal').should('be.visible').screenshot(`${ssPrefix}-duplicate-bootstrap5`);
   });
 
   it('PageMoveRenameModal is shown successfully', () => {
-    cy.visit('/Sandbox/Bootstrap4');
+    cy.visit('/Sandbox/Bootstrap5');
     cy.waitUntilSkeletonDisappear();
 
     cy.waitUntil(() => {
@@ -76,7 +76,7 @@ context('Modal for page operation', () => {
     cy.getByTestid('open-page-move-rename-modal-btn').filter(':visible').click({force: true});
     cy.getByTestid('grw-page-rename-button').should('be.disabled');
 
-    cy.getByTestid('page-rename-modal').should('be.visible').screenshot(`${ssPrefix}-rename-bootstrap4`);
+    cy.getByTestid('page-rename-modal').should('be.visible').screenshot(`${ssPrefix}-rename-bootstrap5`);
   });
 
 });
@@ -144,7 +144,7 @@ context('Page Accessories Modal', () => {
     cy.getByTestid('page-history').should('be.visible');
 
     cy.waitUntilSpinnerDisappear();
-    cy.screenshot(`${ssPrefix}-open-page-history-bootstrap4`);
+    cy.screenshot(`${ssPrefix}-open-page-history-bootstrap5`);
   });
 
   it('Page Attachment Data is shown successfully', () => {
@@ -155,7 +155,7 @@ context('Page Accessories Modal', () => {
     cy.waitUntilSpinnerDisappear();
     cy.getByTestid('page-attachment').should('be.visible').contains('No attachments yet.');
 
-    cy.screenshot(`${ssPrefix}-open-page-attachment-data-bootstrap4`);
+    cy.screenshot(`${ssPrefix}-open-page-attachment-data-bootstrap5`);
   });
 
   it('Share Link Management is shown successfully', () => {
@@ -167,7 +167,7 @@ context('Page Accessories Modal', () => {
     cy.getByTestid('page-accessories-modal').should('be.visible');
     cy.getByTestid('share-link-management').should('be.visible');
 
-    cy.screenshot(`${ssPrefix}-open-share-link-management-bootstrap4`);
+    cy.screenshot(`${ssPrefix}-open-share-link-management-bootstrap5`);
   });
 });
 
@@ -184,7 +184,7 @@ context('Tag Oprations', { scrollBehavior: false }, () =>{
     const ssPrefix = 'tag-operations-add-new-tag-'
     const tag = 'we';
 
-    cy.visit('/Sandbox/Bootstrap4');
+    cy.visit('/Sandbox/Bootstrap5');
     cy.collapseSidebar(true);
 
     // Add tag

+ 1 - 1
apps/app/test/cypress/e2e/22-sharelink/22-sharelink--access-to-sharelink.cy.ts

@@ -9,7 +9,7 @@ context('Access to sharelink by guest', () => {
       cy.login(user.username, user.password);
     });
 
-    cy.visit('/Sandbox/Bootstrap4');
+    cy.visit('/Sandbox/Bootstrap5');
 
     // open dropdown
     cy.waitUntil(() => {

+ 16 - 20
apps/app/test/cypress/e2e/23-editor/23-editor--saving.cy.ts

@@ -1,4 +1,4 @@
-context('PageCreateModal', () => {
+context('PageCreateButton', () => {
 
   const ssPrefix = 'page-create-modal-';
 
@@ -9,41 +9,37 @@ context('PageCreateModal', () => {
     });
   });
 
-  it("PageCreateModal is shown and closed successfully", () => {
+  it("DropendMenu is shown successfully", () => {
     cy.visit('/');
     cy.collapseSidebar(true, true);
 
+    cy.getByTestid('grw-page-create-button').trigger('mouseover');
+
     cy.waitUntil(() => {
       // do
-      cy.getByTestid('newPageBtn').click({force: true});
+      cy.getByTestid('grw-page-create-button-dropend-toggle').click({force: true});
       // wait until
-      return cy.getByTestid('page-create-modal').then($elem => $elem.is(':visible'));
-    });
-
-    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
-      cy.screenshot(`${ssPrefix}new-page-modal-opened`);
-      cy.get('button.close').click();
+      return cy.getByTestid('grw-page-create-button-dropend-menu').then($elem => $elem.is(':visible'));
     });
 
-    cy.screenshot(`${ssPrefix}page-create-modal-closed`);
+    cy.screenshot(`${ssPrefix}page-create-button-dropend-menu-shown`);
   });
 
   it("Successfully Create Today's page", () => {
-    const pageName = "Today's page";
     cy.visit('/');
     cy.collapseSidebar(true);
 
+    cy.getByTestid('grw-page-create-button').trigger('mouseover');
+
     cy.waitUntil(() => {
       // do
-      cy.getByTestid('newPageBtn').click({force: true});
+      cy.getByTestid('grw-page-create-button-dropend-toggle').click({force: true});
       // wait until
-      return cy.getByTestid('page-create-modal').then($elem => $elem.is(':visible'));
+      return cy.getByTestid('grw-page-create-button-dropend-menu').then($elem => $elem.is(':visible'));
     });
 
-    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
-      cy.get('.page-today-input2').type(pageName);
-      cy.screenshot(`${ssPrefix}today-add-page-name`);
-      cy.getByTestid('btn-create-memo').click();
+    cy.getByTestid('grw-page-create-button-dropend-menu').should('be.visible').within(() => {
+      cy.get('button').eq(1).click();
     });
 
     cy.getByTestid('page-editor').should('be.visible');
@@ -60,7 +56,7 @@ context('PageCreateModal', () => {
     cy.screenshot(`${ssPrefix}create-today-page`);
   });
 
-  it('Successfully create page under specific path', () => {
+  it.skip('Successfully create page under specific path', () => {
     const pageName = 'child';
 
     cy.visit('/foo/bar');
@@ -98,7 +94,7 @@ context('PageCreateModal', () => {
     cy.screenshot(`${ssPrefix}create-page-under-specific-page`);
   });
 
-  it('Trying to create template page under the root page fail', () => {
+  it.skip('Trying to create template page under the root page fail', () => {
     cy.visit('/');
     cy.collapseSidebar(true);
 
@@ -137,7 +133,7 @@ context('PageCreateModal', () => {
 });
 
 
-context('Shortcuts', () => {
+context.skip('Shortcuts', () => {
   const ssPrefix = 'shortcuts';
 
   beforeEach(() => {

+ 13 - 36
apps/app/test/cypress/e2e/23-editor/23-editor--template-modal.cy.ts

@@ -15,30 +15,18 @@ context('TemplateModal', () => {
 
     // move to edit mode
     cy.get('#grw-page-editor-mode-manager').as('pageEditorModeManager').should('be.visible');
-    cy.waitUntil(() => {
-      // do
-      cy.get('@pageEditorModeManager').within(() => {
-        cy.get('button:nth-child(2)').click();
-      });
-      // until
-      return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
-    })
-    cy.get('.grw-editor-navbar-bottom').should('be.visible');
+    cy.getByTestid('editor-button').click();
+    cy.getByTestid('grw-editor-navbar-bottom').should('be.visible');
 
-    // show TemplateModal
-    cy.waitUntil(() => {
-      // do
-      cy.get('.navbar-editor > ul > li:nth-child(16) > button').click({force: true});
-      // wait until
-      return cy.getByTestid('template-modal').then($elem => $elem.is(':visible'));
-    });
+    // open TemplateModal
+    cy.getByTestid('open-template-button').click();
+    cy.getByTestid('template-modal').should('be.visible');
+    cy.screenshot(`${ssPrefix}opened`);
 
     // close TemplateModal
     cy.getByTestid('template-modal').should('be.visible').within(() => {
-      cy.screenshot(`${ssPrefix}opened`);
-      cy.get('button.close').click();
+      cy.get('.btn-close').click();
     });
-
     cy.screenshot(`${ssPrefix}close`);
   });
 
@@ -48,30 +36,19 @@ context('TemplateModal', () => {
 
     // move to edit mode
     cy.get('#grw-page-editor-mode-manager').as('pageEditorModeManager').should('be.visible');
-    cy.waitUntil(() => {
-      // do
-      cy.get('@pageEditorModeManager').within(() => {
-        cy.get('button:nth-child(2)').click();
-      });
-      // until
-      return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
-    })
-    cy.get('.grw-editor-navbar-bottom').should('be.visible');
+    cy.getByTestid('editor-button').click();
+    cy.getByTestid('grw-editor-navbar-bottom').should('be.visible');
 
-    // show TemplateModal
-    cy.waitUntil(() => {
-      // do
-      cy.get('.navbar-editor > ul > li:nth-child(16) > button').click({force: true});
-      // wait until
-      return cy.getByTestid('template-modal').then($elem => $elem.is(':visible'));
-    });
+    // open TemplateModal
+    cy.getByTestid('open-template-button').click();
+    cy.getByTestid('template-modal').should('be.visible');
 
     // select template and template locale
     cy.getByTestid('template-modal').should('be.visible').within(() => {
       // select first template
       cy.get('.list-group > .list-group-item:nth-child(1)').click();
       // check preview exist
-      cy.get('.card-body > .page-editor-preview-body > .wiki').should('exist');
+      cy.get('.card-body').should('be.visible');
       cy.screenshot(`${ssPrefix}select-template`);
 
       // change template locale

+ 27 - 41
apps/app/test/cypress/e2e/23-editor/23-editor--with-navigation.cy.ts

@@ -2,15 +2,9 @@ import path from 'path-browserify';
 
 function openEditor() {
   cy.get('#grw-page-editor-mode-manager').as('pageEditorModeManager').should('be.visible');
-  cy.waitUntil(() => {
-    // do
-    cy.get('@pageEditorModeManager').within(() => {
-      cy.get('button:nth-child(2)').click();
-    });
-    // until
-    return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
-  })
-  cy.get('.CodeMirror').should('be.visible');
+  cy.getByTestid('editor-button').click();
+  cy.getByTestid('grw-editor-navbar-bottom').should('be.visible');
+  cy.get('.cm-content').should('be.visible');
 }
 
 context('Editor while uploading to a new page', () => {
@@ -38,8 +32,8 @@ context('Editor while uploading to a new page', () => {
 
     // input the body
     const body = 'Hello World!';
-    cy.get('.CodeMirror textarea').type(body + '\n\n', { force: true });
-    cy.get('.CodeMirror-code').should('contain.text', body);
+    cy.get('.cm-content').should('be.visible').type(body, { force: true });
+    cy.getByTestid('page-editor-preview-body').should('contain.text', body);
 
     // open GrantSelector
     cy.waitUntil(() => {
@@ -48,18 +42,14 @@ context('Editor while uploading to a new page', () => {
         cy.get('button.dropdown-toggle').click({force: true});
       });
       // wait until
-      return cy.getByTestid('grw-grant-selector').within(() => {
-        return Cypress.$('.dropdown-menu.show').is(':visible');
-      });
+      return cy.getByTestid('grw-grant-selector-dropdown-menu').then($elem => $elem.is(':visible'))
     });
 
     // Select "Only me"
-    cy.getByTestid('grw-grant-selector').within(() => {
+    cy.getByTestid('grw-grant-selector-dropdown-menu').find('.dropdown-item').should('have.length', 4).then((menuItems) => {
       // click "Only me"
-      cy.get('.dropdown-menu.show').find('.dropdown-item').should('have.length', 4).then((menuItems) => {
-        menuItems[2].click();
-      });
-    });
+      menuItems[2].click();
+    })
 
     cy.getByTestid('grw-grant-selector').find('.dropdown-toggle').should('contain.text', 'Only me');
     cy.screenshot(`${ssPrefix}-prevent-grantselector-modified-2`);
@@ -67,7 +57,7 @@ context('Editor while uploading to a new page', () => {
     // intercept API req/res for fixing labels
     const dummyAttachmentId = '64b000000000000000000000';
     let uploadedAttachmentId = '';
-    cy.intercept('POST', '/_api/attachments.add', (req) => {
+    cy.intercept('POST', '/_api/v3/attachment', (req) => {
       req.continue((res) => {
         // store the attachment id
         uploadedAttachmentId = res.body.attachment._id;
@@ -86,20 +76,20 @@ context('Editor while uploading to a new page', () => {
 
     // drag-drop a file
     const filePath = path.relative('/', path.resolve(Cypress.spec.relative, '../assets/example.txt'));
-    cy.get('.dropzone').selectFile(filePath, { action: 'drag-drop' });
+    cy.get('.dropzone').eq(0).selectFile(filePath, { action: 'drag-drop' });
     cy.wait('@attachmentsAdd');
 
     cy.screenshot(`${ssPrefix}-prevent-grantselector-modified-3`);
 
     // Update page using shortcut keys
-    cy.get('.CodeMirror').click().type('{ctrl+s}');
+    cy.get('.cm-content').click({force: true}).type('{ctrl+s}');
 
     cy.screenshot(`${ssPrefix}-prevent-grantselector-modified-4`);
 
     // expect
     cy.get('.Toastify__toast').should('contain.text', 'Saved successfully');
-    cy.get('.CodeMirror-code').should('contain.text', body);
-    cy.get('.CodeMirror-code').should('contain.text', '[example.txt](/attachment/64b000000000000000000000');
+    cy.get('.cm-content').should('contain.text', body);
+    cy.get('.cm-content').should('contain.text', '[example.txt](/attachment/64b000000000000000000000');
     cy.getByTestid('grw-grant-selector').find('.dropdown-toggle').should('contain.text', 'Only me');
     cy.screenshot(`${ssPrefix}-prevent-grantselector-modified-5`);
   });
@@ -131,11 +121,9 @@ context('Editor while navigation', () => {
 
     // page1
     const bodyHello = 'hello';
-    cy.get('.CodeMirror').type(bodyHello);
-    cy.get('.CodeMirror').should('contain.text', bodyHello);
-    cy.get('.page-editor-preview-body').should('contain.text', bodyHello);
-    cy.getByTestid('page-editor').should('be.visible');
-    cy.get('.CodeMirror').screenshot(`${ssPrefix}-editor-for-page1`);
+    cy.get('.cm-content').should('be.visible').type(bodyHello, { force: true });
+    cy.getByTestid('page-editor-preview-body').should('contain.text', bodyHello);
+    cy.get('.cm-content').screenshot(`${ssPrefix}-editor-for-page1`);
 
     // save page1
     cy.getByTestid('save-page-btn').click();
@@ -159,31 +147,29 @@ context('Editor while navigation', () => {
     })
 
     openEditor();
-
-    cy.get('.CodeMirror').screenshot(`${ssPrefix}-editor-for-page2`);
+    cy.get('.cm-content').screenshot(`${ssPrefix}-editor-for-page2`);
 
     // type (without save)
     const bodyWorld = ' world!!'
-    cy.get('.CodeMirror').type(`${bodyWorld}`);
-    cy.get('.CodeMirror').should('contain.text', `${bodyHello}${bodyWorld}`);
-    cy.get('.page-editor-preview-body').should('contain.text', `${bodyHello}${bodyWorld}`);
-    cy.get('.CodeMirror').screenshot(`${ssPrefix}-editor-for-page2-modified`);
+    cy.get('.cm-content').should('be.visible').type(`{moveToEnd}${bodyWorld}`, { force: true });
+    cy.getByTestid('page-editor-preview-body').should('contain.text', `${bodyHello}${bodyWorld}`);
+    cy.get('.cm-content').screenshot(`${ssPrefix}-editor-for-page2-modified`);
 
     // create a link to page1
-    cy.get('.CodeMirror').type('\n\n[page1](./page1)');
+    cy.get('.cm-content').type('\n\n[page1](./page1)');
 
     // go to page1
-    cy.get('.page-editor-preview-body').within(() => {
+    cy.getByTestid('page-editor-preview-body').within(() => {
       cy.get("a:contains('page1')").click();
     });
 
     openEditor();
 
-    cy.get('.CodeMirror').screenshot(`${ssPrefix}-editor-for-page1-returned`);
+    cy.get('.cm-content').screenshot(`${ssPrefix}-editor-for-page1-returned`);
 
     // expect
-    cy.get('.CodeMirror').should('contain.text', bodyHello);
-    cy.get('.CodeMirror').should('not.contain.text', bodyWorld); // text that added to page2
-    cy.get('.CodeMirror').should('not.contain.text', 'page1'); // text that added to page2
+    cy.get('.cm-content').should('contain.text', bodyHello);
+    cy.get('.cm-content').should('not.contain.text', bodyWorld); // text that added to page2
+    cy.get('.cm-content').should('not.contain.text', 'page1'); // text that added to page2
   });
 });

+ 3 - 3
apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts

@@ -22,7 +22,7 @@ describe('Access to sidebar', () => {
         cy.visit('/');
 
         // Since this is a sidebar test, call collapseSidebar in beforeEach.
-        cy.collapseSidebar(false);
+        cy.collapseSidebar(false, true);
       });
 
       describe('Test show/collapse button', () => {
@@ -197,7 +197,7 @@ describe('Access to sidebar', () => {
 
         it('Successfully access to custom sidebar', () => {
           cy.getByTestid('grw-sidebar-contents').within(() => {
-            cy.get('.grw-sidebar-content-header > h3').find('a');
+            cy.get('.grw-sidebar-content-header > h4').find('a');
 
             cy.waitUntilSkeletonDisappear();
             cy.screenshot(`${ssPrefix}custom-sidebar-1-access-to-custom-sidebar`, { blackout: blackoutOverride });
@@ -270,7 +270,7 @@ describe('Access to sidebar', () => {
 
         it('Succesfully click all tags button', () => {
           cy.getByTestid('grw-sidebar-content-tags').within(() => {
-            cy.get('.btn-primary').click({force: true});
+            cy.get('.btn-primary').click();
           });
           cy.collapseSidebar(true);
           cy.getByTestid('grw-tags-list').should('be.visible');

+ 1 - 1
packages/editor/src/components/CodeMirrorEditor/Toolbar/TemplateButton.tsx

@@ -22,7 +22,7 @@ export const TemplateButton = (props: Props): JSX.Element => {
   }, [codeMirrorEditor?.view, openTemplateModal]);
 
   return (
-    <button type="button" className="btn btn-toolbar-button" onClick={onClickTempleteButton}>
+    <button type="button" className="btn btn-toolbar-button" onClick={onClickTempleteButton} data-testid="open-template-button">
       <span className="material-symbols-outlined fs-5">file_copy</span>
     </button>
   );

+ 3 - 2
packages/remark-lsx/src/client/components/Lsx.tsx

@@ -1,6 +1,5 @@
 import React, { useCallback, useMemo } from 'react';
 
-
 import { LoadingSpinner } from '@growi/ui/dist/components';
 
 import { useSWRxLsx } from '../stores/lsx';
@@ -117,7 +116,9 @@ const LsxSubstance = React.memo(({
             onClick={() => setSize(size => size + 1)}
           >
             Load more<br />
-            <span className="text-muted small start-items-label">({leftItemsNum} pages left)</span>
+            <span className="text-muted small start-items-label">
+              {leftItemsNum} pages left
+            </span>
           </button>
         </div>
       </div>

+ 15 - 2
packages/remark-lsx/src/client/components/LsxPageList/LsxListView.module.scss

@@ -1,7 +1,20 @@
 @use '@growi/ui/scss/molecules/page_list';
 
 .page-list :global {
-  .page-list-ul > li > a:not(:hover) {
-    text-decoration: none;
+  .page-list-li  {
+    line-height: inherit;
+  }
+
+  .page-list-ul > li > div > a {
+    padding: 0px;
+    color: inherit;
+
+    &:not(:hover) {
+      text-decoration: none;
+    }
+
+    &:hover {
+      color: inherit;
+    }
   }
 }

+ 7 - 5
packages/remark-lsx/src/client/components/LsxPageList/LsxPage.tsx

@@ -65,8 +65,8 @@ export const LsxPage = React.memo((props: Props): JSX.Element => {
   const iconElement: JSX.Element = useMemo(() => {
     const isExists = pageId != null;
     return (isExists)
-      ? <span className="material-symbols-outlined" aria-hidden="true">description</span>
-      : <span className="material-symbols-outlined" aria-hidden="true">draft</span>;
+      ? <span className="material-symbols-outlined fs-5 me-1" aria-hidden="true">description</span>
+      : <span className="material-symbols-outlined fs-5 me-1" aria-hidden="true">draft</span>;
   }, [pageId]);
 
   const pagePathElement: JSX.Element = useMemo(() => {
@@ -110,9 +110,11 @@ export const LsxPage = React.memo((props: Props): JSX.Element => {
   }, [basisViewersCount, pageNode]);
 
   return (
-    <li className={`page-list-li ${styles['page-list-li']}`}>
-      <small>{iconElement}</small> {pagePathElement}
-      <span className="ms-2">{pageListMetaElement}</span>
+    <li className={`page-list-li ${styles['page-list-li']} my-2`}>
+      <div className="d-flex align-items-center">
+        {iconElement} {pagePathElement}
+        <span className="ms-2">{pageListMetaElement}</span>
+      </div>
       {childrenElements}
     </li>
   );