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

Merge branch 'master' into imprv/140673-143662-search-results-form-improvements

maeshinshin 2 лет назад
Родитель
Сommit
bb7d4b7cde
24 измененных файлов с 143 добавлено и 147 удалено
  1. 8 2
      apps/app/public/static/locales/en_US/translation.json
  2. 8 2
      apps/app/public/static/locales/ja_JP/translation.json
  3. 8 2
      apps/app/public/static/locales/zh_CN/translation.json
  4. 4 1
      apps/app/src/components/PageComment.tsx
  5. 2 2
      apps/app/src/components/PageComment/CommentEditor.tsx
  6. 9 5
      apps/app/src/components/PageComment/DeleteCommentModal.tsx
  7. 1 1
      apps/app/src/components/PageComment/SwitchingButtonGroup.module.scss
  8. 2 2
      apps/app/src/components/PageComment/SwitchingButtonGroup.tsx
  9. 2 0
      apps/app/src/components/PageEditor/PageEditor.tsx
  10. 1 0
      apps/app/src/components/PageEditor/Preview.tsx
  11. 10 33
      apps/app/src/components/SavePageControls.tsx
  12. 1 1
      apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx
  13. 1 1
      apps/app/src/components/SlackNotification.tsx
  14. 1 0
      apps/app/src/interfaces/apiv3/page.ts
  15. 1 0
      apps/app/src/interfaces/page.ts
  16. 5 2
      apps/app/src/server/routes/apiv3/page/update-page.ts
  17. 10 3
      apps/app/src/server/service/page/index.ts
  18. 13 36
      apps/app/test/cypress/e2e/23-editor/23-editor--template-modal.cy.ts
  19. 27 41
      apps/app/test/cypress/e2e/23-editor/23-editor--with-navigation.cy.ts
  20. 1 1
      packages/editor/src/components/CodeMirrorEditor/Toolbar/TemplateButton.tsx
  21. 3 2
      packages/remark-lsx/src/client/components/Lsx.tsx
  22. 15 2
      packages/remark-lsx/src/client/components/LsxPageList/LsxListView.module.scss
  23. 7 5
      packages/remark-lsx/src/client/components/LsxPageList/LsxPage.tsx
  24. 3 3
      yarn.lock

+ 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
 }
@@ -178,6 +179,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       const { page } = await updatePage({
         pageId,
         revisionId,
+        wip: opts?.wip,
         body: markdown ?? '',
         grant: grantData?.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}
     >

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

@@ -10,25 +10,20 @@ import {
   DropdownToggle, DropdownMenu, DropdownItem, Modal,
 } from 'reactstrap';
 
-import { toastSuccess, toastError } from '~/client/util/toastr';
 import type { IPageGrantData } from '~/interfaces/page';
 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 {
   useSelectedGrant,
   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';
 
@@ -45,10 +40,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;
@@ -57,33 +49,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 });
@@ -113,7 +90,7 @@ const SavePageButton = (props: {slackChannels: string, isDeviceLargerThanMd?: bo
                 <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
                   {labelOverwriteScopes}
                 </DropdownItem>
-                <DropdownItem onClick={clickUnpublishButtonHandler}>
+                <DropdownItem onClick={saveAndMakeWip}>
                   {labelUnpublishPage}
                 </DropdownItem>
               </DropdownMenu>
@@ -130,7 +107,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

@@ -171,7 +171,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>

+ 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

@@ -34,6 +34,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;

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

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

+ 3 - 3
yarn.lock

@@ -16992,9 +16992,9 @@ tar-stream@^3.0.0:
     streamx "^2.15.0"
 
 tar@^6.1.11, tar@^6.1.2:
-  version "6.2.0"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73"
-  integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
+  integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
   dependencies:
     chownr "^2.0.0"
     fs-minipass "^2.0.0"