Jelajahi Sumber

Merge branch 'master' into fix/admin-customize-styles

Yuki Takei 2 tahun lalu
induk
melakukan
a5f0a65378
37 mengubah file dengan 140 tambahan dan 124 penghapusan
  1. 1 1
      apps/app/public/static/locales/zh_CN/translation.json
  2. 2 2
      apps/app/src/client/services/layout.ts
  3. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  4. 17 6
      apps/app/src/components/AuthorInfo/AuthorInfo.tsx
  5. 1 1
      apps/app/src/components/ContentLinkButtons.tsx
  6. 1 1
      apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  7. 2 2
      apps/app/src/components/PageAuthorInfo/PageAuthorInfo.tsx
  8. 5 7
      apps/app/src/components/PageContentFooter.tsx
  9. 5 4
      apps/app/src/components/PageControls/PageControls.tsx
  10. 2 2
      apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx
  11. 5 0
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.module.scss
  12. 4 3
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx
  13. 12 0
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  14. 1 0
      apps/app/src/components/PageEditor/PageEditor.tsx
  15. 1 1
      apps/app/src/components/PageHeader/PageHeader.tsx
  16. 1 1
      apps/app/src/components/PageHeader/PageTitleHeader.tsx
  17. 1 1
      apps/app/src/components/PageTags/TagsInput.tsx
  18. 5 4
      apps/app/src/components/User/Username.tsx
  19. 9 3
      apps/app/src/server/routes/apiv3/page-listing.ts
  20. 4 3
      apps/app/src/server/routes/apiv3/page/update-page.ts
  21. 2 4
      apps/app/src/server/service/mail.ts
  22. 14 4
      apps/app/src/server/service/page/index.ts
  23. 4 4
      apps/app/src/stores/remote-latest-page.ts
  24. 4 0
      apps/app/src/styles/_layout.scss
  25. 3 1
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts
  26. 2 2
      packages/core/src/interfaces/common.ts
  27. 4 8
      packages/core/src/interfaces/page.ts
  28. 2 1
      packages/core/src/interfaces/revision.ts
  29. 3 2
      packages/editor/src/components/CodeMirrorEditorMain.tsx
  30. 1 0
      packages/editor/src/components/playground/Playground.tsx
  31. 3 2
      packages/editor/src/services/list-util/insert-newline-continue-markup.ts
  32. 2 2
      packages/editor/src/services/table-util/insert-new-row-to-table-markdown.ts
  33. 15 15
      packages/editor/src/stores/use-collaborative-editor-mode.ts
  34. 0 6
      packages/ui/scss/molecules/_page_list.scss
  35. 0 27
      packages/ui/src/components/FootstampIcon.tsx
  36. 1 2
      packages/ui/src/components/PagePath/PageListMeta.tsx
  37. 0 1
      packages/ui/src/components/index.ts

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

@@ -27,7 +27,7 @@
   "Description": "描述",
   "Description": "描述",
   "Admin": "管理",
   "Admin": "管理",
   "administrator": "管理员",
   "administrator": "管理员",
-  "Tags": "Tags",
+  "Tags": "标签",
   "Close": "Close",
   "Close": "Close",
   "Shortcuts": "快捷方式",
   "Shortcuts": "快捷方式",
   "CustomSidebar": "Custom Sidebar",
   "CustomSidebar": "Custom Sidebar",

+ 2 - 2
apps/app/src/client/services/layout.ts

@@ -1,4 +1,4 @@
-import type { IPage } from '@growi/core';
+import type { IPage, IPagePopulatedToShowRevision } from '@growi/core';
 
 
 import { useIsContainerFluid } from '~/stores/context';
 import { useIsContainerFluid } from '~/stores/context';
 import { useEditorMode } from '~/stores/ui';
 import { useEditorMode } from '~/stores/ui';
@@ -16,7 +16,7 @@ const useDetermineExpandContent = (expandContentWidth?: boolean | null): boolean
   return expandContentWidth ?? isContainerFluidDefault ?? false;
   return expandContentWidth ?? isContainerFluidDefault ?? false;
 };
 };
 
 
-export const useShouldExpandContent = (data?: IPage | boolean | null): boolean => {
+export const useShouldExpandContent = (data?: IPage | IPagePopulatedToShowRevision | boolean | null): boolean => {
   const expandContentWidth = (() => {
   const expandContentWidth = (() => {
     // when data is null
     // when data is null
     if (data == null) {
     if (data == null) {

+ 1 - 1
apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -116,7 +116,7 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
             <button
             <button
               type="button"
               type="button"
               id="dropdownMenuButton"
               id="dropdownMenuButton"
-              data-toggle="dropdown"
+              data-bs-toggle="dropdown"
               className="btn btn-outline-secondary dropdown-toggle mb-3"
               className="btn btn-outline-secondary dropdown-toggle mb-3"
               disabled={isExternalGroup || !isSelectableParentUserGroups}
               disabled={isExternalGroup || !isSelectableParentUserGroups}
             >
             >

+ 17 - 6
apps/app/src/components/AuthorInfo/AuthorInfo.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 
 
-import type { IUser } from '@growi/core';
+import type { IUserHasId } from '@growi/core';
+import { isPopulated, type IUser, type Ref } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns/format';
 import { format } from 'date-fns/format';
@@ -10,10 +11,22 @@ import Link from 'next/link';
 
 
 import styles from './AuthorInfo.module.scss';
 import styles from './AuthorInfo.module.scss';
 
 
+const UserLabel = ({ user }: { user: IUserHasId | Ref<IUser> }): JSX.Element => {
+  if (isPopulated(user)) {
+    return (
+      <Link href={pagePathUtils.userHomepagePath(user)} prefetch={false}>
+        {user.name}
+      </Link>
+    );
+  }
+
+  return <i>(anyone)</i>;
+};
+
 
 
-export type AuthorInfoProps = {
+type AuthorInfoProps = {
   date: Date,
   date: Date,
-  user: IUser,
+  user?: IUserHasId | Ref<IUser>,
   mode: 'create' | 'update',
   mode: 'create' | 'update',
   locate: 'subnav' | 'footer',
   locate: 'subnav' | 'footer',
 }
 }
@@ -37,9 +50,7 @@ export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
     : t('author_info.last_revision_posted_at');
     : t('author_info.last_revision_posted_at');
   const userLabel = user != null
   const userLabel = user != null
     ? (
     ? (
-      <Link href={pagePathUtils.userHomepagePath(user)} prefetch={false}>
-        {user.name}
-      </Link>
+      <UserLabel user={user} />
     )
     )
     : <i>Unknown</i>;
     : <i>Unknown</i>;
 
 

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

@@ -40,7 +40,7 @@ RecentlyCreatedLinkButton.displayName = 'RecentlyCreatedLinkButton';
 
 
 
 
 export type ContentLinkButtonsProps = {
 export type ContentLinkButtonsProps = {
-  author: IUserHasId | null,
+  author?: IUserHasId,
 }
 }
 
 
 export const ContentLinkButtons = (props: ContentLinkButtonsProps): JSX.Element => {
 export const ContentLinkButtons = (props: ContentLinkButtonsProps): JSX.Element => {

+ 1 - 1
apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -76,7 +76,7 @@ export const DeleteAttachmentModal: React.FC = () => {
           <span className="material-symbols-outlined">{iconByFormat(attachment.fileFormat)}</span> {attachment.originalName}
           <span className="material-symbols-outlined">{iconByFormat(attachment.fileFormat)}</span> {attachment.originalName}
         </p>
         </p>
         <p>
         <p>
-          uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator as IUser}></Username>
+          uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator}></Username>
         </p>
         </p>
         {content}
         {content}
       </div>
       </div>

+ 2 - 2
apps/app/src/components/PageAuthorInfo/PageAuthorInfo.tsx

@@ -33,12 +33,12 @@ export const PageAuthorInfo = memo((): JSX.Element => {
     <ul className={`grw-page-author-info ${styles['grw-page-author-info']} text-nowrap border-start d-none d-lg-block d-edit-none py-2 ps-4 mb-0 ms-3`}>
     <ul className={`grw-page-author-info ${styles['grw-page-author-info']} text-nowrap border-start d-none d-lg-block d-edit-none py-2 ps-4 mb-0 ms-3`}>
       <li className="pb-1">
       <li className="pb-1">
         {currentPage != null && (
         {currentPage != null && (
-          <AuthorInfo user={currentPage.creator as IUser} date={currentPage.createdAt} mode="create" locate="subnav" />
+          <AuthorInfo user={currentPage.creator} date={currentPage.createdAt} mode="create" locate="subnav" />
         )}
         )}
       </li>
       </li>
       <li className="mt-1 pt-1 border-top">
       <li className="mt-1 pt-1 border-top">
         {currentPage != null && (
         {currentPage != null && (
-          <AuthorInfo user={currentPage.lastUpdateUser as IUser} date={currentPage.updatedAt} mode="update" locate="subnav" />
+          <AuthorInfo user={currentPage.lastUpdateUser} date={currentPage.updatedAt} mode="update" locate="subnav" />
         )}
         )}
       </li>
       </li>
     </ul>
     </ul>

+ 5 - 7
apps/app/src/components/PageContentFooter.tsx

@@ -1,16 +1,14 @@
 import React from 'react';
 import React from 'react';
 
 
-import type { IPage, IUser } from '@growi/core';
+import type { IPage, IPagePopulatedToShowRevision } from '@growi/core';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
-import type { AuthorInfoProps } from './AuthorInfo';
-
 import styles from './PageContentFooter.module.scss';
 import styles from './PageContentFooter.module.scss';
 
 
-const AuthorInfo = dynamic<AuthorInfoProps>(() => import('./AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false });
+const AuthorInfo = dynamic(() => import('./AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false });
 
 
 export type PageContentFooterProps = {
 export type PageContentFooterProps = {
-  page: IPage,
+  page: IPage | IPagePopulatedToShowRevision,
 }
 }
 
 
 export const PageContentFooter = (props: PageContentFooterProps): JSX.Element => {
 export const PageContentFooter = (props: PageContentFooterProps): JSX.Element => {
@@ -29,8 +27,8 @@ export const PageContentFooter = (props: PageContentFooterProps): JSX.Element =>
     <div className={`${styles['page-content-footer']} page-content-footer py-4 d-edit-none d-print-none}`}>
     <div className={`${styles['page-content-footer']} page-content-footer py-4 d-edit-none d-print-none}`}>
       <div className="container-lg grw-container-convertible">
       <div className="container-lg grw-container-convertible">
         <div className="page-meta">
         <div className="page-meta">
-          <AuthorInfo user={creator as IUser} date={createdAt} mode="create" locate="footer" />
-          <AuthorInfo user={lastUpdateUser as IUser} date={updatedAt} mode="update" locate="footer" />
+          <AuthorInfo user={creator} date={createdAt} mode="create" locate="footer" />
+          <AuthorInfo user={lastUpdateUser} date={updatedAt} mode="update" locate="footer" />
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>

+ 5 - 4
apps/app/src/components/PageControls/PageControls.tsx

@@ -49,6 +49,7 @@ type TagsProps = {
 
 
 const Tags = (props: TagsProps): JSX.Element => {
 const Tags = (props: TagsProps): JSX.Element => {
   const { onClickEditTagsButton } = props;
   const { onClickEditTagsButton } = props;
+  const { t } = useTranslation();
 
 
   return (
   return (
     <div className="grw-tag-labels-container d-flex align-items-center">
     <div className="grw-tag-labels-container d-flex align-items-center">
@@ -57,8 +58,8 @@ const Tags = (props: TagsProps): JSX.Element => {
         className="btn btn-sm btn-outline-neutral-secondary"
         className="btn btn-sm btn-outline-neutral-secondary"
         onClick={onClickEditTagsButton}
         onClick={onClickEditTagsButton}
       >
       >
-        <span className="material-symbols-outlined me-1">local_offer</span>
-        Tags
+        <span className="material-symbols-outlined">local_offer</span>
+        <span className="d-none d-sm-inline ms-1">{t('Tags')}</span>
       </button>
       </button>
     </div>
     </div>
   );
   );
@@ -273,7 +274,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
 
 
   return (
   return (
     <div className={`${styles['grw-page-controls']} hstack gap-2`} ref={pageControlsRef}>
     <div className={`${styles['grw-page-controls']} hstack gap-2`} ref={pageControlsRef}>
-      { isDeviceLargerThanMd && (
+      { isViewMode && isDeviceLargerThanMd && (
         <SearchButton />
         <SearchButton />
       )}
       )}
 
 
@@ -284,7 +285,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
       )}
       )}
 
 
       { !hideSubControls && (
       { !hideSubControls && (
-        <div className="hstack gap-1">
+        <div className={`hstack gap-1 ${!isViewMode && 'd-none d-lg-flex'}`}>
           {revisionId != null && _isIPageInfoForOperation && (
           {revisionId != null && _isIPageInfoForOperation && (
             <SubscribeButton
             <SubscribeButton
               status={pageInfo.subscriptionStatus}
               status={pageInfo.subscriptionStatus}

+ 2 - 2
apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx

@@ -27,8 +27,8 @@ export const EditingUserList: FC<Props> = ({ userList }) => {
   }
   }
 
 
   return (
   return (
-    <div className="d-flex flex-column justify-content-end">
-      <div className="d-flex justify-content-end">
+    <div className="d-flex flex-column justify-content-start justify-content-sm-end">
+      <div className="d-flex justify-content-start justify-content-sm-end">
         {firstFourUsers.map(user => (
         {firstFourUsers.map(user => (
           <div className="ms-1">
           <div className="ms-1">
             <UserPicture
             <UserPicture

+ 5 - 0
apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.module.scss

@@ -1,3 +1,8 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
 .editor-navbar :global {
 .editor-navbar :global {
   min-height: 72px;
   min-height: 72px;
+  @include bs.media-breakpoint-down(sm) {
+    min-height: 96px;
+  }
 }
 }

+ 4 - 3
apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx

@@ -12,11 +12,12 @@ export const EditorNavbar = (): JSX.Element => {
   const { data: editingUsers } = useEditingUsers();
   const { data: editingUsers } = useEditingUsers();
 
 
   return (
   return (
-    <div className={`${moduleClass} d-flex justify-content-between px-4 py-1`}>
-      <PageHeader />
-      <EditingUserList
+    <div className={`${moduleClass} d-flex flex-column flex-sm-row justify-content-between ps-3 ps-md-5 ps-xl-4 pe-4 py-1 align-items-sm-end`}>
+      <div className="order-2 order-sm-1"><PageHeader /></div>
+      <div className="order-1 order-sm-2"><EditingUserList
         userList={editingUsers?.userList ?? []}
         userList={editingUsers?.userList ?? []}
       />
       />
+      </div>
     </div>
     </div>
   );
   );
 };
 };

+ 12 - 0
apps/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -1,5 +1,7 @@
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
+import { useDrawerOpened } from '~/stores/ui';
+
 import styles from './EditorNavbarBottom.module.scss';
 import styles from './EditorNavbarBottom.module.scss';
 
 
 const moduleClass = styles['grw-editor-navbar-bottom'];
 const moduleClass = styles['grw-editor-navbar-bottom'];
@@ -8,9 +10,19 @@ const SavePageControls = dynamic(() => import('~/components/SavePageControls').t
 const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSelector').then(mod => mod.OptionsSelector), { ssr: false });
 const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSelector').then(mod => mod.OptionsSelector), { ssr: false });
 
 
 const EditorNavbarBottom = (): JSX.Element => {
 const EditorNavbarBottom = (): JSX.Element => {
+
+  const { mutate: mutateDrawerOpened } = useDrawerOpened();
+
   return (
   return (
     <div className="border-top" data-testid="grw-editor-navbar-bottom">
     <div className="border-top" data-testid="grw-editor-navbar-bottom">
       <div className={`flex-expand-horiz align-items-center p-2 ps-md-3 pe-md-4 ${moduleClass}`}>
       <div className={`flex-expand-horiz align-items-center p-2 ps-md-3 pe-md-4 ${moduleClass}`}>
+        <a
+          role="button"
+          className="nav-link btn-lg p-2 d-md-none me-3 opacity-50"
+          onClick={() => mutateDrawerOpened(true)}
+        >
+          <span className="material-symbols-outlined fs-2">reorder</span>
+        </a>
         <form className="me-auto">
         <form className="me-auto">
           <OptionsSelector />
           <OptionsSelector />
         </form>
         </form>

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

@@ -353,6 +353,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
       <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
         <div className="page-editor-editor-container flex-expand-vert border-end">
         <div className="page-editor-editor-container flex-expand-vert border-end">
           <CodeMirrorEditorMain
           <CodeMirrorEditorMain
+            isEditorMode={editorMode === EditorMode.Editor}
             onChange={markdownChangedHandler}
             onChange={markdownChangedHandler}
             onSave={saveWithShortcut}
             onSave={saveWithShortcut}
             onUpload={uploadHandler}
             onUpload={uploadHandler}

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

@@ -21,7 +21,7 @@ export const PageHeader: FC = () => {
       <PagePathHeader
       <PagePathHeader
         currentPage={currentPage}
         currentPage={currentPage}
       />
       />
-      <div className="mt-1">
+      <div className="mt-0 mt-md-1">
         <PageTitleHeader
         <PageTitleHeader
           currentPage={currentPage}
           currentPage={currentPage}
         />
         />

+ 1 - 1
apps/app/src/components/PageHeader/PageTitleHeader.tsx

@@ -110,7 +110,7 @@ export const PageTitleHeader: FC<Props> = (props) => {
           </div>
           </div>
         ) }
         ) }
         <h1
         <h1
-          className={`mb-0 px-2 fs-4
+          className={`mb-0 mb-sm-1 px-2 fs-4
             ${isRenameInputShown ? 'invisible' : ''} text-truncate
             ${isRenameInputShown ? 'invisible' : ''} text-truncate
             ${isMovable ? 'border border-2 rounded-2' : ''} ${borderColorClass}
             ${isMovable ? 'border border-2 rounded-2' : ''} ${borderColorClass}
           `}
           `}

+ 1 - 1
apps/app/src/components/PageTags/TagsInput.tsx

@@ -39,7 +39,7 @@ export const TagsInput: FC<Props> = (props: Props) => {
   }, [tagsSearch?.tags]);
   }, [tagsSearch?.tags]);
 
 
   const keyDownHandler = useCallback((event: KeyboardEvent<HTMLElement>) => {
   const keyDownHandler = useCallback((event: KeyboardEvent<HTMLElement>) => {
-    if (event.key === ' ') {
+    if (event.code === 'Space') {
       event.preventDefault();
       event.preventDefault();
 
 
       // fix: https://redmine.weseek.co.jp/issues/140689
       // fix: https://redmine.weseek.co.jp/issues/140689

+ 5 - 4
apps/app/src/components/User/Username.tsx

@@ -1,13 +1,14 @@
 import React from 'react';
 import React from 'react';
 
 
-import type { IUser } from '@growi/core';
+import type { IUserHasId } from '@growi/core';
+import { isPopulated, type IUser, type Ref } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import Link from 'next/link';
 import Link from 'next/link';
 
 
-export const Username: React.FC<{ user?: IUser }> = ({ user }): JSX.Element => {
+export const Username: React.FC<{ user?: IUserHasId | Ref<IUser> }> = ({ user }): JSX.Element => {
 
 
-  if (user == null) {
-    return <span>anyone</span>;
+  if (user == null || !isPopulated(user)) {
+    return <i>(anyone)</i>;
   }
   }
 
 
   const name = user.name || '(no name)';
   const name = user.name || '(no name)';

+ 9 - 3
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -1,11 +1,11 @@
 import type {
 import type {
   IPageInfoForListing, IPageInfo,
   IPageInfoForListing, IPageInfo,
 } from '@growi/core';
 } from '@growi/core';
-import { isIPageInfoForEntity } from '@growi/core';
+import { getIdForRef, isIPageInfoForEntity } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, Router } from 'express';
 import type { Request, Router } from 'express';
 import express from 'express';
 import express from 'express';
-import { query, oneOf, validationResult } from 'express-validator';
+import { query, oneOf } from 'express-validator';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
 
 
@@ -157,7 +157,13 @@ const routerFactory = (crowi: Crowi): Router => {
         const basicPageInfo = pageService.constructBasicPageInfo(page, isGuestUser);
         const basicPageInfo = pageService.constructBasicPageInfo(page, isGuestUser);
 
 
         // TODO: use pageService.getCreatorIdForCanDelete to get creatorId (https://redmine.weseek.co.jp/issues/140574)
         // TODO: use pageService.getCreatorIdForCanDelete to get creatorId (https://redmine.weseek.co.jp/issues/140574)
-        const canDeleteCompletely = pageService.canDeleteCompletely(page, page.creator, req.user, false, userRelatedGroups); // use normal delete config
+        const canDeleteCompletely = pageService.canDeleteCompletely(
+          page,
+          page.creator == null ? null : getIdForRef(page.creator),
+          req.user,
+          false,
+          userRelatedGroups,
+        ); // use normal delete config
 
 
         const pageInfo = (!isIPageInfoForEntity(basicPageInfo))
         const pageInfo = (!isIPageInfoForEntity(basicPageInfo))
           ? basicPageInfo
           ? basicPageInfo

+ 4 - 3
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -1,4 +1,4 @@
-import { Origin, allOrigin } from '@growi/core';
+import { Origin, allOrigin, getIdForRef } from '@growi/core';
 import type {
 import type {
   IPage, IRevisionHasId, IUserHasId,
   IPage, IRevisionHasId, IUserHasId,
 } from '@growi/core';
 } from '@growi/core';
@@ -89,6 +89,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     }
     }
 
 
     // persist activity
     // persist activity
+    const creator = updatedPage.creator != null ? getIdForRef(updatedPage.creator) : undefined;
     const parameters = {
     const parameters = {
       targetModel: SupportedTargetModel.MODEL_PAGE,
       targetModel: SupportedTargetModel.MODEL_PAGE,
       target: updatedPage,
       target: updatedPage,
@@ -97,7 +98,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     const activityEvent = crowi.event('activity');
     const activityEvent = crowi.event('activity');
     activityEvent.emit(
     activityEvent.emit(
       'update', res.locals.activity._id, parameters,
       'update', res.locals.activity._id, parameters,
-      { path: updatedPage.path, creator: updatedPage.creator._id.toString() },
+      { path: updatedPage.path, creator },
       preNotifyService.generatePreNotify,
       preNotifyService.generatePreNotify,
     );
     );
 
 
@@ -157,7 +158,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
         return res.apiv3Err(new ErrorV3('Posted param "revisionId" is outdated.', PageUpdateErrorCode.CONFLICT, undefined, { returnLatestRevision }), 409);
         return res.apiv3Err(new ErrorV3('Posted param "revisionId" is outdated.', PageUpdateErrorCode.CONFLICT, undefined, { returnLatestRevision }), 409);
       }
       }
 
 
-      let updatedPage;
+      let updatedPage: PageDocument;
       try {
       try {
         const {
         const {
           grant, userRelatedGrantUserGroupIds, overwriteScopesOfDescendants, wip,
           grant, userRelatedGrantUserGroupIds, overwriteScopesOfDescendants, wip,

+ 2 - 4
apps/app/src/server/service/mail.ts

@@ -7,7 +7,7 @@ import loggerFactory from '~/utils/logger';
 
 
 import S2sMessage from '../models/vo/s2s-message';
 import S2sMessage from '../models/vo/s2s-message';
 
 
-import { S2sMessageHandlable } from './s2s-messaging/handlable';
+import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 
 
 const logger = loggerFactory('growi:service:mail');
 const logger = loggerFactory('growi:service:mail');
 
 
@@ -194,13 +194,11 @@ class MailService implements S2sMessageHandlable {
       throw new Error('Mailer is not completed to set up. Please set up SMTP or AWS setting.');
       throw new Error('Mailer is not completed to set up. Please set up SMTP or AWS setting.');
     }
     }
 
 
-    const renderFilePromisified = promisify(ejs.renderFile);
+    const renderFilePromisified = promisify<string, ejs.Data, string>(ejs.renderFile);
 
 
     const templateVars = config.vars || {};
     const templateVars = config.vars || {};
     const output = await renderFilePromisified(
     const output = await renderFilePromisified(
       config.template,
       config.template,
-      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-      // @ts-ignore
       templateVars,
       templateVars,
     );
     );
 
 

+ 14 - 4
apps/app/src/server/service/page/index.ts

@@ -263,10 +263,14 @@ class PageService implements IPageService {
     if (page.isEmpty) {
     if (page.isEmpty) {
       const Page = mongoose.model<IPage, PageModel>('Page');
       const Page = mongoose.model<IPage, PageModel>('Page');
       const notEmptyClosestAncestor = await Page.findNonEmptyClosestAncestor(page.path);
       const notEmptyClosestAncestor = await Page.findNonEmptyClosestAncestor(page.path);
-      return notEmptyClosestAncestor?.creator ?? null;
+      return notEmptyClosestAncestor?.creator == null
+        ? null
+        : getIdForRef(notEmptyClosestAncestor.creator);
     }
     }
 
 
-    return page.creator ?? null;
+    return page.creator == null
+      ? null
+      : getIdForRef(page.creator);
   }
   }
 
 
   // Use getCreatorIdForCanDelete before execution of canDelete to get creatorId.
   // Use getCreatorIdForCanDelete before execution of canDelete to get creatorId.
@@ -351,13 +355,19 @@ class PageService implements IPageService {
       user: IUserHasId,
       user: IUserHasId,
       isRecursively: boolean,
       isRecursively: boolean,
       canDeleteFunction: (
       canDeleteFunction: (
-        page: PageDocument, creatorId: ObjectIdLike, operator: any, isRecursively: boolean, userRelatedGroups: PopulatedGrantedGroup[]
+        page: PageDocument, creatorId: ObjectIdLike | null, operator: any, isRecursively: boolean, userRelatedGroups: PopulatedGrantedGroup[]
       ) => boolean,
       ) => boolean,
   ): Promise<PageDocument[]> {
   ): Promise<PageDocument[]> {
     const userRelatedGroups = await this.pageGrantService.getUserRelatedGroups(user);
     const userRelatedGroups = await this.pageGrantService.getUserRelatedGroups(user);
     const filteredPages = pages.filter(async(p) => {
     const filteredPages = pages.filter(async(p) => {
       if (p.isEmpty) return true;
       if (p.isEmpty) return true;
-      const canDelete = canDeleteFunction(p, p.creator, user, isRecursively, userRelatedGroups);
+      const canDelete = canDeleteFunction(
+        p,
+        p.creator == null ? null : getIdForRef(p.creator),
+        user,
+        isRecursively,
+        userRelatedGroups,
+      );
       return canDelete;
       return canDelete;
     });
     });
 
 

+ 4 - 4
apps/app/src/stores/remote-latest-page.ts

@@ -1,6 +1,6 @@
 import { useMemo, useCallback } from 'react';
 import { useMemo, useCallback } from 'react';
 
 
-import type { IUser } from '@growi/core';
+import type { IUserHasId } from '@growi/core';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 
 
@@ -13,8 +13,8 @@ export const useRemoteRevisionBody = (initialData?: string): SWRResponse<string,
   return useSWRStatic<string, Error>('remoteRevisionBody', initialData);
   return useSWRStatic<string, Error>('remoteRevisionBody', initialData);
 };
 };
 
 
-export const useRemoteRevisionLastUpdateUser = (initialData?: IUser): SWRResponse<IUser, Error> => {
-  return useSWRStatic<IUser, Error>('remoteRevisionLastUpdateUser', initialData);
+export const useRemoteRevisionLastUpdateUser = (initialData?: IUserHasId): SWRResponse<IUserHasId, Error> => {
+  return useSWRStatic<IUserHasId, Error>('remoteRevisionLastUpdateUser', initialData);
 };
 };
 
 
 export const useRemoteRevisionLastUpdatedAt = (initialData?: Date): SWRResponse<Date, Error> => {
 export const useRemoteRevisionLastUpdatedAt = (initialData?: Date): SWRResponse<Date, Error> => {
@@ -24,7 +24,7 @@ export const useRemoteRevisionLastUpdatedAt = (initialData?: Date): SWRResponse<
 export type RemoteRevisionData = {
 export type RemoteRevisionData = {
   remoteRevisionId: string,
   remoteRevisionId: string,
   remoteRevisionBody: string,
   remoteRevisionBody: string,
-  remoteRevisionLastUpdateUser: IUser,
+  remoteRevisionLastUpdateUser?: IUserHasId,
   remoteRevisionLastUpdatedAt: Date,
   remoteRevisionLastUpdatedAt: Date,
 }
 }
 
 

+ 4 - 0
apps/app/src/styles/_layout.scss

@@ -2,6 +2,10 @@
 
 
 @use './variables' as var;
 @use './variables' as var;
 
 
+body {
+  min-height: 100vh;
+}
+
 .dynamic-layout-root {
 .dynamic-layout-root {
   @extend .flex-expand-vert;
   @extend .flex-expand-vert;
 }
 }

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

@@ -270,7 +270,9 @@ describe('Access to sidebar', () => {
 
 
         it('Succesfully click all tags button', () => {
         it('Succesfully click all tags button', () => {
           cy.getByTestid('grw-sidebar-content-tags').within(() => {
           cy.getByTestid('grw-sidebar-content-tags').within(() => {
-            cy.get('.btn-primary').click();
+            cy.get('.btn-primary').as('check-all-tags-button');
+            cy.get('@check-all-tags-button').should('be.visible');
+            cy.get('@check-all-tags-button').click({force: true});
           });
           });
           cy.collapseSidebar(true);
           cy.collapseSidebar(true);
           cy.getByTestid('grw-tags-list').should('be.visible');
           cy.getByTestid('grw-tags-list').should('be.visible');

+ 2 - 2
packages/core/src/interfaces/common.ts

@@ -10,11 +10,11 @@ export type Ref<T> = string | T & HasObjectId;
 
 
 export type Nullable<T> = T | null | undefined;
 export type Nullable<T> = T | null | undefined;
 
 
-export const isPopulated = <T>(ref: Ref<T>): ref is T & HasObjectId => {
+export const isPopulated = <T>(ref: T & HasObjectId | Ref<T>): ref is T & HasObjectId => {
   return !(typeof ref === 'string');
   return !(typeof ref === 'string');
 };
 };
 
 
-export const getIdForRef = <T>(ref: Ref<T>): string => {
+export const getIdForRef = <T>(ref: T & HasObjectId | Ref<T>): string => {
   return isPopulated(ref)
   return isPopulated(ref)
     ? ref._id
     ? ref._id
     : ref;
     : ref;

+ 4 - 8
packages/core/src/interfaces/page.ts

@@ -20,7 +20,7 @@ export type IPage = {
   status: string,
   status: string,
   revision?: Ref<IRevision>,
   revision?: Ref<IRevision>,
   tags: Ref<ITag>[],
   tags: Ref<ITag>[],
-  creator: any,
+  creator?: Ref<IUser>,
   createdAt: Date,
   createdAt: Date,
   updatedAt: Date,
   updatedAt: Date,
   seenUsers: Ref<IUser>[],
   seenUsers: Ref<IUser>[],
@@ -30,7 +30,7 @@ export type IPage = {
   grant: PageGrant,
   grant: PageGrant,
   grantedUsers: Ref<IUser>[],
   grantedUsers: Ref<IUser>[],
   grantedGroups: IGrantedGroup[],
   grantedGroups: IGrantedGroup[],
-  lastUpdateUser: Ref<IUser>,
+  lastUpdateUser?: Ref<IUser>,
   liker: Ref<IUser>[],
   liker: Ref<IUser>[],
   commentCount: number
   commentCount: number
   slackChannels: string,
   slackChannels: string,
@@ -43,13 +43,9 @@ export type IPage = {
   ttlTimestamp?: Date
   ttlTimestamp?: Date
 }
 }
 
 
-export type IPagePopulatedToList = Omit<IPageHasId, 'lastUpdateUser'> & {
-  lastUpdateUser: IUserHasId,
-}
-
 export type IPagePopulatedToShowRevision = Omit<IPageHasId, 'lastUpdateUser'|'creator'|'deleteUser'|'grantedGroups'|'revision'|'author'> & {
 export type IPagePopulatedToShowRevision = Omit<IPageHasId, 'lastUpdateUser'|'creator'|'deleteUser'|'grantedGroups'|'revision'|'author'> & {
-  lastUpdateUser: IUserHasId,
-  creator: IUserHasId | null,
+  lastUpdateUser?: IUserHasId,
+  creator?: IUserHasId,
   deleteUser: IUserHasId,
   deleteUser: IUserHasId,
   grantedGroups: { type: GroupType, item: IUserGroupHasId }[],
   grantedGroups: { type: GroupType, item: IUserGroupHasId }[],
   revision?: IRevisionHasId,
   revision?: IRevisionHasId,

+ 2 - 1
packages/core/src/interfaces/revision.ts

@@ -1,3 +1,4 @@
+import type { Ref } from './common';
 import type { HasObjectId } from './has-object-id';
 import type { HasObjectId } from './has-object-id';
 import type { IUser } from './user';
 import type { IUser } from './user';
 
 
@@ -12,7 +13,7 @@ export const allOrigin = Object.values(Origin);
 
 
 export type IRevision = {
 export type IRevision = {
   body: string,
   body: string,
-  author: IUser,
+  author: Ref<IUser>,
   hasDiffToPrev: boolean;
   hasDiffToPrev: boolean;
   createdAt: Date,
   createdAt: Date,
   updatedAt: Date,
   updatedAt: Date,

+ 3 - 2
packages/editor/src/components/CodeMirrorEditorMain.tsx

@@ -21,18 +21,19 @@ type Props = CodeMirrorEditorProps & {
   user?: IUserHasId,
   user?: IUserHasId,
   pageId?: string,
   pageId?: string,
   initialValue?: string,
   initialValue?: string,
+  isEditorMode: boolean,
   onEditorsUpdated?: (userList: IUserHasId[]) => void,
   onEditorsUpdated?: (userList: IUserHasId[]) => void,
 }
 }
 
 
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
   const {
   const {
-    user, pageId, initialValue,
+    user, pageId, initialValue, isEditorMode,
     onSave, onEditorsUpdated, ...otherProps
     onSave, onEditorsUpdated, ...otherProps
   } = props;
   } = props;
 
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
 
-  useCollaborativeEditorMode(user, pageId, initialValue, onEditorsUpdated, codeMirrorEditor);
+  useCollaborativeEditorMode(isEditorMode, user, pageId, initialValue, onEditorsUpdated, codeMirrorEditor);
 
 
   // setup additional extensions
   // setup additional extensions
   useEffect(() => {
   useEffect(() => {

+ 1 - 0
packages/editor/src/components/playground/Playground.tsx

@@ -70,6 +70,7 @@ export const Playground = (): JSX.Element => {
       <div className="flex-expand-horiz">
       <div className="flex-expand-horiz">
         <div className="flex-expand-vert">
         <div className="flex-expand-vert">
           <CodeMirrorEditorMain
           <CodeMirrorEditorMain
+            isEditorMode
             onSave={saveHandler}
             onSave={saveHandler}
             onChange={setMarkdownToPreview}
             onChange={setMarkdownToPreview}
             onUpload={uploadHandler}
             onUpload={uploadHandler}

+ 3 - 2
packages/editor/src/services/list-util/insert-newline-continue-markup.ts

@@ -1,8 +1,9 @@
 import type { ChangeSpec } from '@codemirror/state';
 import type { ChangeSpec } from '@codemirror/state';
 import { EditorView } from '@codemirror/view';
 import { EditorView } from '@codemirror/view';
 
 
-// https://regex101.com/r/7BN2fR/5
-const indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
+// https://regex101.com/r/r9plEA/1
+const indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]\s))(\s*)/;
+// https://regex101.com/r/HFYoFN/1
 const indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
 const indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
 
 
 export const insertNewlineContinueMarkup = (editor: EditorView): void => {
 export const insertNewlineContinueMarkup = (editor: EditorView): void => {

+ 2 - 2
packages/editor/src/services/table-util/insert-new-row-to-table-markdown.ts

@@ -130,8 +130,6 @@ const removeRow = (editor: EditorView) => {
   const bolPos = editor.state.doc.line(curLine).from;
   const bolPos = editor.state.doc.line(curLine).from;
   const eolPos = editor.state.doc.line(curLine).to;
   const eolPos = editor.state.doc.line(curLine).to;
 
 
-  const nextCurPos = editor.state.doc.lineAt(getCurPos(editor)).to + 1;
-
   editor.dispatch({
   editor.dispatch({
     changes: {
     changes: {
       from: bolPos,
       from: bolPos,
@@ -139,6 +137,8 @@ const removeRow = (editor: EditorView) => {
     },
     },
   });
   });
 
 
+  const nextCurPos = editor.state.doc.lineAt(getCurPos(editor)).to + 1;
+
   editor.dispatch({
   editor.dispatch({
     selection: { anchor: nextCurPos },
     selection: { anchor: nextCurPos },
   });
   });

+ 15 - 15
packages/editor/src/stores/use-collaborative-editor-mode.ts

@@ -18,6 +18,7 @@ type UserLocalState = {
 }
 }
 
 
 export const useCollaborativeEditorMode = (
 export const useCollaborativeEditorMode = (
+    isEnabled: boolean,
     user?: IUserHasId,
     user?: IUserHasId,
     pageId?: string,
     pageId?: string,
     initialValue?: string,
     initialValue?: string,
@@ -30,8 +31,9 @@ export const useCollaborativeEditorMode = (
 
 
   const { data: socket } = useGlobalSocket();
   const { data: socket } = useGlobalSocket();
 
 
-  const cleanupYDoc = () => {
-    if (cPageId === pageId) {
+  // Cleanup Ydoc
+  useEffect(() => {
+    if (cPageId === pageId && isEnabled) {
       return;
       return;
     }
     }
 
 
@@ -49,10 +51,11 @@ export const useCollaborativeEditorMode = (
 
 
     // reset editors
     // reset editors
     onEditorsUpdated?.([]);
     onEditorsUpdated?.([]);
-  };
+  }, [cPageId, isEnabled, onEditorsUpdated, pageId, provider?.awareness, socket, ydoc]);
 
 
-  const setupYDoc = () => {
-    if (ydoc != null) {
+  // Setup Ydoc
+  useEffect(() => {
+    if (ydoc != null || !isEnabled) {
       return;
       return;
     }
     }
 
 
@@ -63,9 +66,10 @@ export const useCollaborativeEditorMode = (
 
 
     const _ydoc = new Y.Doc();
     const _ydoc = new Y.Doc();
     setYdoc(_ydoc);
     setYdoc(_ydoc);
-  };
+  }, [isEnabled, provider, ydoc]);
 
 
-  const setupProvider = () => {
+  // Setup provider
+  useEffect(() => {
     if (provider != null || ydoc == null || socket == null || onEditorsUpdated == null) {
     if (provider != null || ydoc == null || socket == null || onEditorsUpdated == null) {
       return;
       return;
     }
     }
@@ -104,9 +108,10 @@ export const useCollaborativeEditorMode = (
     });
     });
 
 
     setProvider(socketIOProvider);
     setProvider(socketIOProvider);
-  };
+  }, [initialValue, onEditorsUpdated, pageId, provider, socket, user, ydoc]);
 
 
-  const setupYDocExtensions = () => {
+  // Setup Ydoc Extensions
+  useEffect(() => {
     if (ydoc == null || provider == null || codeMirrorEditor == null) {
     if (ydoc == null || provider == null || codeMirrorEditor == null) {
       return;
       return;
     }
     }
@@ -127,10 +132,5 @@ export const useCollaborativeEditorMode = (
       cleanupYUndoManagerKeymap?.();
       cleanupYUndoManagerKeymap?.();
       cleanupYCollab?.();
       cleanupYCollab?.();
     };
     };
-  };
-
-  useEffect(cleanupYDoc, [cPageId, onEditorsUpdated, pageId, provider, socket, ydoc]);
-  useEffect(setupYDoc, [provider, ydoc]);
-  useEffect(setupProvider, [initialValue, onEditorsUpdated, pageId, provider, socket, user, ydoc]);
-  useEffect(setupYDocExtensions, [codeMirrorEditor, provider, ydoc]);
+  }, [codeMirrorEditor, provider, ydoc]);
 };
 };

+ 0 - 6
packages/ui/scss/molecules/_page_list.scss

@@ -44,12 +44,6 @@
         margin-right: 2px;
         margin-right: 2px;
       }
       }
 
 
-      .footstamp-icon {
-        svg {
-          width: 14px;
-          height: 14px;
-        }
-      }
       .seen-users-count {
       .seen-users-count {
         &.strength-0,
         &.strength-0,
         &.strength-1,
         &.strength-1,

+ 0 - 27
packages/ui/src/components/FootstampIcon.tsx

@@ -1,27 +0,0 @@
-export const FootstampIcon = (): JSX.Element => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    width={16}
-    height={16}
-    viewBox="0 0 16 16"
-  >
-    <path d="M7.34,8,3.31,9a1.83,1.83,0,0,1-1.24-.08A1.28,1.28,0,0,1,1.34,8a3.24,3.24,0,0,1,.2-1.82A6.06,6.06,0,0,1,2.6,4.35h0a2.56,
-    2.56,0,0,1,3.34-.77A5.65,5.65,0,0,1,7.69,4.73a3.23,3.23,0,0,1,1,1.53A1.29,1.29,0,0,1,8.42,7.4,1.86,1.86,0,0,1,7.34,8Zm-3-3.82a2.17,2.17,0,0,
-    0-1.05.74h0a4.75,4.75,0,0,0-.89,1.52,2.37,2.37,0,0,0-.17,1.3.38.38,0,0,0,.23.31,1,1,0,0,0,.65,
-      0l4-.94a1,1,0,0,0,.58-.3.39.39,0,0,0,.07-.38,2.32,2.32,0,0,0-.73-1.08,4.7,4.7,0,0,0-1.47-1A2.07,2.07,0,0,0,4.33,4.2Z"
-    />
-    <path d="M7.26,1.39a.57.57,0,0,0-.18,0,.81.81,0,0,0-.61,1l.09.38a.81.81,0,0,0,.79.63l.19,0a.82.82,0,0,0,.6-1L8.05,2a.81.81,0,0,0-.79-.63Z" />
-    <path d="M.81,2.9a.55.55,0,0,0-.18,0h0a.81.81,0,0,0-.61,1l.09.38A.81.81,0,0,0,.9,4.9l.18,0h0a.82.82,0,0,0,.61-1L1.6,3.52A.8.8,0,0,0,.81,2.9Z" />
-    <path d="M2.29.61a.57.57,0,0,0-.18,0,.81.81,0,0,0-.61,1l.16.7a.81.81,0,0,0,.79.63l.19,0h0a.8.8,0,0,0,.6-1l-.16-.71A.82.82,0,0,0,2.29.61Z" />
-    <path d="M4.93,0,4.75,0a.82.82,0,0,0-.61,1l.16.7a.82.82,0,0,0,.79.63l.19,0h0a.82.82,0,0,0,.61-1L5.72.63A.81.81,0,0,0,4.93,0Z" />
-    <path d="M13.22,16l-4.1-.54A1.88,1.88,0,0,1,8,14.94a1.34,1.34,0,0,1-.36-1.12,3.19,3.19,0,0,1,.83-1.62,5.73,5.73,0,0,1,1.62-1.32h0a2.57,2.57,
-    0,0,1,3.4.44A5.82,5.82,0,0,1,14.7,13a3.21,3.21,0,0,1,.38,1.78,1.28,1.28,0,0,1-.63,1A1.94,1.94,0,0,1,13.22,16Zm-1.48-4.64a2.12,2.12,0,0,
-    0-1.24.33h0a5.07,5.07,0,0,0-1.37,1.11,2.41,2.41,0,0,0-.62,1.16.43.43,0,0,0,.11.37,1.08,1.08,0,0,0,.61.24l4.11.53A1,1,0,0,0,14,15a.41.41,0,0,
-    0,.2-.33,2.47,2.47,0,0,0-.3-1.28,5,5,0,0,0-1-1.42A2.12,2.12,0,0,0,11.74,11.34Z"
-    />
-    <path d="M15.19,9.69a.82.82,0,0,0-.81.71l-.05.39a.82.82,0,0,0,.7.91h.11a.81.81,0,0,0,.8-.7l.05-.39a.8.8,0,0,0-.7-.91Z" />
-    <path d="M8.62,8.84a.82.82,0,0,0-.81.7l0,.39a.82.82,0,0,0,.7.91h.11a.81.81,0,0,0,.8-.71l.06-.39a.82.82,0,0,0-.7-.91Z" />
-    <path d="M10.8,7.22a.81.81,0,0,0-.8.7l-.09.72a.81.81,0,0,0,.7.91h.1a.83.83,0,0,0,.81-.71l.09-.72a.82.82,0,0,0-.7-.91Z" />
-    <path d="M13.49,7.57a.81.81,0,0,0-.8.71l-.1.71a.82.82,0,0,0,.7.91h.11a.81.81,0,0,0,.8-.71l.1-.71a.81.81,0,0,0-.7-.91Z" />
-  </svg>
-);

+ 1 - 2
packages/ui/src/components/PagePath/PageListMeta.tsx

@@ -5,7 +5,6 @@ import assert from 'assert';
 import type { IPageHasId } from '@growi/core';
 import type { IPageHasId } from '@growi/core';
 import { templateChecker, pagePathUtils } from '@growi/core/dist/utils';
 import { templateChecker, pagePathUtils } from '@growi/core/dist/utils';
 
 
-import { FootstampIcon } from '../FootstampIcon';
 
 
 const { isTopPage } = pagePathUtils;
 const { isTopPage } = pagePathUtils;
 const { checkTemplatePath } = templateChecker;
 const { checkTemplatePath } = templateChecker;
@@ -47,7 +46,7 @@ const SeenUsersCount = (props: SeenUsersCountProps): JSX.Element => {
 
 
   return (
   return (
     <span className={`seen-users-count ${shouldSpaceOutIcon ? 'me-2' : ''} ${strengthClass}`}>
     <span className={`seen-users-count ${shouldSpaceOutIcon ? 'me-2' : ''} ${strengthClass}`}>
-      <i className="footstamp-icon"><FootstampIcon /></i>
+      <span className="material-symbols-outlined">footprint</span>
       {count}
       {count}
     </span>
     </span>
   );
   );

+ 0 - 1
packages/ui/src/components/index.ts

@@ -1,5 +1,4 @@
 export * from './Attachment';
 export * from './Attachment';
-export * from './FootstampIcon';
 export * from './PagePath';
 export * from './PagePath';
 export * from './LoadingSpinner';
 export * from './LoadingSpinner';
 export * from './UserPicture';
 export * from './UserPicture';