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

Merge branch 'dev/7.0.x' into fix/141467-theme-select-style

satof3 2 лет назад
Родитель
Сommit
56eb90b3c1
68 измененных файлов с 393 добавлено и 273 удалено
  1. 1 1
      apps/app/package.json
  2. 1 1
      apps/app/src/client/models/MarkdownTable.js
  3. 18 0
      apps/app/src/client/services/use-toastr-on-error.tsx
  4. 2 2
      apps/app/src/components/Admin/AdminHome/AdminHome.jsx
  5. 1 1
      apps/app/src/components/Admin/App/AppSettingsPageContents.tsx
  6. 2 2
      apps/app/src/components/Admin/App/MaskedInput.tsx
  7. 4 3
      apps/app/src/components/Admin/AuditLog/ActivityTable.tsx
  8. 3 2
      apps/app/src/components/Admin/AuditLog/AuditLogSettings.tsx
  9. 3 2
      apps/app/src/components/Admin/AuditLog/DateRangePicker.tsx
  10. 5 3
      apps/app/src/components/Admin/AuditLog/SelectActionDropdown.tsx
  11. 1 1
      apps/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  12. 1 1
      apps/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  13. 2 2
      apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  14. 2 2
      apps/app/src/components/Admin/G2GDataTransferExportForm.tsx
  15. 4 4
      apps/app/src/components/Admin/G2GDataTransferStatusIcon.tsx
  16. 2 2
      apps/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  17. 7 2
      apps/app/src/components/Admin/Notification/NotificationTypeIcon.tsx
  18. 1 1
      apps/app/src/components/Admin/Notification/UserTriggerNotification.jsx
  19. 2 2
      apps/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  20. 4 4
      apps/app/src/components/Admin/Security/SecurityManagementContents.jsx
  21. 1 1
      apps/app/src/components/Admin/Security/SecuritySetting.jsx
  22. 4 4
      apps/app/src/components/Admin/SlackIntegration/Bridge.jsx
  23. 3 3
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  24. 2 2
      apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  25. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  26. 8 4
      apps/app/src/components/Admin/Users/SortIcons.tsx
  27. 2 2
      apps/app/src/components/Admin/Users/UserMenu.module.scss
  28. 5 2
      apps/app/src/components/Admin/Users/UserMenu.tsx
  29. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderItemControl.tsx
  30. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderMenu.tsx
  31. 1 1
      apps/app/src/components/Bookmarks/BookmarkMoveToRootBtn.tsx
  32. 3 2
      apps/app/src/components/Common/CustomCopyToClipBoard.tsx
  33. 1 1
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  34. 2 2
      apps/app/src/components/Me/AssociateModal.tsx
  35. 3 3
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  36. 2 3
      apps/app/src/components/PageControls/BookmarkButtons.tsx
  37. 4 2
      apps/app/src/components/PageControls/LikeButtons.tsx
  38. 2 1
      apps/app/src/components/PageControls/SeenUserInfo.tsx
  39. 3 10
      apps/app/src/components/PageControls/user-list-popover.module.scss
  40. 54 90
      apps/app/src/components/PageCreateModal.tsx
  41. 1 1
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  42. 1 1
      apps/app/src/components/PageEditor/HandsontableModal.tsx
  43. 2 2
      apps/app/src/components/PageEditor/LinkEditModal.tsx
  44. 9 7
      apps/app/src/components/PageEditor/PageEditor.tsx
  45. 53 0
      apps/app/src/components/PageHeader/EditingUserList.tsx
  46. 17 4
      apps/app/src/components/PageHeader/PageHeader.tsx
  47. 5 0
      apps/app/src/components/PageHeader/user-list-popover.module.scss
  48. 1 1
      apps/app/src/components/PageManagement/ApiErrorMessage.jsx
  49. 1 1
      apps/app/src/components/PageRenameModal.tsx
  50. 2 17
      apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx
  51. 1 1
      apps/app/src/components/Sidebar/Sidebar.tsx
  52. 1 1
      apps/app/src/components/SystemVersion.tsx
  53. 8 5
      apps/app/src/components/TreeItem/SimpleItem.tsx
  54. 1 1
      apps/app/src/components/UsersHomepageFooter.tsx
  55. 2 2
      apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx
  56. 2 1
      apps/app/src/pages/trash.page.tsx
  57. 33 0
      apps/app/src/stores/use-editing-users.ts
  58. 6 4
      packages/editor/src/components/CodeMirrorEditorMain.tsx
  59. 1 4
      packages/editor/src/services/file-dropzone/use-file-dropzone/use-file-dropzone.ts
  60. 36 11
      packages/editor/src/stores/use-collaborative-editor-mode.ts
  61. 2 2
      packages/remark-attachment-refs/src/client/components/AttachmentList.tsx
  62. 2 2
      packages/remark-lsx/src/client/components/Lsx.tsx
  63. 1 1
      packages/remark-lsx/src/client/components/LsxPageList/LsxListView.tsx
  64. 12 0
      packages/ui/scss/molecules/_user-list-popover.scss
  65. 7 2
      packages/ui/src/components/PagePath/PageListMeta.tsx
  66. 5 1
      packages/ui/src/components/UserPicture.tsx
  67. 10 9
      packages/ui/src/utils/use-rect.ts
  68. 2 18
      yarn.lock

+ 1 - 1
apps/app/package.json

@@ -268,7 +268,7 @@
     "pretty-bytes": "^6.1.1",
     "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
-    "react-dropzone": "^11.2.4",
+    "react-dropzone": "^14.2.3",
     "react-hotkeys": "^2.0.0",
     "react-input-autosize": "^3.0.0",
     "rehype-rewrite": "^3.0.6",

+ 1 - 1
apps/app/src/client/models/MarkdownTable.js

@@ -1,5 +1,5 @@
 import csvToMarkdown from 'csv-to-markdown-table';
-import markdownTable from 'markdown-table';
+import { markdownTable } from 'markdown-table';
 import stringWidth from 'string-width';
 
 // https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83

+ 18 - 0
apps/app/src/client/services/use-toastr-on-error.tsx

@@ -0,0 +1,18 @@
+import { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastError } from '~/client/util/toastr';
+
+export const useToastrOnError = <P, R>(method?: (param?: P) => Promise<R|undefined>): (param?: P) => Promise<R|undefined> => {
+  const { t } = useTranslation('commons');
+
+  return useCallback(async(param) => {
+    try {
+      return await method?.(param);
+    }
+    catch (err) {
+      toastError(t('toaster.create_failed', { target: 'a page' }));
+    }
+  }, [method, t]);
+};

+ 2 - 2
apps/app/src/components/Admin/AdminHome/AdminHome.jsx

@@ -52,7 +52,7 @@ const AdminHome = (props) => {
             </p>
             <hr />
             <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
-              <i className="fa fa-link ms-1" aria-hidden="true"></i>
+              <span className="material-symbols-outlined ms-1" aria-hidden="true">link</span>
               <strong>{t('admin:maintenance_mode.end_maintenance_mode')}</strong>
             </a>
           </div>
@@ -65,7 +65,7 @@ const AdminHome = (props) => {
           <div className={`alert ${migrationStatus.isV5Compatible == null ? 'alert-warning' : 'alert-info'}`}>
             {t('admin:v5_page_migration.migration_desc')}
             <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
-              <i className="fa fa-link ms-1" aria-hidden="true"></i>
+              <span className="material-symbols-outlined ms-1" aria-hidden="true">link</span>
               <strong>{t('admin:v5_page_migration.upgrade_to_v5')}</strong>
             </a>
           </div>

+ 1 - 1
apps/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -62,7 +62,7 @@ const AppSettingsPageContents = (props: Props) => {
             </p>
             <hr />
             <a className="btn-link" href="#maintenance-mode" rel="noopener noreferrer">
-              <i className="fa fa-fw fa-arrow-down ms-1" aria-hidden="true"></i>
+              <span className="material-symbols-outlined ms-1" aria-hidden="true">expand_more</span>
               <strong>{t('admin:maintenance_mode.end_maintenance_mode')}</strong>
             </a>
           </div>

+ 2 - 2
apps/app/src/components/Admin/App/MaskedInput.tsx

@@ -33,9 +33,9 @@ export default function MaskedInput(props: Props): JSX.Element {
       />
       <span onClick={togglePassword} className={styles.PasswordReveal}>
         {passwordShown ? (
-          <i className="fa fa-eye" />
+          <span className="material-symbols-outlined">visibility</span>
         ) : (
-          <i className="fa fa-eye-slash" />
+          <span className="material-symbols-outlined">visibility_off</span>
         )}
       </span>
     </div>

+ 4 - 3
apps/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
@@ -7,7 +8,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { useTranslation } from 'react-i18next';
 import { Tooltip } from 'reactstrap';
 
-import { IActivityHasId } from '~/interfaces/activity';
+import type { IActivityHasId } from '~/interfaces/activity';
 
 type Props = {
   activityList: IActivityHasId[]
@@ -64,7 +65,7 @@ export const ActivityTable : FC<Props> = (props: Props) => {
                   {activity.endpoint}
                   <CopyToClipboard text={activity.endpoint} onCopy={showToolTip}>
                     <button type="button" className="btn btn-outline-secondary border-0 pull-right" id="tooltipTarget">
-                      <i className="fa fa-clipboard" aria-hidden="true"></i>
+                      <span className="material-symbols-outlined" aria-hidden="true">content_paste</span>
                     </button>
                   </CopyToClipboard>
                   <Tooltip placement="top" isOpen={tooltopOpen} fade={false} target="tooltipTarget">

+ 3 - 2
apps/app/src/components/Admin/AuditLog/AuditLogSettings.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState } from 'react';
+import type { FC } from 'react';
+import React, { useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
 import { Collapse } from 'reactstrap';
@@ -54,7 +55,7 @@ export const AuditLogSettings: FC = () => {
       </p>
       <p className="mt-1">
         <button type="button" className="btn btn-link p-0" aria-expanded="false" onClick={() => setIsExpandActionList(!isExpandActionList)}>
-          <i className={`fa fa-fw fa-arrow-right ${isExpandActionList ? 'fa-rotate-90' : ''}`}></i>
+          <span className={`material-symbols-outlined me-1 ${isExpandActionList ? 'fa-rotate-90' : ''}`}>navigate_next</span>
           { t('admin:audit_log_management.action_list') }
         </button>
       </p>

+ 3 - 2
apps/app/src/components/Admin/AuditLog/DateRangePicker.tsx

@@ -1,4 +1,5 @@
-import React, { FC, forwardRef, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { forwardRef, useCallback } from 'react';
 
 import { addDays, format } from 'date-fns';
 import DatePicker from 'react-datepicker';
@@ -19,7 +20,7 @@ const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>((props: Custo
   return (
     <div className="input-group admin-audit-log">
       <span className="input-group-text">
-        <i className="fa fa-fw fa-calendar" />
+        <span className="material-symbols-outlined me-1">calendar_month</span>
       </span>
       <input
         ref={ref}

+ 5 - 3
apps/app/src/components/Admin/AuditLog/SelectActionDropdown.tsx

@@ -1,9 +1,11 @@
-import React, { FC, useMemo, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useMemo, useCallback } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
+import type { SupportedActionType, SupportedActionCategoryType } from '~/interfaces/activity';
 import {
-  SupportedActionType, SupportedActionCategoryType, SupportedActionCategory,
+  SupportedActionCategory,
   PageActions, CommentActions, TagActions, ShareLinkActions, AttachmentActions, InAppNotificationActions, SearchActions, UserActions, AdminActions,
 } from '~/interfaces/activity';
 
@@ -78,7 +80,7 @@ export const SelectActionDropdown: FC<Props> = (props: Props) => {
   return (
     <div className="btn-group me-2 admin-audit-log">
       <button className="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown">
-        <i className="fa fa-fw fa-bolt" />{t('admin:audit_log_management.action')}
+        <span className="material-symbols-outlined me-1">bolt</span>{t('admin:audit_log_management.action')}
       </button>
       <ul className="dropdown-menu select-action-dropdown" aria-labelledby="dropdownMenuButton">
         {dropdownItems.map(item => (

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx

@@ -69,7 +69,7 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
             aria-expanded="false"
             aria-controls="collapseExampleHtml"
           >
-            <i className="fa fa-fw fa-chevron-right" aria-hidden="true"></i>
+            <span className="material-symbols-outlined me-1" aria-hidden="true">navigate_next</span>
             Example for Google Tag Manager
           </a>
           <div className="collapse" id="collapseExampleHtml">

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -66,7 +66,7 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
             aria-expanded="false"
             aria-controls="collapseExampleScript"
           >
-            <i className="fa fa-fw fa-chevron-right" aria-hidden="true"></i>
+            <span className="material-symbols-outlined me-1" aria-hidden="true">navigate_next</span>
             Example for Google Tag Manager
           </a>
           <div className="collapse" id="collapseExampleScript">

+ 2 - 2
apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx

@@ -166,10 +166,10 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
           <div className="row">
             <div className="col-sm-12">
               <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={checkAll}>
-                <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
+                <span className="material-symbols-outlined">check_box</span> {t('admin:export_management.check_all')}
               </button>
               <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={uncheckAll}>
-                <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
+                <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
               </button>
             </div>
           </div>

+ 2 - 2
apps/app/src/components/Admin/G2GDataTransferExportForm.tsx

@@ -202,12 +202,12 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
       <form className="mt-3 row row-cols-lg-auto g-3 align-items-center">
         <div className="col-12">
           <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={checkAll}>
-            <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
+            <span className="material-symbols-outlined">check_box</span>, {t('admin:export_management.check_all')}
           </button>
         </div>
         <div className="col-12">
           <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={uncheckAll}>
-            <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
+            <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
           </button>
         </div>
       </form>

+ 4 - 4
apps/app/src/components/Admin/G2GDataTransferStatusIcon.tsx

@@ -21,23 +21,23 @@ const G2GDataTransferStatusIcon = ({ status, className, ...props }: Props): JSX.
 
   if (status === G2G_PROGRESS_STATUS.COMPLETED) {
     return (
-      <i className={`fa fa-check-circle-o fa-fw text-info ${className}`} aria-label="completed" {...props} />
+      <span className={`material-symbols-outlined text-info ${className}`} aria-label="completed" {...props}>check_circle</span>
     );
   }
 
   if (status === G2G_PROGRESS_STATUS.ERROR) {
     return (
-      <i className={`fa fa-exclamation-circle fa-fw text-danger ${className}`} aria-label="error" {...props} />
+      <span className={`material-symbols-outlined text-danger ${className}`} aria-label="error" {...props}>error</span>
     );
   }
 
   if (status === G2G_PROGRESS_STATUS.SKIPPED) {
     return (
-      <i className={`fa fa-ban fa-fw ${className}`} aria-label="skipped" {...props} />
+      <span className={`material-symbols-outlined ${className}`} aria-label="skipped" {...props}>block</span>
     );
   }
 
-  return <i className={`fa fa-circle-o fa-fw ${className}`} aria-label="pending" {...props} />;
+  return <span className={`material-symbols-outlined ${className}`} aria-label="pending" {...props}>circle</span>;
 };
 
 export default G2GDataTransferStatusIcon;

+ 2 - 2
apps/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -449,12 +449,12 @@ class ImportForm extends React.Component {
         <form className="row row-cols-lg-auto g-3 align-items-center">
           <div className="col-12">
             <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={this.checkAll}>
-              <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
+              <span className="material-symbols-outlined">check_box</span> {t('admin:export_management.check_all')}
             </button>
           </div>
           <div className="col-12">
             <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={this.uncheckAll}>
-              <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
+              <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
             </button>
           </div>
         </form>

+ 7 - 2
apps/app/src/components/Admin/Notification/NotificationTypeIcon.tsx

@@ -23,8 +23,13 @@ export const NotificationTypeIcon = (props: NotificationTypeIconProps): JSX.Elem
   }
 
   const elemId = `notification-${type}-${_id}`;
-  const className = type === 'mail' ? 'icon-fw fa fa-envelope-o' : 'icon-fw fa fa-hashtag';
+  const iconName = type === 'mail' ? 'mail' : 'tag';
   const toolChip = type === 'mail' ? 'Mail' : 'Slack';
 
-  return <><i id={elemId} className={className}></i><UncontrolledTooltip target={elemId}>{toolChip}</UncontrolledTooltip></>;
+  return (
+    <>
+      <span id={elemId} className="material-symbols-outlined me-1">{iconName}</span>
+      <UncontrolledTooltip target={elemId}>{toolChip}</UncontrolledTooltip>
+    </>
+  );
 };

+ 1 - 1
apps/app/src/components/Admin/Notification/UserTriggerNotification.jsx

@@ -112,7 +112,7 @@ class UserTriggerNotification extends React.Component {
               <td>
                 <div className="input-group notify-to-option" id="slack-input">
                   <div>
-                    <span className="input-group-text"><i className="fa fa-hashtag" /></span>
+                    <span className="input-group-text"><span className="material-symbols-outlined">tag</span></span>
                   </div>
                   <input
                     className="form-control"

+ 2 - 2
apps/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx

@@ -181,7 +181,7 @@ class LocalSecuritySettingContents extends React.Component {
                   <div className="alert alert-warning p-1 my-1 small d-inline-block">
                     <span>{t('commons:alert.password_reset_please_enable_mailer')}</span>
                     <Link href="/admin/app#mail-settings">
-                      <i className="fa fa-link"></i> {t('app_setting.mail_settings')}
+                      <span className="material-symbols-outlined">link</span> {t('app_setting.mail_settings')}
                     </Link>
                   </div>
                 )}
@@ -210,7 +210,7 @@ class LocalSecuritySettingContents extends React.Component {
                   <div className="alert alert-warning p-1 my-1 small d-inline-block">
                     <span>{t('commons:alert.please_enable_mailer')}</span>
                     <Link href="/admin/app#mail-settings">
-                      <i className="fa fa-link"></i> {t('app_setting.mail_settings')}
+                      <span className="material-symbols-outlined">link</span> {t('app_setting.mail_settings')}
                     </Link>
                   </div>
                 )}

+ 4 - 4
apps/app/src/components/Admin/Security/SecurityManagementContents.jsx

@@ -30,19 +30,19 @@ const SecurityManagementContents = () => {
   const navTabMapping = useMemo(() => {
     return {
       passport_local: {
-        Icon: () => <i className="fa fa-users" />,
+        Icon: () => <span className="material-symbols-outlined">groups</span>,
         i18n: 'ID/Pass',
       },
       passport_ldap: {
-        Icon: () => <i className="fa fa-sitemap" />,
+        Icon: () => <span className="material-symbols-outlined">network_node</span>,
         i18n: 'LDAP',
       },
       passport_saml: {
-        Icon: () => <i className="fa fa-key" />,
+        Icon: () => <span className="material-symbols-outlined">key</span>,
         i18n: 'SAML',
       },
       passport_oidc: {
-        Icon: () => <i className="fa fa-key" />,
+        Icon: () => <span className="material-symbols-outlined">key</span>,
         i18n: 'OIDC',
       },
       passport_google: {

+ 1 - 1
apps/app/src/components/Admin/Security/SecuritySetting.jsx

@@ -296,7 +296,7 @@ class SecuritySetting extends React.Component {
                     aria-expanded="false"
                     onClick={() => this.setExpantOtherDeleteOptionsState(deletionType, !expantDeleteOptionsState)}
                   >
-                    <i className={`fa fa-fw fa-arrow-right ${expantDeleteOptionsState ? 'fa-rotate-90' : ''}`}></i>
+                    <span className={`material-symbols-outlined me-1 ${expantDeleteOptionsState ? 'fa-rotate-90' : ''}`}>navigate_next</span>
                     { t('security_settings.other_options') }
                   </button>
                   <Collapse isOpen={expantDeleteOptionsState}>

+ 4 - 4
apps/app/src/components/Admin/SlackIntegration/Bridge.jsx

@@ -72,15 +72,15 @@ const Bridge = (props) => {
   // all green
   else if (errorCount === 0) {
     description = t('admin:slack_integration.integration_sentence.integration_successful');
-    iconClass = 'fa fa-check text-success';
-    iconName = '';
+    iconClass = 'material-symbols-outlined text-success';
+    iconName = 'check';
     hrClass = 'border-success admin-border-success';
   }
   // some of them failed
   else {
     description = t('admin:slack_integration.integration_sentence.integration_some_ws_is_not_complete');
-    iconClass = 'fa fa-check text-warning';
-    iconName = '';
+    iconClass = 'material-symbols-outlined text-warning';
+    iconName = 'check';
     hrClass = 'border-warning admin-border-failed';
   }
 

+ 3 - 3
apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -115,7 +115,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.REGISTER_SLACK_CONFIGURATION)}
         // eslint-disable-next-line max-len
-        title={<><span className="me-3">3</span>{t('admin:slack_integration.accordion.register_secret_and_token')}{isEnterdSecretAndToken && <i className="ms-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="me-3">3</span>{t('admin:slack_integration.accordion.register_secret_and_token')}{isEnterdSecretAndToken && <span className="material-symbols-outlined ms-3 text-success">check</span>}</>}
       >
         <CustomBotWithoutProxySecretTokenSection
           onUpdatedSecretToken={props.onUpdatedSecretToken}
@@ -138,7 +138,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         // eslint-disable-next-line max-len
-        title={<><span className="me-3">5</span>{t('admin:slack_integration.accordion.test_connection')}{isLatestConnectionSuccess && <i className="ms-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="me-3">5</span>{t('admin:slack_integration.accordion.test_connection')}{isLatestConnectionSuccess && <span className="material-symbols-outlined ms-3 text-success">check</span>}</>}
       >
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <p className="text-center text-warning">
@@ -148,7 +148,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
           <form className="align-items-center" onSubmit={e => submitForm(e)}>
             <div className="input-group col-8">
               <div>
-                <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-hashtag" /></span>
+                <span className="input-group-text" id="slack-channel-addon"><span className="material-symbols-outlined">tag</span></span>
               </div>
               <input
                 className="form-control"

+ 2 - 2
apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -246,7 +246,7 @@ const TestProcess = ({
         <form className="justify-content-center" onSubmit={e => submitForm(e)}>
           <div className="input-group col-8">
             <div>
-              <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-hashtag" /></span>
+              <span className="input-group-text" id="slack-channel-addon"><span className="material-symbols-outlined">tag</span></span>
             </div>
             <input
               className="form-control"
@@ -391,7 +391,7 @@ const WithProxyAccordions = (props) => {
               <>
                 <span className="me-3">{key}</span>
                 {t(`admin:slack_integration.accordion.${value.title}`)}
-                {value.title === 'test_connection' && isLatestConnectionSuccess && <i className="ms-3 text-success fa fa-check"></i>}
+                {value.title === 'test_connection' && isLatestConnectionSuccess && <span className="material-symbols-outlined ms-3 text-success">check</span>}
               </>
             )}
             key={key}

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

@@ -214,7 +214,7 @@ export const UserGroupTable: FC<Props> = ({
                           {onRemove != null
                           && (
                             <button className="dropdown-item" type="button" role="button" onClick={onClickRemove} data-user-group-id={group._id}>
-                              <i className="icon-fw fa fa-chain-broken"></i> {t('admin:user_group_management.remove_child_group')}
+                              <span className="material-symbols-outlined me-1">group_remove</span> {t('admin:user_group_management.remove_child_group')}
                             </button>
                           )}
                           <button className="dropdown-item" type="button" role="button" onClick={onClickDelete} data-user-group-id={group._id}>

+ 8 - 4
apps/app/src/components/Admin/Users/SortIcons.tsx

@@ -13,15 +13,19 @@ export const SortIcons = (props: SortIconsProps): JSX.Element => {
   return (
     <div className="d-flex flex-column text-center">
       <a
-        className={`fa ${isSelected && isAsc ? 'fa-chevron-up' : 'fa-angle-up'}`}
+        className={`${isSelected && isAsc ? 'text-primary' : 'text-muted'}`}
         aria-hidden="true"
         onClick={() => onClick('asc')}
-      />
+      >
+        <span className="material-symbols-outlined">expand_less</span>
+      </a>
       <a
-        className={`fa ${isSelected && !isAsc ? 'fa-chevron-down' : 'fa-angle-down'}`}
+        className={`${isSelected && !isAsc ? 'text-primary' : 'text-muted'}`}
         aria-hidden="true"
         onClick={() => onClick('desc')}
-      />
+      >
+        <span className="material-symbols-outlined">expand_more</span>
+      </a>
     </div>
   );
 };

+ 2 - 2
apps/app/src/components/Admin/Users/UserMenu.module.scss

@@ -1,5 +1,5 @@
 .grw-usermenu-notification-icon :global {
   position: absolute;
-  top: -4px;
-  left: 30px;
+  top: -6px;
+  left: 3px;
 }

+ 5 - 2
apps/app/src/components/Admin/Users/UserMenu.tsx

@@ -95,10 +95,13 @@ const UserMenu = (props: UserMenuProps) => {
   return (
     <UncontrolledDropdown id="userMenu" size="sm">
       <DropdownToggle caret color="secondary" outline>
-        {/* TODO:fontsize: 20px */}
         <span className="material-symbols-outlined fs-5">settings</span>
         {(user.status === USER_STATUS.INVITED && !isInvitationEmailSended)
-        && <i className={`fa fa-circle text-danger grw-usermenu-notification-icon ${styles['grw-usermenu-notification-icon']}`} />}
+        && (
+          <span className={`material-symbols-outlined fill fs-6 text-danger grw-usermenu-notification-icon ${styles['grw-usermenu-notification-icon']}`}>
+            circle
+          </span>
+        )}
       </DropdownToggle>
       <DropdownMenu strategy="fixed">
         {renderEditMenu()}

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkFolderItemControl.tsx

@@ -35,7 +35,7 @@ export const BookmarkFolderItemControl: React.FC<{
             onClick={onClickMoveToRoot}
             className="grw-page-control-dropdown-item"
           >
-            <i className="fa fa-fw fa-bookmark-o grw-page-control-dropdown-icon"></i>
+            <span className="material-symbols-outlined grw-page-control-dropdown-icon">bookmark</span>
             {t('bookmark_folder.move_to_root')}
           </DropdownItem>
         )}

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkFolderMenu.tsx

@@ -118,7 +118,7 @@ export const BookmarkFolderMenu = (props: BookmarkFolderMenuProps): JSX.Element
           onClick={onUnbookmarkHandler}
           className="grw-bookmark-folder-menu-item text-danger"
         >
-          <i className="fa fa-bookmark"></i>{' '}
+          <span className="material-symbols-outlined">bookmark</span>{' '}
           <span className="mx-2">
             {t('bookmark_folder.cancel_bookmark')}
           </span>

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkMoveToRootBtn.tsx

@@ -15,7 +15,7 @@ export const BookmarkMoveToRootBtn: React.FC<{
       className="grw-page-control-dropdown-item"
       data-testid="add-remove-bookmark-btn"
     >
-      <i className="fa fa-fw fa-bookmark-o grw-page-control-dropdown-icon"></i>
+      <span className="material-symbols-outlined grw-page-control-dropdown-icon">bookmark</span>
       {t('bookmark_folder.move_to_root')}
     </DropdownItem>
   );

+ 3 - 2
apps/app/src/components/Common/CustomCopyToClipBoard.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { useTranslation } from 'react-i18next';
@@ -25,7 +26,7 @@ const CustomCopyToClipBoard: FC<Props> = (props: Props) => {
     <>
       <CopyToClipboard text={props.textToBeCopied || ''} onCopy={showToolTip}>
         <div className="btn input-group-text" id="tooltipTarget">
-          <i className="fa fa-clipboard mx-1" aria-hidden="true"></i>
+          <span className="material-symbols-outlined mx-1" aria-hidden="true">content_paste</span>
         </div>
       </CopyToClipboard>
       <Tooltip target="tooltipTarget" fade={false} isOpen={tooltipOpen}>

+ 1 - 1
apps/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -170,7 +170,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
             className="grw-page-control-dropdown-item"
             data-testid="add-remove-bookmark-btn"
           >
-            <i className="fa fa-fw fa-bookmark-o grw-page-control-dropdown-icon"></i>
+            <span className="material-symbols-outlined grw-page-control-dropdown-icon">bookmark</span>
             { pageInfo.isBookmarked ? t('remove_bookmark') : t('add_bookmark') }
           </DropdownItem>
         ) }

+ 2 - 2
apps/app/src/components/Me/AssociateModal.tsx

@@ -66,7 +66,7 @@ const AssociateModal = (props: Props): JSX.Element => {
               className={activeTab === 1 ? 'active' : ''}
               onClick={() => setActiveTab(1)}
             >
-              <i className="fa fa-sitemap"></i> LDAP
+              <span className="material-symbols-outlined">network_node</span> LDAP
             </NavLink>
             <NavLink
               className={activeTab === 2 ? 'active' : ''}
@@ -113,7 +113,7 @@ const AssociateModal = (props: Props): JSX.Element => {
       </ModalBody>
       <ModalFooter className="border-top-0">
         <button type="button" className="btn btn-primary mt-3" onClick={clickAddLdapAccountHandler}>
-          <i className="fa fa-plus-circle" aria-hidden="true"></i>
+          <span className="material-symbols-outlined" aria-hidden="true">add_circle</span>
           {t('add')}
         </button>
       </ModalFooter>

+ 3 - 3
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -322,14 +322,14 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
         )}
 
         { isGuestUser && (
-          <>
-            <Link href="/login#register" className="btn" prefetch={false}>
+          <div className="mt-2">
+            <Link href="/login#register" className="btn me-2" prefetch={false}>
               <span className="material-symbols-outlined me-1">person_add</span>{t('Sign up')}
             </Link>
             <Link href="/login#login" className="btn btn-primary" prefetch={false}>
               <span className="material-symbols-outlined me-1">login</span>{t('Sign in')}
             </Link>
-          </>
+          </div>
         ) }
       </div>
 

+ 2 - 3
apps/app/src/components/PageControls/BookmarkButtons.tsx

@@ -1,6 +1,5 @@
-import React, {
-  FC, useState, useCallback,
-} from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import DropdownToggle from 'reactstrap/esm/DropdownToggle';

+ 4 - 2
apps/app/src/components/PageControls/LikeButtons.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import type { IUser } from '@growi/core';
 import { useTranslation } from 'next-i18next';
@@ -8,6 +9,7 @@ import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 import UserPictureList from '../Common/UserPictureList';
 
 import styles from './LikeButtons.module.scss';
+import popoverStyles from './user-list-popover.module.scss';
 
 type LikeButtonsProps = {
 
@@ -65,7 +67,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         {sumOfLikers}
       </button>
       <Popover placement="bottom" isOpen={isPopoverOpen} target="po-total-likes" toggle={togglePopover} trigger="legacy">
-        <PopoverBody className="user-list-popover">
+        <PopoverBody className={`user-list-popover ${popoverStyles['user-list-popover']}`}>
           <div className="px-2 text-end user-list-content text-truncate text-muted">
             {props.likers?.length ? <UserPictureList users={props.likers} /> : t('No users have liked this yet.')}
           </div>

+ 2 - 1
apps/app/src/components/PageControls/SeenUserInfo.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState } from 'react';
+import type { FC } from 'react';
+import React, { useState } from 'react';
 
 import type { IUser } from '@growi/core';
 import { FootstampIcon } from '@growi/ui/dist/components';

+ 3 - 10
apps/app/src/components/PageControls/user-list-popover.module.scss

@@ -1,12 +1,5 @@
-.user-list-popover :global {
-  --bs-popover-max-width: 200px;
-  --bs-popover-body-padding-x: .5rem;
-  --bs-popover-body-padding-y: .5rem;
+@use '@growi/ui/scss/molecules/user-list-popover';
 
-  .user-list-content {
-    direction: rtl;
-  }
-  .cls-1 {
-    isolation: isolate;
-  }
+.user-list-popover :global {
+  @extend %user-list-popover
 }

+ 54 - 90
apps/app/src/components/PageCreateModal.jsx → apps/app/src/components/PageCreateModal.tsx

@@ -5,33 +5,38 @@ import React, {
 import { pagePathUtils, pathUtils } from '@growi/core/dist/utils';
 import { format } from 'date-fns';
 import { useTranslation } from 'next-i18next';
-import { useRouter } from 'next/router';
 import {
   Modal, ModalHeader, ModalBody, UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
-import { toastError } from '~/client/util/toastr';
+import { useCreateTemplatePage } from '~/client/services/create-page';
+import { useCreatePageAndTransit } from '~/client/services/create-page/use-create-page-and-transit';
+import { useToastrOnError } from '~/client/services/use-toastr-on-error';
 import { useCurrentUser, useIsSearchServiceReachable } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
-import { EditorMode, useEditorMode } from '~/stores/ui';
+
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 import styles from './PageCreateModal.module.scss';
 
 const {
-  isCreatablePage, generateEditorPath, isUsersHomepage,
+  isCreatablePage, isUsersHomepage,
 } = pagePathUtils;
 
-const PageCreateModal = () => {
+const PageCreateModal: React.FC = () => {
   const { t } = useTranslation();
-  const router = useRouter();
 
   const { data: currentUser } = useCurrentUser();
 
   const { data: pageCreateModalData, close: closeCreateModal } = usePageCreateModal();
-  const { isOpened, path } = pageCreateModalData;
+
+  const path = pageCreateModalData?.path;
+  const isOpened = pageCreateModalData?.isOpened ?? false;
+
+  const { createAndTransit } = useCreatePageAndTransit();
+  const { createTemplate } = useCreateTemplatePage();
 
   const { data: isReachable } = useIsSearchServiceReachable();
   const pathname = path || '';
@@ -39,26 +44,13 @@ const PageCreateModal = () => {
   const isCreatable = isCreatablePage(pathname) || isUsersHomepage(pathname);
   const pageNameInputInitialValue = isCreatable ? pathUtils.addTrailingSlash(pathname) : '/';
   const now = format(new Date(), 'yyyy/MM/dd');
+  const todaysParentPath = [userHomepagePath, t('create_page_dropdown.todays.memo', { ns: 'commons' }), now].join('/');
 
-  const { mutate: mutateEditorMode } = useEditorMode();
-
-  const [todayInput1, setTodayInput1] = useState(t('Memo'));
-  const [todayInput2, setTodayInput2] = useState('');
+  const [todayInput, setTodayInput] = useState('');
   const [pageNameInput, setPageNameInput] = useState(pageNameInputInitialValue);
   const [template, setTemplate] = useState(null);
   const [isMatchedWithUserHomepagePath, setIsMatchedWithUserHomepagePath] = useState(false);
 
-  // ensure pageNameInput is synced with selectedPagePath || currentPagePath
-  useEffect(() => {
-    if (isOpened) {
-      setPageNameInput(isCreatable ? pathUtils.addTrailingSlash(pathname) : '/');
-    }
-  }, [isOpened, pathname, isCreatable]);
-
-  useEffect(() => {
-    setTodayInput1(t('Memo'));
-  }, [t]);
-
   const checkIsUsersHomepageDebounce = useMemo(() => {
     const checkIsUsersHomepage = () => {
       setIsMatchedWithUserHomepagePath(isUsersHomepage(pageNameInput));
@@ -69,10 +61,11 @@ const PageCreateModal = () => {
 
   useEffect(() => {
     if (isOpened) {
-      checkIsUsersHomepageDebounce(pageNameInput);
+      checkIsUsersHomepageDebounce();
     }
   }, [isOpened, checkIsUsersHomepageDebounce, pageNameInput]);
 
+
   function transitBySubmitEvent(e, transitHandler) {
     // prevent page transition by submit
     e.preventDefault();
@@ -80,19 +73,11 @@ const PageCreateModal = () => {
   }
 
   /**
-   * change todayInput1
-   * @param {string} value
-   */
-  function onChangeTodayInput1Handler(value) {
-    setTodayInput1(value);
-  }
-
-  /**
-   * change todayInput2
+   * change todayInput
    * @param {string} value
    */
-  function onChangeTodayInput2Handler(value) {
-    setTodayInput2(value);
+  function onChangeTodayInputHandler(value) {
+    setTodayInput(value);
   }
 
   /**
@@ -103,53 +88,41 @@ const PageCreateModal = () => {
     setTemplate(value);
   }
 
-  /**
-   * join path, check if creatable, then redirect
-   * @param {string} paths
-   */
-  const redirectToEditor = useCallback(async(...paths) => {
-    try {
-      const editorPath = generateEditorPath(...paths);
-      await router.push(editorPath);
-      mutateEditorMode(EditorMode.Editor);
-
-      // close modal
-      closeCreateModal();
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [closeCreateModal, mutateEditorMode, router]);
-
   /**
    * access today page
    */
-  function createTodayPage() {
-    let tmpTodayInput1 = todayInput1;
-    if (tmpTodayInput1 === '') {
-      tmpTodayInput1 = t('Memo');
-    }
-    redirectToEditor(userHomepagePath, tmpTodayInput1, now, todayInput2);
-  }
+  const createTodayPage = useCallback(async() => {
+    const joinedPath = [todaysParentPath, todayInput].join('/');
+    return createAndTransit(
+      { path: joinedPath, wip: true },
+      { shouldCheckPageExists: true, onTerminated: closeCreateModal },
+    );
+  }, [closeCreateModal, createAndTransit, todayInput, todaysParentPath]);
 
   /**
    * access input page
    */
-  function createInputPage() {
-    redirectToEditor(pageNameInput);
-  }
-
-  const ppacSubmitHandler = useCallback((input) => {
-    redirectToEditor(input);
-  }, [redirectToEditor]);
+  const createInputPage = useCallback(async() => {
+    return createAndTransit(
+      { path: pageNameInput, optionalParentPath: '/', wip: true },
+      { shouldCheckPageExists: true, onTerminated: closeCreateModal },
+    );
+  }, [closeCreateModal, createAndTransit, pageNameInput]);
 
   /**
    * access template page
    */
-  function createTemplatePage(e) {
-    const pageName = (template === 'children') ? '_template' : '__template';
-    redirectToEditor(pathname, pageName);
-  }
+  const createTemplatePage = useCallback(async() => {
+
+    const label = (template === 'children') ? '_template' : '__template';
+
+    await createTemplate?.(label);
+    closeCreateModal();
+  }, [closeCreateModal, createTemplate, template]);
+
+  const createTodaysMemoWithToastr = useToastrOnError(createTodayPage);
+  const createInputPageWithToastr = useToastrOnError(createInputPage);
+  const createTemplateWithToastr = useToastrOnError(createTemplatePage);
 
   function renderCreateTodayForm() {
     if (!isOpened) {
@@ -158,31 +131,22 @@ const PageCreateModal = () => {
     return (
       <div className="row">
         <fieldset className="col-12 mb-4">
-          <h3 className="grw-modal-head pb-2">{t("Create today's")}</h3>
+          <h3 className="grw-modal-head pb-2">{t('create_page_dropdown.todays.desc', { ns: 'commons' })}</h3>
 
           <div className="d-sm-flex align-items-center justify-items-between">
 
             <div className="d-flex align-items-center flex-fill flex-wrap flex-lg-nowrap">
-              <div className="d-flex align-items-center">
-                <span>{userHomepagePath}/</span>
-                <form onSubmit={e => transitBySubmitEvent(e, createTodayPage)}>
-                  <input
-                    type="text"
-                    className="page-today-input1 form-control text-center mx-2"
-                    value={todayInput1}
-                    onChange={e => onChangeTodayInput1Handler(e.target.value)}
-                  />
-                </form>
-                <span className="page-today-suffix">/{now}/</span>
+              <div className="d-flex align-items-center text-nowrap">
+                <span>{todaysParentPath}/</span>
               </div>
-              <form className="mt-1 mt-lg-0 ms-lg-2 w-100" onSubmit={e => transitBySubmitEvent(e, createTodayPage)}>
+              <form className="mt-1 mt-lg-0 ms-lg-2 w-100" onSubmit={(e) => { transitBySubmitEvent(e, createTodaysMemoWithToastr) }}>
                 <input
                   type="text"
                   className="page-today-input2 form-control w-100"
                   id="page-today-input2"
                   placeholder={t('Input page name (optional)')}
-                  value={todayInput2}
-                  onChange={e => onChangeTodayInput2Handler(e.target.value)}
+                  value={todayInput}
+                  onChange={e => onChangeTodayInputHandler(e.target.value)}
                 />
               </form>
             </div>
@@ -192,7 +156,7 @@ const PageCreateModal = () => {
                 type="button"
                 data-testid="btn-create-memo"
                 className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ms-3"
-                onClick={createTodayPage}
+                onClick={createTodaysMemoWithToastr}
               >
                 <span className="material-symbols-outlined">description</span>{t('Create')}
               </button>
@@ -221,13 +185,13 @@ const PageCreateModal = () => {
                   <PagePathAutoComplete
                     initializedPath={pageNameInputInitialValue}
                     addTrailingSlash
-                    onSubmit={ppacSubmitHandler}
+                    onSubmit={createInputPageWithToastr}
                     onInputChange={value => setPageNameInput(value)}
                     autoFocus
                   />
                 )
                 : (
-                  <form onSubmit={e => transitBySubmitEvent(e, createInputPage)}>
+                  <form onSubmit={(e) => { transitBySubmitEvent(e, createInputPageWithToastr) }}>
                     <input
                       type="text"
                       value={pageNameInput}
@@ -245,7 +209,7 @@ const PageCreateModal = () => {
                 type="button"
                 data-testid="btn-create-page-under-below"
                 className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ms-3"
-                onClick={createInputPage}
+                onClick={createInputPageWithToastr}
                 disabled={isMatchedWithUserHomepagePath}
               >
                 <span className="material-symbols-outlined">description</span>{t('Create')}
@@ -300,7 +264,7 @@ const PageCreateModal = () => {
                 data-testid="grw-btn-edit-page"
                 type="button"
                 className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ms-3"
-                onClick={createTemplatePage}
+                onClick={createTemplateWithToastr}
                 disabled={template == null}
               >
                 <span className="material-symbols-outlined">description</span>{t('Edit')}

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

@@ -91,7 +91,7 @@ const EditorNavbarBottom = (): JSX.Element => {
             >
               <div className="grw-slack-logo">
                 <SlackLogo />
-                <span className="grw-btn-slack-triangle fa fa-caret-up ms-2"></span>
+                <span className="grw-btn-slack-triangle material-symbols-outlined ms-2">arrow_drop_up</span>
               </div>
             </Button>
           ) : (

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

@@ -471,7 +471,7 @@ export const HandsontableModal = (): JSX.Element => {
             onClick={toggleDataImportArea}
           >
             <span className="me-3">{t('handsontable_modal.data_import')}</span>
-            <i className={isDataImportAreaExpanded ? 'fa fa-angle-up' : 'fa fa-angle-down'}></i>
+            <span className="material-symbols-outlined">{isDataImportAreaExpanded ? 'expand_less' : 'expand_more'}</span>
           </button>
           <div role="group" className="btn-group">
             <button type="button" className="btn btn-secondary" onClick={() => { alignButtonHandler('l') }}>

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

@@ -180,8 +180,8 @@ export const LinkEditModal = (): JSX.Element => {
         </div>
         <div className="d-flex align-items-center justify-content-center">
           <span className="lead mx-3">
-            <i className="d-none d-sm-block fa fa-caret-right"></i>
-            <i className="d-sm-none fa fa-caret-down"></i>
+            <span className="d-none d-sm-block material-symbols-outlined">arrow_right</span>
+            <span className="d-sm-none material-symbols-outlined">arrow_drop_down</span>
           </span>
         </div>
         <div className="card w-100 p-1 mb-0">

+ 9 - 7
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -6,7 +6,7 @@ import React, {
 import type EventEmitter from 'events';
 import nodePath from 'path';
 
-import type { IPageHasId } from '@growi/core';
+import type { IPageHasId, IUserHasId } from '@growi/core';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { pathUtils } from '@growi/core/dist/utils';
 import {
@@ -53,6 +53,7 @@ import {
   EditorMode,
   useEditorMode, useSelectedGrant,
 } from '~/stores/ui';
+import { useEditingUsers } from '~/stores/use-editing-users';
 import { useNextThemes } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
@@ -87,7 +88,8 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   const { t } = useTranslation();
 
-  const [previewRect, previewRef] = useRect();
+  const previewRef = useRef<HTMLDivElement>(null);
+  const [previewRect] = useRect(previewRef);
 
   const { data: isNotFound } = useIsNotFound();
   const { data: pageId } = useCurrentPageId();
@@ -116,6 +118,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
   const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
   const { data: user } = useCurrentUser();
+  const { onEditorsUpdated } = useEditingUsers();
 
   const { data: socket } = useGlobalSocket();
 
@@ -156,8 +159,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     // set to ref
     initialValueRef.current = initialValue;
   }, [initialValue]);
-
-
   const [markdownToPreview, setMarkdownToPreview] = useState<string>(initialValue);
   const setMarkdownPreviewWithDebounce = useMemo(() => debounce(100, throttle(150, (value: string) => {
     setMarkdownToPreview(value);
@@ -307,7 +308,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
     isOriginOfScrollSyncEditor = true;
     scrollEditor(codeMirrorEditor.view.scrollDOM, previewRef.current);
-  }, [codeMirrorEditor, previewRef]);
+  }, [codeMirrorEditor]);
 
   const scrollEditorHandlerThrottle = useMemo(() => throttle(25, scrollEditorHandler), [scrollEditorHandler]);
 
@@ -323,7 +324,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
     isOriginOfScrollSyncPreview = true;
     scrollPreview(codeMirrorEditor.view.scrollDOM, previewRef.current);
-  }, [codeMirrorEditor, previewRef]);
+  }, [codeMirrorEditor]);
 
   const scrollPreviewHandlerThrottle = useMemo(() => throttle(25, scrollPreviewHandler), [scrollPreviewHandler]);
 
@@ -444,10 +445,11 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
             acceptedUploadFileType={acceptedUploadFileType}
             onScroll={scrollEditorHandlerThrottle}
             indentSize={currentIndentSize ?? defaultIndentSize}
-            userName={user?.name}
+            user={user ?? undefined}
             pageId={pageId ?? undefined}
             initialValue={initialValue}
             onOpenEditor={markdown => setMarkdownToPreview(markdown)}
+            onEditorsUpdated={onEditorsUpdated}
             editorTheme={editorSettings?.theme}
             editorKeymap={editorSettings?.keymapMode}
           />

+ 53 - 0
apps/app/src/components/PageHeader/EditingUserList.tsx

@@ -0,0 +1,53 @@
+import { type FC, useState } from 'react';
+
+import type { IUserHasId } from '@growi/core';
+import { UserPicture } from '@growi/ui/dist/components';
+import { Popover, PopoverBody } from 'reactstrap';
+
+import UserPictureList from '../Common/UserPictureList';
+
+import popoverStyles from './user-list-popover.module.scss';
+
+type Props = {
+  className: string,
+  userList: IUserHasId[]
+}
+
+export const EditingUserList: FC<Props> = ({ className, userList }) => {
+  const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+  const togglePopover = () => setIsPopoverOpen(!isPopoverOpen);
+
+  const firstFourUsers = userList.slice(0, 4);
+  const remainingUsers = userList.slice(4);
+
+  return (
+    <div className={className}>
+      {userList.length > 0 && (
+        <div className="d-flex justify-content-end">
+          {firstFourUsers.map(user => (
+            <div className="ms-1">
+              <UserPicture
+                user={user}
+                noLink
+                additionalClassName="border border-info"
+              />
+            </div>
+          ))}
+          {remainingUsers.length > 0 && (
+            <div className="ms-1">
+              <button type="button" id="btn-editing-user" className="btn border-0 bg-info-subtle rounded-pill p-0">
+                <span className="fw-bold text-info p-1">+{remainingUsers.length}</span>
+              </button>
+              <Popover placement="bottom" isOpen={isPopoverOpen} target="btn-editing-user" toggle={togglePopover} trigger="legacy">
+                <PopoverBody className={`user-list-popover ${popoverStyles['user-list-popover']}`}>
+                  <UserPictureList users={remainingUsers} />
+                </PopoverBody>
+              </Popover>
+            </div>
+          )}
+        </div>
+      )}
+    </div>
+  );
+};

+ 17 - 4
apps/app/src/components/PageHeader/PageHeader.tsx

@@ -1,7 +1,11 @@
 import type { FC } from 'react';
 
+import { DevidedPagePath } from '@growi/core/dist/models';
+
 import { useSWRxCurrentPage } from '~/stores/page';
+import { useEditingUsers } from '~/stores/use-editing-users';
 
+import { EditingUserList } from './EditingUserList';
 import { PagePathHeader } from './PagePathHeader';
 import { PageTitleHeader } from './PageTitleHeader';
 
@@ -11,20 +15,29 @@ const moduleClass = styles['page-header'] ?? '';
 
 export const PageHeader: FC = () => {
   const { data: currentPage } = useSWRxCurrentPage();
+  const { data: editingUsers } = useEditingUsers();
 
   if (currentPage == null) {
     return <></>;
   }
 
+  const dPagePath = new DevidedPagePath(currentPage.path, true);
+
   return (
     <div className={moduleClass}>
       <PagePathHeader
         currentPage={currentPage}
       />
-      <PageTitleHeader
-        className="mt-2"
-        currentPage={currentPage}
-      />
+      <div className="row mt-2">
+        <PageTitleHeader
+          className="col"
+          currentPage={currentPage}
+        />
+        <EditingUserList
+          className={`${dPagePath.isRoot ? 'mt-1' : 'col mt-2'}`}
+          userList={editingUsers?.userList ?? []}
+        />
+      </div>
     </div>
   );
 };

+ 5 - 0
apps/app/src/components/PageHeader/user-list-popover.module.scss

@@ -0,0 +1,5 @@
+@use '@growi/ui/scss/molecules/user-list-popover';
+
+.user-list-popover :global {
+  @extend %user-list-popover;
+}

+ 1 - 1
apps/app/src/components/PageManagement/ApiErrorMessage.jsx

@@ -39,7 +39,7 @@ const ApiErrorMessage = (props) => {
           <>
             <strong><span className="material-symbols-outlined me-1">lightbulb</span> { t('page_api_error.outdated') }</strong>
             <a className="btn-link" onClick={reload}>
-              <i className="fa fa-angle-double-right"></i> { t('Load latest') }
+              <span className="material-symbols-outlined">keyboard_double_arrow_right</span> { t('Load latest') }
             </a>
           </>
         );

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

@@ -287,7 +287,7 @@ const PageRenameModal = (): JSX.Element => {
 
         <p className="mt-2">
           <button type="button" className="btn btn-link mt-2 p-0" aria-expanded="false" onClick={() => setExpandOtherOptions(!expandOtherOptions)}>
-            <i className={`fa fa-fw fa-arrow-right ${expandOtherOptions ? 'fa-rotate-90' : ''}`}></i>
+            <span className={`material-symbols-outlined me-1 ${expandOtherOptions ? 'fa-rotate-90' : ''}`}>navigate_next</span>
             { t('modal_rename.label.Other options') }
           </button>
         </p>

+ 2 - 17
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx

@@ -1,10 +1,9 @@
-import React, { useState, useCallback } from 'react';
+import React, { useState } from 'react';
 
-import { useTranslation } from 'react-i18next';
 import { Dropdown } from 'reactstrap';
 
 import { useCreateTemplatePage } from '~/client/services/create-page';
-import { toastError } from '~/client/util/toastr';
+import { useToastrOnError } from '~/client/services/use-toastr-on-error';
 
 import { CreateButton } from './CreateButton';
 import { DropendMenu } from './DropendMenu';
@@ -12,20 +11,6 @@ import { DropendToggle } from './DropendToggle';
 import { useCreateNewPage, useCreateTodaysMemo } from './hooks';
 
 
-const useToastrOnError = <P, R>(method?: (param?: P) => Promise<R|undefined>): (param?: P) => Promise<R|undefined> => {
-  const { t } = useTranslation('commons');
-
-  return useCallback(async(param) => {
-    try {
-      return await method?.(param);
-    }
-    catch (err) {
-      toastError(t('toaster.create_failed', { target: 'a page' }));
-    }
-  }, [method, t]);
-};
-
-
 export const PageCreateButton = React.memo((): JSX.Element => {
   const [isHovered, setIsHovered] = useState(false);
 

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

@@ -136,7 +136,7 @@ const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Elemen
   return (
     <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
       <Nav onPrimaryItemHover={primaryItemHoverHandler} />
-      <div className={`sidebar-contents-container flex-grow-1 overflow-y-auto ${openClass}`} style={{ width: collapsibleContentsWidth }}>
+      <div className={`sidebar-contents-container flex-grow-1 overflow-y-auto overflow-x-hidden ${openClass}`} style={{ width: collapsibleContentsWidth }}>
         {children}
       </div>
     </div>

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

@@ -29,7 +29,7 @@ const SystemVersion = (props: Props): JSX.Element => {
         </span>
         { showShortcutsButton && (
           <button type="button" className="btn btn-link ms-2 p-0" onClick={() => openShortcutsModal()}>
-            <i className="fa fa-keyboard-o"></i>&nbsp;<span className={`cmd-key ${os}`}></span>-/
+            <span className="material-symbols-outlined">keyboard</span>&nbsp;<span className={`cmd-key ${os}`}></span>-/
           </button>
         ) }
       </div>

+ 8 - 5
apps/app/src/components/TreeItem/SimpleItem.tsx

@@ -47,10 +47,13 @@ const SimpleItemContent = ({ page }: { page: IPageForItem }) => {
   const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
 
   return (
-    <div className="flex-grow-1 d-flex align-items-center pe-none">
+    <div
+      className="flex-grow-1 d-flex align-items-center pe-none"
+      style={{ minWidth: 0 }}
+    >
       {shouldShowAttentionIcon && (
         <>
-          <i id="path-recovery" className="fa fa-warning mr-2 text-warning"></i>
+          <span id="path-recovery" className="material-symbols-outlined mr-2 text-warning">warning</span>
           <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
             {t('tooltip.operation.attention.rename')}
           </UncontrolledTooltip>
@@ -59,9 +62,9 @@ const SimpleItemContent = ({ page }: { page: IPageForItem }) => {
       {page != null && page.path != null && page._id != null && (
         <div className="grw-pagetree-title-anchor flex-grow-1">
           <div className="d-flex align-items-center">
-            <span className={`text-truncate ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</span>
+            <span className={`text-truncate me-1 ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</span>
             { page.wip && (
-              <span className="wip-page-badge badge rounded-pill text-bg-secondary ms-2">WIP</span>
+              <span className="wip-page-badge badge rounded-pill me-1 text-bg-secondary">WIP</span>
             )}
           </div>
         </div>
@@ -206,7 +209,7 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
           {hasDescendants && (
             <button
               type="button"
-              className={`grw-pagetree-triangle-btn btn ${isOpen ? 'grw-pagetree-open' : ''}`}
+              className={`grw-pagetree-triangle-btn btn p-0 ${isOpen ? 'grw-pagetree-open' : ''}`}
               onClick={onClickLoadChildren}
             >
               <div className="d-flex justify-content-center">

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

@@ -27,7 +27,7 @@ export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Elemen
     <div className={`container-lg user-page-footer py-5 ${styles['user-page-footer']}`}>
       <div className="grw-user-page-list-m d-edit-none">
         <h2 id="bookmarks-list" className="grw-user-page-header border-bottom pb-2 mb-3 d-flex">
-          <i style={{ fontSize: '1.3em' }} className="fa fa-fw fa-bookmark-o"></i>
+          <span style={{ fontSize: '1.3em' }} className="material-symbols-outlined">bookmark</span>
           {t('footer.bookmarks')}
           <span className="ms-auto ps-2 ">
             <button

+ 2 - 2
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx

@@ -120,11 +120,11 @@ export const ExternalGroupManagement: FC = () => {
   const navTabMapping = useMemo(() => {
     return {
       ldap: {
-        Icon: () => <i className="fa fa-sitemap" />,
+        Icon: () => <span className="material-symbols-outlined">network_node</span>,
         i18n: 'LDAP',
       },
       keycloak: {
-        Icon: () => <i className="fa fa-key" />,
+        Icon: () => <span className="material-symbols-outlined">key</span>,
         i18n: 'Keycloak',
       },
     };

+ 2 - 1
apps/app/src/pages/trash.page.tsx

@@ -1,4 +1,5 @@
-import React, { ReactNode } from 'react';
+import type { ReactNode } from 'react';
+import React from 'react';
 
 import type { IUser, IUserHasId } from '@growi/core';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';

+ 33 - 0
apps/app/src/stores/use-editing-users.ts

@@ -0,0 +1,33 @@
+import { useCallback } from 'react';
+
+import type { IUserHasId } from '@growi/core';
+import { useSWRStatic } from '@growi/core/dist/swr';
+import type { SWRResponse } from 'swr';
+
+type EditingUsersStatus = {
+  userList: IUserHasId[],
+}
+
+type EditingUsersStatusUtils = {
+  onEditorsUpdated(
+    userList: IUserHasId[],
+  ): void,
+}
+
+export const useEditingUsers = (status?: EditingUsersStatus): SWRResponse<EditingUsersStatus, Error> & EditingUsersStatusUtils => {
+  const initialData: EditingUsersStatus = {
+    userList: [],
+  };
+  const swrResponse = useSWRStatic<EditingUsersStatus, Error>('editingUsers', status, { fallbackData: initialData });
+
+  const { mutate } = swrResponse;
+
+  const onEditorsUpdated = useCallback((userList: IUserHasId[]): void => {
+    mutate({ userList });
+  }, [mutate]);
+
+  return {
+    ...swrResponse,
+    onEditorsUpdated,
+  };
+};

+ 6 - 4
packages/editor/src/components/CodeMirrorEditorMain.tsx

@@ -2,6 +2,7 @@ import { useEffect } from 'react';
 
 import { type Extension } from '@codemirror/state';
 import { keymap, scrollPastEnd } from '@codemirror/view';
+import type { IUserHasId } from '@growi/core/dist/interfaces';
 
 import { GlobalCodeMirrorEditorKey } from '../consts';
 import { setDataLine } from '../services/extensions/setDataLine';
@@ -17,23 +18,24 @@ const additionalExtensions: Extension[] = [
 ];
 
 type Props = CodeMirrorEditorProps & {
-  userName?: string,
+  user?: IUserHasId,
   pageId?: string,
   initialValue?: string,
   onOpenEditor?: (markdown: string) => void,
+  onEditorsUpdated?: (userList: IUserHasId[]) => void,
 }
 
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
   const {
     acceptedUploadFileType,
-    indentSize, userName, pageId, initialValue,
+    indentSize, user, pageId, initialValue,
     editorTheme, editorKeymap,
-    onSave, onChange, onUpload, onScroll, onOpenEditor,
+    onSave, onChange, onUpload, onScroll, onOpenEditor, onEditorsUpdated,
   } = props;
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
-  useCollaborativeEditorMode(userName, pageId, initialValue, onOpenEditor, codeMirrorEditor);
+  useCollaborativeEditorMode(user, pageId, initialValue, onOpenEditor, onEditorsUpdated, codeMirrorEditor);
 
   // setup additional extensions
   useEffect(() => {

+ 1 - 4
packages/editor/src/services/file-dropzone/use-file-dropzone/use-file-dropzone.ts

@@ -36,10 +36,7 @@ export const useFileDropzone = (props: Props): FileDropzoneState => {
   }, [onUpload, setIsUploading, acceptedUploadFileType]);
 
   let accept: Accept | undefined;
-  if (acceptedUploadFileType === AcceptedUploadFileType.ALL) {
-    accept = { 'application/*': [] };
-  }
-  else if (acceptedUploadFileType === AcceptedUploadFileType.IMAGE) {
+  if (acceptedUploadFileType === AcceptedUploadFileType.IMAGE) {
     accept = { 'image/*': [] };
   }
 

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

@@ -1,6 +1,6 @@
 import { useEffect, useState } from 'react';
 
-import { GlobalSocketEventName } from '@growi/core/dist/interfaces';
+import { GlobalSocketEventName, type IUserHasId } from '@growi/core/dist/interfaces';
 import { useGlobalSocket, GLOBAL_SOCKET_NS } from '@growi/core/dist/swr';
 // see: https://github.com/yjs/y-codemirror.next#example
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -12,11 +12,19 @@ import * as Y from 'yjs';
 import { userColor } from '../consts';
 import { UseCodeMirrorEditor } from '../services';
 
+type UserLocalState = {
+  name: string;
+  user?: IUserHasId;
+  color: string;
+  colorLight: string;
+}
+
 export const useCollaborativeEditorMode = (
-    userName?: string,
+    user?: IUserHasId,
     pageId?: string,
     initialValue?: string,
     onOpenEditor?: (markdown: string) => void,
+    onEditorsUpdated?: (userList: IUserHasId[]) => void,
     codeMirrorEditor?: UseCodeMirrorEditor,
 ): void => {
   const [ydoc, setYdoc] = useState<Y.Doc | null>(null);
@@ -34,8 +42,8 @@ export const useCollaborativeEditorMode = (
     ydoc?.destroy();
     setYdoc(null);
 
-    // NOTICE: Destorying the provider leaves awareness in the other user's connection,
-    // so only awareness is destoryed here
+    // NOTICE: Destroying the provider leaves awareness in the other user's connection,
+    // so only awareness is destroyed here
     provider?.awareness.destroy();
 
     // TODO: catch ydoc:sync:error GlobalSocketEventName.YDocSyncError
@@ -43,6 +51,9 @@ export const useCollaborativeEditorMode = (
 
     setIsInit(false);
     setCPageId(pageId);
+
+    // reset editors
+    onEditorsUpdated?.([]);
   };
 
   const setupYDoc = () => {
@@ -50,7 +61,7 @@ export const useCollaborativeEditorMode = (
       return;
     }
 
-    // NOTICE: Old provider destory at the time of ydoc setup,
+    // NOTICE: Old provider destroy at the time of ydoc setup,
     // because the awareness destroying is not sync to other clients
     provider?.destroy();
     setProvider(null);
@@ -60,7 +71,7 @@ export const useCollaborativeEditorMode = (
   };
 
   const setupProvider = () => {
-    if (provider != null || ydoc == null || socket == null) {
+    if (provider != null || ydoc == null || socket == null || onEditorsUpdated == null) {
       return;
     }
 
@@ -71,15 +82,29 @@ export const useCollaborativeEditorMode = (
       { autoConnect: true },
     );
 
-    socketIOProvider.awareness.setLocalStateField('user', {
-      name: userName ? `${userName}` : `Guest User ${Math.floor(Math.random() * 100)}`,
+    const userLocalState: UserLocalState = {
+      name: user?.name ? `${user.name}` : `Guest User ${Math.floor(Math.random() * 100)}`,
+      user,
       color: userColor.color,
       colorLight: userColor.light,
-    });
+    };
+
+    socketIOProvider.awareness.setLocalStateField('user', userLocalState);
 
     socketIOProvider.on('sync', (isSync: boolean) => {
       if (isSync) {
         socket.emit(GlobalSocketEventName.YDocSync, { pageId, initialValue });
+        const userList: IUserHasId[] = Array.from(socketIOProvider.awareness.states.values(), value => value.user.user && value.user.user);
+        onEditorsUpdated(userList);
+      }
+    });
+
+    // update args type see: SocketIOProvider.Awareness.awarenessUpdate
+    socketIOProvider.awareness.on('update', (update: any) => {
+      const { added, removed } = update;
+      if (added.length > 0 || removed.length > 0) {
+        const userList: IUserHasId[] = Array.from(socketIOProvider.awareness.states.values(), value => value.user.user && value.user.user);
+        onEditorsUpdated(userList);
       }
     });
 
@@ -113,9 +138,9 @@ export const useCollaborativeEditorMode = (
     setIsInit(true);
   };
 
-  useEffect(cleanupYDocAndProvider, [cPageId, pageId, provider, socket, ydoc]);
+  useEffect(cleanupYDocAndProvider, [cPageId, onEditorsUpdated, pageId, provider, socket, ydoc]);
   useEffect(setupYDoc, [provider, ydoc]);
-  useEffect(setupProvider, [initialValue, pageId, provider, socket, userName, ydoc]);
+  useEffect(setupProvider, [initialValue, onEditorsUpdated, pageId, provider, socket, user, ydoc]);
   useEffect(setupYDocExtensions, [codeMirrorEditor, provider, ydoc]);
   useEffect(initializeEditor, [codeMirrorEditor, isInit, onOpenEditor, ydoc]);
 };

+ 2 - 2
packages/remark-attachment-refs/src/client/components/AttachmentList.tsx

@@ -28,7 +28,7 @@ export const AttachmentList = ({
     return (
       <div className="text-muted">
         <small>
-          <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
+          <span className="material-symbols-outlined fs-5 me-1" aria-hidden="true">info</span>
           {
             refsContext.options?.prefix != null
               ? `${refsContext.options.prefix} and descendant pages have no attachments`
@@ -51,7 +51,7 @@ export const AttachmentList = ({
     if (error != null) {
       return (
         <div className="text-warning">
-          <i className="fa fa-exclamation-triangle fa-fw"></i>
+          <span className="material-symbols-outlined me-1">warning</span>
           {refsContext.toString()} (-&gt; <small>{error.message}</small>)
         </div>
       );

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

@@ -53,7 +53,7 @@ const LsxSubstance = React.memo(({
     return (
       <details>
         <summary className="text-warning">
-          <i className="fa fa-exclamation-triangle fa-fw"></i> {lsxContext.toString()}
+          <span className="material-symbols-outlined me-1">warning</span> {lsxContext.toString()}
         </summary>
         <small className="ms-3 text-muted">{errorMessage}</small>
       </details>
@@ -137,7 +137,7 @@ LsxSubstance.displayName = 'LsxSubstance';
 const LsxDisabled = React.memo((): JSX.Element => {
   return (
     <div className="text-muted">
-      <i className="fa fa-fw fa-info-circle"></i>
+      <span className="material-symbols-outlined fs-5 me-1" aria-hidden="true">info</span>
       <small>lsx is not available on the share link page</small>
     </div>
   );

+ 1 - 1
packages/remark-lsx/src/client/components/LsxPageList/LsxListView.tsx

@@ -27,7 +27,7 @@ export const LsxListView = React.memo((props: Props): JSX.Element => {
       return (
         <div className="text-muted">
           <small>
-            <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
+            <span className="material-symbols-outlined fs-5 me-1" aria-hidden="true">info</span>
             $lsx(<a href={lsxContext.pagePath}>{lsxContext.pagePath}</a>) has no contents
           </small>
         </div>

+ 12 - 0
packages/ui/scss/molecules/_user-list-popover.scss

@@ -0,0 +1,12 @@
+%user-list-popover {
+  --bs-popover-max-width: 200px;
+  --bs-popover-body-padding-x: .5rem;
+  --bs-popover-body-padding-y: .5rem;
+
+  .user-list-content {
+    direction: rtl;
+  }
+  .cls-1 {
+    isolation: isolate;
+  }
+}

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

@@ -86,7 +86,7 @@ export const PageListMeta: FC<PageListMetaProps> = (props: PageListMetaProps) =>
 
   let likerCount;
   if (props.likerCount != null && props.likerCount > 0) {
-    likerCount = <span className={`${shouldSpaceOutIcon ? 'me-2' : ''}`}><i className="fa fa-heart-o" />{props.likerCount}</span>;
+    likerCount = <span className={`${shouldSpaceOutIcon ? 'me-2' : ''}`}><span className="material-symbols-outlined">favorite</span>{props.likerCount}</span>;
   }
 
   let locked;
@@ -96,7 +96,12 @@ export const PageListMeta: FC<PageListMetaProps> = (props: PageListMetaProps) =>
 
   let bookmarkCount;
   if (props.bookmarkCount != null && props.bookmarkCount > 0) {
-    bookmarkCount = <span className={`${shouldSpaceOutIcon ? 'me-2' : ''}`}><i className="fa fa-bookmark-o" />{props.bookmarkCount}</span>;
+    bookmarkCount = (
+      <span className={`${shouldSpaceOutIcon ? 'me-2' : ''}`}>
+        <span className="material-symbols-outlined">bookmark</span>
+        {props.bookmarkCount}
+      </span>
+    );
   }
 
   return (

+ 5 - 1
packages/ui/src/components/UserPicture.tsx

@@ -74,18 +74,22 @@ type Props = {
   size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl',
   noLink?: boolean,
   noTooltip?: boolean,
+  additionalClassName?: string
 };
 
 export const UserPicture = memo((props: Props): JSX.Element => {
 
   const {
-    user, size, noLink, noTooltip,
+    user, size, noLink, noTooltip, additionalClassName,
   } = props;
 
   const classNames = ['rounded-circle', 'picture'];
   if (size != null) {
     classNames.push(`picture-${size}`);
   }
+  if (additionalClassName != null) {
+    classNames.push(additionalClassName);
+  }
   const className = classNames.join(' ');
 
   if (user == null || !isUserObj(user)) {

+ 10 - 9
packages/ui/src/utils/use-rect.ts

@@ -1,6 +1,8 @@
-// ref: https://gist.github.com/morajabi/523d7a642d8c0a2f71fcfa0d8b3d2846?permalink_comment_id=4688158#gistcomment-4688158
+// based on https://gist.github.com/morajabi/523d7a642d8c0a2f71fcfa0d8b3d2846?permalink_comment_id=4688158#gistcomment-4688158
 
-import { useState, useRef, useEffect } from 'react';
+import {
+  useState, useEffect, RefObject, useCallback,
+} from 'react';
 
 type MutableRefObject<T> = {
   current: T
@@ -24,29 +26,28 @@ const useEffectInEvent = (
 };
 
 export const useRect = <T extends HTMLDivElement | null>(
+  reference: RefObject<T>,
   event: EventType = 'resize',
 ): [DOMRect | undefined, MutableRefObject<T | null>, number] => {
   const [rect, setRect] = useState<DOMRect>();
 
-  const reference = useRef<T>(null);
-
   const [screenHeight, setScreenHeight] = useState(window.innerHeight);
 
-  const set = (): void => {
+  const set = useCallback(() => {
     setRect(reference.current?.getBoundingClientRect());
-  };
+  }, [reference]);
 
   useEffectInEvent(event, true, set);
-  const handleResize = () => {
+  const handleResize = useCallback(() => {
     setScreenHeight(window.innerHeight);
-  };
+  }, []);
 
   useEffect(() => {
     window.addEventListener(event, handleResize);
     return () => {
       window.removeEventListener(event, handleResize);
     };
-  }, [event]);
+  }, [event, handleResize]);
 
   return [rect, reference, screenHeight];
 };

+ 2 - 18
yarn.lock

@@ -5093,7 +5093,7 @@ atob@^2.1.2:
   resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
   integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
 
-attr-accept@^2.2.1, attr-accept@^2.2.2:
+attr-accept@^2.2.2:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
   integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
@@ -7356,7 +7356,7 @@ depd@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
 
-dequal@^2.0.0, dequal@^2.0.2, dequal@^2.0.3:
+dequal@^2.0.0, dequal@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
   integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
@@ -8593,13 +8593,6 @@ file-entry-cache@^6.0.1:
   dependencies:
     flat-cache "^3.0.4"
 
-file-selector@^0.2.2:
-  version "0.2.4"
-  resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80"
-  integrity sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==
-  dependencies:
-    tslib "^2.0.3"
-
 file-selector@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc"
@@ -14696,15 +14689,6 @@ react-dom@^18.2.0:
     loose-envify "^1.1.0"
     scheduler "^0.23.0"
 
-react-dropzone@^11.2.4:
-  version "11.2.4"
-  resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.2.4.tgz#391a8d2e41a8a974340f83524d306540192e3313"
-  integrity sha512-EGSvK2CxFTuc28WxwuJCICyuYFX8b+sRumwU6Bs6sTbElV2HtQkT0d6C+HEee6XfbjiLIZ+Th9uji27rvo2wGw==
-  dependencies:
-    attr-accept "^2.2.1"
-    file-selector "^0.2.2"
-    prop-types "^15.7.2"
-
 react-dropzone@^14.2.3:
   version "14.2.3"
   resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b"