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

Merge branch 'dev/7.0.x' into imprv/142221-new-welcome-markdown

Shun Miyazawa 2 лет назад
Родитель
Сommit
0515dd079b
100 измененных файлов с 348 добавлено и 5433 удалено
  1. 0 1
      _obsolete/packages/.eslintignore
  2. 0 1
      _obsolete/packages/hackmd/.eslintignore
  3. 0 1
      _obsolete/packages/hackmd/.gitignore
  4. 0 25
      _obsolete/packages/hackmd/package.json
  5. 0 152
      _obsolete/packages/hackmd/src/hackmd-agent.js
  6. 0 41
      _obsolete/packages/hackmd/src/hackmd-styles.ts
  7. 0 16
      _obsolete/packages/hackmd/src/index.ts
  8. 0 21
      _obsolete/packages/hackmd/src/style.scss
  9. 0 14
      _obsolete/packages/hackmd/tsconfig.json
  10. 0 30
      _obsolete/packages/hackmd/vite.config.js
  11. 0 33
      apps/app/_obsolete/src/client/services/side-effects/hackmd-draft-updated.ts
  12. 0 51
      apps/app/_obsolete/src/client/util/codemirror/autorefresh.ext.js
  13. 0 47
      apps/app/_obsolete/src/client/util/codemirror/drawio-fold.ext.js
  14. 0 19
      apps/app/_obsolete/src/client/util/codemirror/gfm-growi.mode.js
  15. 0 41
      apps/app/_obsolete/src/client/util/codemirror/update-display-util.ext.js
  16. 0 50
      apps/app/_obsolete/src/components/Navbar/GlobalSearch.module.scss
  17. 0 144
      apps/app/_obsolete/src/components/Navbar/GlobalSearch.tsx
  18. 0 114
      apps/app/_obsolete/src/components/Navbar/GrowiNavbar.module.scss
  19. 0 141
      apps/app/_obsolete/src/components/Navbar/GrowiNavbar.tsx
  20. 0 103
      apps/app/_obsolete/src/components/Navbar/GrowiSubNavigation.module.scss
  21. 0 55
      apps/app/_obsolete/src/components/Navbar/GrowiSubNavigation.tsx
  22. 0 41
      apps/app/_obsolete/src/components/Navbar/GrowiSubNavigationSwitcher.module.scss
  23. 0 97
      apps/app/_obsolete/src/components/Navbar/GrowiSubNavigationSwitcher.tsx
  24. 0 144
      apps/app/_obsolete/src/components/PageEditor/AbstractEditor.tsx
  25. 0 50
      apps/app/_obsolete/src/components/PageEditor/Editor.module.scss
  26. 0 365
      apps/app/_obsolete/src/components/PageEditor/Editor.tsx
  27. 0 159
      apps/app/_obsolete/src/components/PageEditor/EditorIcon.jsx
  28. 0 62
      apps/app/_obsolete/src/components/PageEditor/EmojiPicker.tsx
  29. 0 116
      apps/app/_obsolete/src/components/PageEditor/EmojiPickerHelper.ts
  30. 0 46
      apps/app/_obsolete/src/components/PageEditor/MarkdownLinkUtil.js
  31. 0 95
      apps/app/_obsolete/src/components/PageEditor/MarkdownTableInterceptor.js
  32. 0 44
      apps/app/_obsolete/src/components/PageEditor/PasteHelper.js
  33. 0 44
      apps/app/_obsolete/src/components/PageEditor/PreventMarkdownListInterceptor.js
  34. 0 274
      apps/app/_obsolete/src/components/PageEditor/TextAreaEditor.jsx
  35. 0 521
      apps/app/_obsolete/src/components/PageEditorByHackmd.tsx
  36. 0 115
      apps/app/_obsolete/src/components/PageEditorByHackmd/HackmdEditor.jsx
  37. 0 101
      apps/app/_obsolete/src/components/UncontrolledCodeMirror.tsx
  38. 0 16
      apps/app/_obsolete/src/interfaces/hackmd.ts
  39. 0 346
      apps/app/_obsolete/src/server/routes/hackmd.js
  40. 0 22
      apps/app/_obsolete/src/stores/hackmd.ts
  41. 0 170
      apps/app/_obsolete/src/styles/_override.scss
  42. 0 32
      apps/app/_obsolete/src/styles/theme/_hsl-functions.scss
  43. 0 108
      apps/app/_obsolete/src/styles/theme/_hsl-reboot-bootstrap-theme-colors.scss
  44. 0 29
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-border-colors.scss
  45. 0 22
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-buttons.scss
  46. 0 60
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-colors.scss
  47. 0 38
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-dropdown.scss
  48. 0 52
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-nav.scss
  49. 0 74
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-tables.scss
  50. 0 3
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-text.scss
  51. 0 103
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-theme-colors.scss
  52. 0 9
      apps/app/_obsolete/src/styles/theme/mixins/_count-badge.scss
  53. 0 23
      apps/app/_obsolete/src/styles/theme/mixins/_hsl-badge.scss
  54. 0 146
      apps/app/_obsolete/src/styles/theme/mixins/_hsl-button.scss
  55. 0 72
      apps/app/_obsolete/src/styles/theme/mixins/_list-group.scss
  56. 0 22
      apps/app/_obsolete/src/styles/theme/mixins/_page-editor-mode-manager.scss
  57. 0 159
      apps/app/bin/cdn/cdn-resources-downloader.ts
  58. 0 33
      apps/app/bin/download-cdn-resources.ts
  59. 0 1
      apps/app/config/logger/config.dev.js
  60. 0 1
      apps/app/package.json
  61. 0 221
      apps/app/resource/cdn-manifests.js
  62. 30 9
      apps/app/src/client/services/side-effects/page-updated.ts
  63. 0 0
      apps/app/src/client/services/update-page/conflict.tsx
  64. 0 6
      apps/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  65. 0 6
      apps/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  66. 0 6
      apps/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  67. 4 4
      apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  68. 1 4
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  69. 1 4
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  70. 2 2
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  71. 5 4
      apps/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  72. 4 4
      apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  73. 2 8
      apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  74. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  75. 1 1
      apps/app/src/components/Admin/Users/PasswordResetModal.jsx
  76. 9 0
      apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss
  77. 4 4
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  78. 3 2
      apps/app/src/components/InAppNotification/PageNotification/ModelNotification.tsx
  79. 14 14
      apps/app/src/components/InAppNotification/PageNotification/useActionAndMsg.ts
  80. 1 1
      apps/app/src/components/InstallerForm.tsx
  81. 1 1
      apps/app/src/components/InvitedForm.tsx
  82. 1 1
      apps/app/src/components/Me/DisassociateModal.tsx
  83. 1 1
      apps/app/src/components/Me/ExternalAccountRow.jsx
  84. 0 2
      apps/app/src/components/Navbar/PageEditorModeManager.module.scss
  85. 1 1
      apps/app/src/components/PageAlert/PageRedirectedAlert.tsx
  86. 2 1
      apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  87. 1 1
      apps/app/src/components/PageComment/CommentControl.tsx
  88. 3 2
      apps/app/src/components/PageDeleteModal.tsx
  89. 3 3
      apps/app/src/components/PageEditor/HandsontableModal.tsx
  90. 45 85
      apps/app/src/components/PageEditor/PageEditor.tsx
  91. 135 0
      apps/app/src/components/PageEditor/conflict.tsx
  92. 37 98
      apps/app/src/components/PageStatusAlert.tsx
  93. 4 1
      apps/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx
  94. 25 0
      apps/app/src/components/ReactMarkdownComponents/Header.module.scss
  95. 1 1
      apps/app/src/components/ReactMarkdownComponents/Header.tsx
  96. 3 1
      apps/app/src/components/ReactMarkdownComponents/TableWithEditButton.tsx
  97. 1 1
      apps/app/src/components/RevisionComparer/RevisionComparer.tsx
  98. 1 1
      apps/app/src/components/SearchPage/SearchResultContent.tsx
  99. 1 1
      apps/app/src/components/UsersHomepageFooter.tsx
  100. 0 15
      apps/app/src/interfaces/interceptor-manager.ts

+ 0 - 1
_obsolete/packages/.eslintignore

@@ -1 +0,0 @@
-**/*

+ 0 - 1
_obsolete/packages/hackmd/.eslintignore

@@ -1 +0,0 @@
-/dist/**

+ 0 - 1
_obsolete/packages/hackmd/.gitignore

@@ -1 +0,0 @@
-/dist

+ 0 - 25
_obsolete/packages/hackmd/package.json

@@ -1,25 +0,0 @@
-{
-  "name": "@growi/hackmd",
-  "version": "7.0.0-RC.0",
-  "description": "GROWI js and css files to use hackmd",
-  "license": "MIT",
-  "type": "module",
-  "main": "dist/index.cjs",
-  "module": "dist/index.js",
-  "types": "dist/index.d.ts",
-  "scripts": {
-    "build": "vite build",
-    "clean": "shx rm -rf dist",
-    "dev": "vite build --mode dev",
-    "watch": "yarn dev -w --emptyOutDir=false",
-    "lint:js": "yarn eslint **/*.{js,ts}",
-    "lint:typecheck": "tsc",
-    "lint": "npm-run-all -p lint:*",
-    "version": "yarn version --no-git-tag-version --preid=RC"
-  },
-  "dependencies": {},
-  "devDependencies": {
-    "penpal": "^4.0.0",
-    "throttle-debounce": "^5.0.0"
-  }
-}

+ 0 - 152
_obsolete/packages/hackmd/src/hackmd-agent.js

@@ -1,152 +0,0 @@
-/**
- * GROWI agent for HackMD
- *
- * This file will be transpiled as a single JS
- *  and should be load from HackMD head via 'routes/hackmd.js' route
- *
- * USAGE:
- *  <script src="${hostname of GROWI}/_hackmd/load-agent"></script>
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- */
-import connectToParent from 'penpal/lib/connectToParent';
-import { debounce } from 'throttle-debounce';
-
-const DEBUG_PENPAL = false;
-
-/* eslint-disable no-console  */
-
-const allowedOrigin = '<%= origin %>'; // will be replaced by ejs
-
-
-/**
- * return the value of CodeMirror
- */
-function getValueOfCodemirror() {
-  // get CodeMirror instance
-  const editor = window.editor;
-  return editor.doc.getValue();
-}
-
-/**
- * set the specified document to CodeMirror
- * @param {string} value
- */
-function setValueToCodemirror(value) {
-  // get CodeMirror instance
-  const editor = window.editor;
-  editor.doc.setValue(value);
-}
-
-/**
- * set the specified document to CodeMirror on window loaded
- * @param {string} value
- */
-function setValueToCodemirrorOnInit(newValue) {
-  if (window.cmClient != null) {
-    setValueToCodemirror(newValue);
-    return;
-  }
-
-  const intervalId = setInterval(() => {
-    if (window.cmClient != null) {
-      clearInterval(intervalId);
-      setValueToCodemirror(newValue);
-    }
-  }, 250);
-
-}
-
-/**
- * postMessage to GROWI to notify body changes
- * @param {string} body
- */
-function postParentToNotifyBodyChanges(body) {
-  window.growi.notifyBodyChanges(body);
-}
-// generate debounced function
-const debouncedPostParentToNotifyBodyChanges = debounce(800, postParentToNotifyBodyChanges);
-
-/**
- * postMessage to GROWI to save with shortcut
- * @param {string} document
- */
-function postParentToSaveWithShortcut(document) {
-  window.growi.saveWithShortcut(document);
-}
-
-function addEventListenersToCodemirror() {
-  // get CodeMirror instance
-  const codemirror = window.CodeMirror;
-  // get CodeMirror editor instance
-  const editor = window.editor;
-
-  // e.g. 404 not found
-  if (codemirror == null || editor == null) {
-    return;
-  }
-
-  // == change event
-  editor.on('change', (cm, change) => {
-    if (change.origin === 'ignoreHistory') {
-      // do nothing because this operation triggered by other user
-      return;
-    }
-    debouncedPostParentToNotifyBodyChanges(cm.doc.getValue());
-  });
-
-  // == save event
-  // Reset save commands and Cmd-S/Ctrl-S shortcuts that initialized by HackMD
-  codemirror.commands.save = function(cm) {
-    postParentToSaveWithShortcut(cm.doc.getValue());
-  };
-  delete editor.options.extraKeys['Cmd-S'];
-  delete editor.options.extraKeys['Ctrl-S'];
-}
-
-function connectToParentWithPenpal() {
-  const connection = connectToParent({
-    parentOrigin: allowedOrigin,
-    // Methods child is exposing to parent
-    methods: {
-      getValue() {
-        return getValueOfCodemirror();
-      },
-      setValue(newValue) {
-        setValueToCodemirror(newValue);
-      },
-      setValueOnInit(newValue) {
-        setValueToCodemirrorOnInit(newValue);
-      },
-    },
-    debug: DEBUG_PENPAL,
-  });
-  connection.promise
-    .then((parent) => {
-      window.growi = parent;
-    })
-    .catch((err) => {
-      console.log(err);
-    });
-}
-
-/**
- * main
- */
-(function() {
-  // check HackMD is in iframe
-  if (window === window.parent) {
-    console.log('[GROWI] Loading agent for HackMD is not processed because currently not in iframe');
-    return;
-  }
-
-  console.log('[HackMD] Loading GROWI agent for HackMD...');
-
-  window.addEventListener('load', () => {
-    addEventListenersToCodemirror();
-  });
-
-  connectToParentWithPenpal();
-
-  console.log('[HackMD] GROWI agent for HackMD has successfully loaded.');
-}());

+ 0 - 41
_obsolete/packages/hackmd/src/hackmd-styles.ts

@@ -1,41 +0,0 @@
-/**
- * GROWI styles loader for HackMD
- *
- * This file will be transpiled as a single JS
- *  and should be load from HackMD head via 'routes/hackmd.js' route
- *
- * USAGE:
- *  <script src="${hostname of GROWI}/_hackmd/load-styles"></script>
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- */
-
-/* eslint-disable no-console  */
-
-const styles = '<%= styles %>'; // will be replaced by ejs
-
-/**
- * Insert link tag to load style file
- */
-function insertStyle() {
-  const element = document.createElement('style');
-  element.appendChild(document.createTextNode(unescape(styles)));
-  document.getElementsByTagName('head')[0].appendChild(element);
-}
-
-/**
- * main
- */
-(function() {
-  // check HackMD is in iframe
-  if (window === window.parent) {
-    console.log('[GROWI] Loading styles for HackMD is not processed because currently not in iframe');
-    return;
-  }
-
-  console.log('[HackMD] Loading GROWI styles for HackMD...');
-
-  insertStyle();
-
-  console.log('[HackMD] GROWI styles for HackMD has successfully loaded.');
-}());

+ 0 - 16
_obsolete/packages/hackmd/src/index.ts

@@ -1,16 +0,0 @@
-import fs from 'node:fs';
-import path from 'node:path';
-
-const isProduction = process.env.NODE_ENV === 'production';
-const dirPath = isProduction ? '.' : '../dist';
-const stylesJSFile = fs.readFileSync(path.resolve(__dirname, `${dirPath}/hackmd-styles.js`));
-const agentJSFile = fs.readFileSync(path.resolve(__dirname, `${dirPath}/hackmd-agent.js`));
-const stylesCSSFile = fs.readFileSync(path.resolve(__dirname, `${dirPath}/style.css`));
-
-// export to app as string
-const hackmdFiles = {
-  stylesJS: stylesJSFile.toString(),
-  agentJS: agentJSFile.toString(),
-  stylesCSS: stylesCSSFile.toString().replace(/(\r\n|\n|\r)/gm, ''), // https://stackoverflow.com/questions/10805125/how-to-remove-all-line-breaks-from-a-string
-};
-export default hackmdFiles;

+ 0 - 21
_obsolete/packages/hackmd/src/style.scss

@@ -1,21 +0,0 @@
-.navbar-header {
-  .navbar-brand {
-    display: none;
-  }
-}
-
-.navbar-form {
-  margin-left: 15px;
-}
-
-.navbar-right {
-  .ui-new, .ui-publish {
-    display: none;
-  }
-}
-
-.CodeMirror pre.CodeMirror-line {
-  font-family: Osaka-Mono, 'MS Gothic', Monaco, Menlo, Consolas, 'Courier New', monospace;
-  font-size: 14px;
-  line-height: 20px;
-}

+ 0 - 14
_obsolete/packages/hackmd/tsconfig.json

@@ -1,14 +0,0 @@
-{
-  "$schema": "http://json.schemastore.org/tsconfig",
-  "extends": "../../tsconfig.base.json",
-  "compilerOptions": {
-    "isolatedModules": false,
-
-    "baseUrl": ".",
-    "paths": {
-    }
-  },
-  "include": [
-    "src"
-  ]
-}

+ 0 - 30
_obsolete/packages/hackmd/vite.config.js

@@ -1,30 +0,0 @@
-import { defineConfig } from 'vite';
-import dts from 'vite-plugin-dts';
-
-
-// https://vitejs.dev/config/
-export default defineConfig({
-  plugins: [
-    dts({ copyDtsFiles: true }),
-  ],
-  build: {
-    outDir: 'dist',
-    lib: {
-      entry: [
-        'src/index.ts',
-        'src/hackmd-styles.ts',
-        'src/hackmd-agent.js',
-        'src/style.scss',
-      ],
-      name: 'hackmd-libs',
-      formats: ['es', 'cjs'],
-    },
-    rollupOptions: {
-      external: [
-        'node:fs',
-        'node:path',
-      ],
-    },
-    sourcemap: true,
-  },
-});

+ 0 - 33
apps/app/_obsolete/src/client/services/side-effects/hackmd-draft-updated.ts

@@ -1,33 +0,0 @@
-import { useCallback, useEffect } from 'react';
-
-import { SocketEventName } from '~/interfaces/websocket';
-import { useIsHackmdDraftUpdatingInRealtime } from '~/stores/hackmd';
-import { useCurrentPageId } from '~/stores/page';
-import { useGlobalSocket } from '~/stores/websocket';
-
-export const useHackmdDraftUpdatedEffect = (): void => {
-
-  const { data: currentPageId } = useCurrentPageId();
-  const { mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
-
-  const { data: socket } = useGlobalSocket();
-
-  const setIsHackmdDraftUpdatingInRealtime = useCallback((data) => {
-    const { s2cMessagePageUpdated } = data;
-    if (s2cMessagePageUpdated.pageId === currentPageId) {
-      mutateIsHackmdDraftUpdatingInRealtime(true);
-    }
-  }, [currentPageId, mutateIsHackmdDraftUpdatingInRealtime]);
-
-  // listen socket for hackmd saved
-  useEffect(() => {
-
-    if (socket == null) { return }
-
-    socket.on(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
-
-    return () => {
-      socket.off(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
-    };
-  }, [setIsHackmdDraftUpdatingInRealtime, socket]);
-};

+ 0 - 51
apps/app/_obsolete/src/client/util/codemirror/autorefresh.ext.js

@@ -1,51 +0,0 @@
-/**
- * extends codemirror/addon/display/autorefresh
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- * @see https://codemirror.net/addon/display/autorefresh.js
- * @see https://github.com/scniro/react-codemirror2/issues/83#issuecomment-398825212
- */
-/* eslint-disable */
-
-// CodeMirror, copyright (c) by Marijn Haverbeke and others
-// Distributed under an MIT license: http://codemirror.net/LICENSE
-
-(function(mod) {
-  mod(require("codemirror"));
-})(function(CodeMirror) {
-  "use strict"
-
-  CodeMirror.defineOption("autoRefresh", false, function(cm, val) {
-    if (cm.state.autoRefresh) {
-      stopListening(cm, cm.state.autoRefresh)
-      cm.state.autoRefresh = null
-    }
-    if (val && (val.force || cm.display.wrapper.offsetHeight == 0))
-      startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250})
-  })
-
-  function startListening(cm, state) {
-    function check() {
-      if (cm.display.wrapper.offsetHeight) {
-        stopListening(cm, state)
-        if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight)
-          cm.refresh()
-      } else {
-        state.timeout = setTimeout(check, state.delay)
-      }
-    }
-    state.timeout = setTimeout(check, state.delay)
-    state.hurry = function() {
-      clearTimeout(state.timeout)
-      state.timeout = setTimeout(check, 50)
-    }
-    CodeMirror.on(window, "mouseup", state.hurry)
-    CodeMirror.on(window, "keyup", state.hurry)
-  }
-
-  function stopListening(_cm, state) {
-    clearTimeout(state.timeout)
-    CodeMirror.off(window, "mouseup", state.hurry)
-    CodeMirror.off(window, "keyup", state.hurry)
-  }
-});

+ 0 - 47
apps/app/_obsolete/src/client/util/codemirror/drawio-fold.ext.js

@@ -1,47 +0,0 @@
-/* eslint-disable */
-
-import mdu from '../../../components/PageEditor/MarkdownDrawioUtil.js';
-
-(function(mod) {
-  mod(require("codemirror"));
-})(function(CodeMirror) {
-  "use strict"
-
-  CodeMirror.registerGlobalHelper('fold', 'drawio', function (mode, cm) {
-    return true;
-  }, function(cm, start) {
-    function isBeginningOfDrawio(lineNo) {
-      let line = cm.getLine(lineNo);
-      let match = mdu.lineBeginPartOfDrawioRE.exec(line);
-      if (match) {
-        return true;
-      }
-      return false;
-    }
-    function isEndOfDrawio(lineNo) {
-      let line = cm.getLine(lineNo);
-      let match = mdu.lineEndPartOfDrawioRE.exec(line);
-      if (match) {
-        return true;
-      }
-      return false;
-    }
-
-    let drawio = isBeginningOfDrawio(start.line);
-    if (drawio === false) { return; }
-
-    let lastLine = cm.lastLine();
-    let end = start.line;
-    while(end < lastLine) {
-      end += 1;
-      if (isEndOfDrawio(end)) {
-        break;
-      }
-    }
-
-    return {
-      from: CodeMirror.Pos(start.line, cm.getLine(start.line).length),
-      to: CodeMirror.Pos(end, cm.getLine(end).length)
-    };
-  });
-});

+ 0 - 19
apps/app/_obsolete/src/client/util/codemirror/gfm-growi.mode.js

@@ -1,19 +0,0 @@
-// https://discuss.codemirror.net/t/cm-header-margin-padding-height/75/5
-window.CodeMirror.defineMode('gfm-growi', (cmConfig, modeCfg) => {
-  // based on Markdown (GitHub-flavour) mode
-  // https://codemirror.net/doc/manual.html#option_mode
-  // https://codemirror.net/mode/index.html
-  modeCfg.name = 'gfm';
-  modeCfg.highlightFormatting = true;
-  const mode = window.CodeMirror.getMode(cmConfig, modeCfg);
-
-  const origToken = mode.token;
-  mode.token = function(stream, state) {
-    let classes = origToken(stream, state) || '';
-    // https://regex101.com/r/Fep0w2/1
-    classes = classes.replace(/(^| )header(\S*)/g, '$1header$2 line-grw-cm-header-line');
-    return /^\s*$/.test(classes) ? null : classes;
-  };
-
-  return mode;
-});

+ 0 - 41
apps/app/_obsolete/src/client/util/codemirror/update-display-util.ext.js

@@ -1,41 +0,0 @@
-import { sawCollapsedSpans } from 'codemirror/src/line/saw_special_spans';
-import { getLine } from 'codemirror/src/line/utils_line';
-import { heightAtLine, visualLineEndNo, visualLineNo } from 'codemirror/src/line/spans';
-import { DisplayUpdate } from 'codemirror/src/display/update_display';
-import { adjustView } from 'codemirror/src/display/view_tracking';
-
-class UpdateDisplayUtil {
-
-  /**
-   * Transplant 'updateDisplayIfNeeded' method to fix weseek/growi#703
-   *
-   * @see https://github.com/weseek/growi/issues/703
-   * @see https://github.com/codemirror/CodeMirror/blob/5.42.0/src/display/update_display.js#L125
-   *
-   * @param {CodeMirror} cm
-   */
-  static forceUpdateViewOffset(cm) {
-    const doc = cm.doc;
-    const display = cm.display;
-
-    const update = new DisplayUpdate(cm, cm.getViewport());
-
-    // Compute a suitable new viewport (from & to)
-    const end = doc.first + doc.size;
-    let from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first);
-    let to = Math.min(end, update.visible.to + cm.options.viewportMargin);
-    if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom);
-    if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo);
-    if (sawCollapsedSpans) {
-      from = visualLineNo(cm.doc, from);
-      to = visualLineEndNo(cm.doc, to);
-    }
-    adjustView(cm, from, to);
-
-    display.viewOffset = heightAtLine(getLine(doc, display.viewFrom));
-  }
-
-}
-
-
-export default UpdateDisplayUtil;

+ 0 - 50
apps/app/_obsolete/src/components/Navbar/GlobalSearch.module.scss

@@ -1,50 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-
-// input styles
-.grw-global-search :global {
-  .dropdown-toggle {
-    min-width: 95px;
-    padding-left: 1.5rem;
-  }
-
-  .search-typeahead {
-    .rbt-menu {
-      right: 0;
-      left: auto;
-
-      @include bs.media-breakpoint-up(md) {
-        right: auto;
-        left: 0;
-      }
-
-      @include bs.media-breakpoint-down(sm) {
-        left: auto !important;
-        width: 90vw;
-      }
-    }
-  }
-
-  // using react-bootstrap-typeahead
-  // see: https://github.com/ericgio/react-bootstrap-typeahead
-  .rbt-input.form-control {
-    height: 30px;
-    .rbt-input-wrapper {
-      margin-left: 8px;
-    }
-  }
-
-  .grw-shortcut-key-indicator {
-    position: absolute;
-    top: 0;
-    right: 4px;
-
-    display: flex;
-    align-items: center;
-    height: 30px;
-
-    code {
-      padding-right: 0.4rem;
-      padding-left: 0.4rem;
-    }
-  }
-}

+ 0 - 144
apps/app/_obsolete/src/components/Navbar/GlobalSearch.tsx

@@ -1,144 +0,0 @@
-import React, {
-  useState, useCallback, useRef, useEffect,
-} from 'react';
-
-import assert from 'assert';
-
-import { pathUtils } from '@growi/core/dist/utils';
-import { useTranslation } from 'next-i18next';
-import { useRouter } from 'next/router';
-
-import { IFocusable } from '~/client/interfaces/focusable';
-import { useKeywordManager } from '~/client/services/search-operation';
-import { IPageWithSearchMeta } from '~/interfaces/search';
-import {
-  useIsSearchScopeChildrenAsDefault, useIsSearchServiceReachable,
-} from '~/stores/context';
-import { useCurrentPagePath } from '~/stores/page';
-import { useGlobalSearchFormRef } from '~/stores/ui';
-
-import SearchForm from '../../../../src/components/SearchForm';
-
-import styles from './GlobalSearch.module.scss';
-
-
-export type GlobalSearchProps = {
-  dropup?: boolean,
-}
-
-export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
-  const { t } = useTranslation('commons');
-
-  const { dropup } = props;
-
-  const { returnPathForURL } = pathUtils;
-
-  const router = useRouter();
-
-  const globalSearchFormRef = useRef<IFocusable>(null);
-
-  useGlobalSearchFormRef(globalSearchFormRef);
-
-  const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
-  const { data: isSearchScopeChildrenAsDefault } = useIsSearchScopeChildrenAsDefault();
-  const { data: currentPagePath } = useCurrentPagePath();
-
-  const [text, setText] = useState('');
-  const [isScopeChildren, setScopeChildren] = useState<boolean|undefined>(isSearchScopeChildrenAsDefault ?? false);
-  const [isFocused, setFocused] = useState<boolean>(false);
-
-  const { pushState } = useKeywordManager();
-
-  useEffect(() => {
-    setScopeChildren(isSearchScopeChildrenAsDefault);
-  }, [isSearchScopeChildrenAsDefault]);
-
-
-  const gotoPage = useCallback((data: IPageWithSearchMeta[]) => {
-    assert(data.length > 0);
-
-    const page = data[0].data; // should be single page selected
-
-    // navigate to page
-    if (page != null) {
-      router.push(returnPathForURL(page.path, page._id));
-    }
-  }, [returnPathForURL, router]);
-
-  const search = useCallback(() => {
-    // construct search query
-    let q = text;
-    if (isScopeChildren) {
-      q += ` prefix:${currentPagePath ?? window.location.pathname}`;
-    }
-
-    pushState(q);
-  }, [currentPagePath, isScopeChildren, router, text]);
-
-  const scopeLabel = isScopeChildren
-    ? t('header_search_box.label.This tree')
-    : t('header_search_box.label.All pages');
-
-  const isIndicatorShown = !isFocused && (text.length === 0);
-
-
-  if (isScopeChildren == null || isSearchServiceReachable == null) {
-    return <></>;
-  }
-
-  return (
-    <div className={`grw-global-search ${styles['grw-global-search']} mb-0 d-print-none ${isSearchServiceReachable ? '' : 'has-error'}`}>
-      <div className="input-group flex-nowrap">
-        <div className={` ${dropup ? 'dropup' : ''}`}>
-          <button
-            className="btn btn-secondary dropdown-toggle py-0"
-            type="button"
-            data-bs-toggle="dropdown"
-            aria-haspopup="true"
-            data-testid="select-search-scope"
-          >
-            {scopeLabel}
-          </button>
-          <div className="dropdown-menu">
-            <button
-              className="dropdown-item"
-              type="button"
-              onClick={() => {
-                setScopeChildren(false);
-                globalSearchFormRef.current?.focus();
-              }}
-            >
-              { t('header_search_box.item_label.All pages') }
-            </button>
-            <button
-              data-tesid="search-current-tree"
-              className="dropdown-item"
-              type="button"
-              onClick={() => {
-                setScopeChildren(true);
-                globalSearchFormRef.current?.focus();
-              }}
-            >
-              { t('header_search_box.item_label.This tree') }
-            </button>
-          </div>
-        </div>
-        <SearchForm
-          ref={globalSearchFormRef}
-          isSearchServiceReachable={isSearchServiceReachable || false}
-          dropup={dropup}
-          onChange={gotoPage}
-          onBlur={() => setFocused(false)}
-          onFocus={() => setFocused(true)}
-          onInputChange={text => setText(text)}
-          onSubmit={search}
-        />
-        { isIndicatorShown && (
-          <span className="grw-shortcut-key-indicator">
-            <code className="bg-transparent text-muted">/</code>
-          </span>
-        ) }
-      </div>
-    </div>
-  );
-};

+ 0 - 114
apps/app/_obsolete/src/components/Navbar/GrowiNavbar.module.scss

@@ -1,114 +0,0 @@
-@use '~/styles/variables' as var;
-@use '~/styles/bootstrap/init' as bs;
-@use '~/styles/mixins';
-
-.grw-navbar :global {
-
-  .confidential {
-    font-weight: bold;
-  }
-
-}
-
-.grw-navbar :global {
-  top: #{-1 * var.$grw-navbar-height} !important;
-
-  z-index: var.$grw-navbar-z-index !important;
-  max-height: var.$grw-navbar-height + var.$grw-navbar-border-width;
-  border-top: 0;
-  border-right: 0;
-  border-bottom: var.$grw-navbar-border-width solid;
-  border-left: 0;
-
-  .grw-app-title {
-    @include mixins.variable-font-size(24px);
-  }
-
-  .grw-navbar-search {
-    position: absolute;
-    left: 50%;
-    transform: translate(-50%, 0%);
-  }
-
-  .nav-link,
-  .nav-item.confidential {
-    display: flex;
-    align-items: center;
-    min-height: var.$grw-navbar-height;
-    padding: 0 1rem;
-  }
-
-  .nav-link {
-    &:hover {
-      background: rgba(0, 0, 0, 0.1);
-    }
-
-    &:focus {
-      background: rgba(0, 0, 0, 0);
-    }
-  }
-  .nav-item.confidential {
-    :not(i) {
-      @include mixins.variable-font-size(14px);
-    }
-
-    @include bs.media-breakpoint-only(md) {
-      max-width: 100px;
-    }
-
-    max-width: 120px;
-    max-height: var.$grw-navbar-height;
-    overflow: hidden;
-    background: rgba(0, 0, 0, 0.2);
-  }
-
-  .grw-notification-dropdown {
-    .dropdown-menu {
-      max-width: 70vw;
-    }
-  }
-}
-
-// layout for GlobalSearch
-.grw-navbar :global {
-  .grw-global-search-container {
-    // centering on navbar
-    top: var.$grw-navbar-height / 2;
-    left: 50vw;
-    z-index: bs.$zindex-fixed + 1;
-    transform: translate(-50%, -50%);
-
-    .rbt-input.form-control {
-      width: 200px;
-      transition: 0.3s ease-out;
-
-      // focus
-      &.focus {
-        width: 300px;
-      }
-
-      @include bs.media-breakpoint-up(md) {
-        width: 300px;
-      }
-      @include bs.media-breakpoint-up(lg) {
-        // focus
-        &.focus {
-          width: 400px;
-        }
-      }
-      @include bs.media-breakpoint-up(xl) {
-        width: 350px;
-        // focus
-        &.focus {
-          width: 450px;
-        }
-      }
-    }
-  }
-}
-
-.grw-notification-badge {
-  position: absolute;
-  top: 6px;
-  right: 3.5px;
-}

+ 0 - 141
apps/app/_obsolete/src/components/Navbar/GrowiNavbar.tsx

@@ -1,141 +0,0 @@
-import React, {
-  FC, memo, useMemo, useRef,
-} from 'react';
-
-import { useTranslation } from 'next-i18next';
-import dynamic from 'next/dynamic';
-import { useRipple } from 'react-use-ripple';
-import { UncontrolledTooltip } from 'reactstrap';
-
-import {
-  useIsSearchPage, useIsGuestUser, useIsReadOnlyUser, useIsSearchServiceConfigured, useAppTitle, useConfidential,
-} from '~/stores/context';
-import { usePageCreateModal } from '~/stores/modal';
-import { useCurrentPagePath } from '~/stores/page';
-import { useIsDeviceSmallerThanMd } from '~/stores/ui';
-
-
-import { GlobalSearchProps } from './GlobalSearch';
-
-import styles from './GrowiNavbar.module.scss';
-
-const NavbarRight = memo((): JSX.Element => {
-  const { t } = useTranslation();
-
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: isGuestUser } = useIsGuestUser();
-  const { data: isReadOnlyUser } = useIsReadOnlyUser();
-
-  // ripple
-  const newButtonRef = useRef(null);
-  useRipple(newButtonRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
-
-  const { open: openCreateModal } = usePageCreateModal();
-
-  const isAuthenticated = isGuestUser === false;
-
-  const authenticatedNavItem = useMemo(() => {
-    return (
-      <>
-        {!isReadOnlyUser
-          && (
-            <li className="nav-item d-none d-md-block">
-              <button
-                className="px-md-3 nav-link btn-create-page border-0 bg-transparent"
-                type="button"
-                ref={newButtonRef}
-                data-testid="newPageBtn"
-                onClick={() => openCreateModal(currentPagePath || '')}
-              >
-                <span className="material-symbols-outlined">edit</span>
-                <span className="d-none d-lg-block">{ t('commons:New') }</span>
-              </button>
-            </li>
-          )
-        }
-      </>
-    );
-  }, [isReadOnlyUser, t, openCreateModal, currentPagePath]);
-
-  const notAuthenticatedNavItem = useMemo(() => {
-    return (
-      <>
-        <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>
-      </>
-    );
-  }, []);
-
-  return (
-    <>
-      {isAuthenticated ? authenticatedNavItem : notAuthenticatedNavItem}
-    </>
-  );
-});
-NavbarRight.displayName = 'NavbarRight';
-
-type ConfidentialProps = {
-  confidential?: string,
-}
-const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps): JSX.Element => {
-  const { confidential } = props;
-
-  if (confidential == null || confidential.length === 0) {
-    return <></>;
-  }
-
-  return (
-    <li className="nav-item confidential text-light">
-      <i id="confidentialTooltip"></i><span className="material-symbols-outlined d-md-none">info</span>
-      <span className="d-none d-md-inline">
-        {confidential}
-      </span>
-      <UncontrolledTooltip
-        placement="bottom"
-        target="confidentialTooltip"
-        className="d-md-none"
-      >
-        {confidential}
-      </UncontrolledTooltip>
-    </li>
-  );
-});
-Confidential.displayName = 'Confidential';
-
-type Props = {
-  isGlobalSearchHidden?: boolean
-}
-
-export const GrowiNavbar = (props: Props): JSX.Element => {
-
-  const { isGlobalSearchHidden } = props;
-
-  const GlobalSearch = dynamic<GlobalSearchProps>(() => import('./GlobalSearch').then(mod => mod.GlobalSearch), { ssr: false });
-
-  const { data: appTitle } = useAppTitle();
-  const { data: confidential } = useConfidential();
-  const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured();
-  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
-  const { data: isSearchPage } = useIsSearchPage();
-
-  return (
-    <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
-
-      <div className="grw-app-title d-none d-md-block">
-        {appTitle}
-      </div>
-
-      {/* Navbar Right  */}
-      <ul className="navbar-nav ms-auto">
-        <NavbarRight />
-        <Confidential confidential={confidential} />
-      </ul>
-
-      <div className="grw-global-search-container position-absolute">
-        { !isGlobalSearchHidden && isSearchServiceConfigured && !isDeviceSmallerThanMd && !isSearchPage && (
-          <GlobalSearch />
-        ) }
-      </div>
-    </nav>
-  );
-
-};

+ 0 - 103
apps/app/_obsolete/src/components/Navbar/GrowiSubNavigation.module.scss

@@ -1,103 +0,0 @@
-@use '~/styles/variables' as var;
-@use '@growi/core/scss/bootstrap/init' as bs;
-@use '~/styles/mixins';
-
-%subnav-buttons-height {
-  height: 40px;
-}
-
-%compact-subnav-buttons-height {
-  height: 32px;
-}
-
-// https://github.com/css-modules/css-modules/issues/295#issuecomment-404873976
-// workaround to use '&' in global scope
-.grw-subnav {
-  :global {
-    min-height: var.$grw-subnav-min-height;
-    padding-top: 8px;
-    padding-bottom: 8px;
-
-    @include bs.media-breakpoint-up(md) {
-      min-height: var.$grw-subnav-min-height-md;
-    }
-
-    h1 {
-      @include mixins.variable-font-size(32px);
-      line-height: 1.4em;
-    }
-
-    .btn-copy {
-      &:not(:hover):not(:active) {
-        background-color: transparent !important;
-      }
-      opacity: 0.5;
-    }
-
-    .btn-subscribe {
-      @extend %subnav-buttons-height;
-      font-size: 20px;
-    }
-
-    .btn-like,
-    .btn-bookmark,
-    .btn-seen-user {
-      @extend %subnav-buttons-height;
-      padding-right: 6px;
-      padding-left: 8px;
-      font-size: 20px;
-      svg {
-        width: 20px;
-        height: 20px;
-      }
-    }
-    .total-likes,
-    .total-bookmarks {
-      display: flex;
-      align-items: flex-end;
-      padding-right: 8px;
-      padding-left: 6px;
-      font-size: 14px;
-      font-weight: bs.$font-weight-bold;
-    }
-    .seen-user-count {
-      padding-right: 6px;
-      padding-left: 6px;
-      font-size: 14px;
-      font-weight: bs.$font-weight-bold;
-      vertical-align: bottom;
-    }
-
-    .btn-page-item-control {
-      height: 40px;
-      font-size: 16px;
-    }
-
-    .user-list-popover {
-      max-width: 200px;
-
-      .user-list-content {
-        direction: rtl;
-
-        .liker-user-count,
-        .seen-user-count {
-          font-size: 12px;
-          font-weight: bolder;
-        }
-      }
-      .cls-1 {
-        isolation: isolate;
-      }
-    }
-  }
-
-  &:global {
-    &:hover {
-      .btn-copy,
-      .btn-edit-tags {
-        // change button opacity
-        opacity: unset;
-      }
-    }
-  }
-}

+ 0 - 55
apps/app/_obsolete/src/components/Navbar/GrowiSubNavigation.tsx

@@ -1,55 +0,0 @@
-import React from 'react';
-
-import {
-  EditorMode, useEditorMode,
-} from '~/stores/ui';
-
-import PagePathNav from '../PagePathNav';
-
-
-import styles from './GrowiSubNavigation.module.scss';
-
-
-export type GrowiSubNavigationProps = {
-  pagePath?: string,
-  pageId?: string,
-  isNotFound?: boolean,
-  isTagLabelsDisabled?: boolean,
-  tags?: string[],
-  rightComponent?: React.FunctionComponent,
-  additionalClasses?: string[],
-}
-
-export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element => {
-
-  const { data: editorMode } = useEditorMode();
-
-  const {
-    pageId, pagePath,
-    rightComponent: RightComponent,
-    additionalClasses = [],
-  } = props;
-
-  const isViewMode = editorMode === EditorMode.View;
-  const isEditorMode = !isViewMode;
-
-  return (
-    <div className={`
-      grw-subnav ${styles['grw-subnav']} d-flex align-items-center justify-content-between
-      ${additionalClasses.join(' ')}`}
-    >
-      {/* Left side */}
-      <div className="d-flex grw-subnav-start-side">
-        <div className="grw-path-nav-container">
-          { pagePath != null && (
-            <PagePathNav pageId={pageId} pagePath={pagePath} isSingleLineMode={isEditorMode} />
-          ) }
-        </div>
-      </div>
-      {/* Right side. */}
-      { RightComponent && (
-        <RightComponent />
-      ) }
-    </div>
-  );
-};

+ 0 - 41
apps/app/_obsolete/src/components/Navbar/GrowiSubNavigationSwitcher.module.scss

@@ -1,41 +0,0 @@
-@use '~/styles/variables' as var;
-@use '@growi/core/scss/bootstrap/init' as bs;
-
-/*
- * Fixed ver
- */
-$easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
-
-.grw-subnav-fixed-container {
-  top: var.$grw-navbar-border-width;
-  z-index: bs.$zindex-sticky - 5;
-}
-
-/*
- * Switching show/hide
- */
-.grw-subnav-switcher {
-  :global {
-    .grw-subnav-fixed-container {
-      transition: transform 150ms $easeInOutCubic;
-    }
-
-    /*
-    * shadow
-    */
-    .grw-subnav-append-shadow-container {
-      .grw-subnav {
-        box-shadow: 0px 0px 6px 3px rgba(black, 0.15);
-      }
-    }
-  }
-
-  &:global {
-    &.grw-subnav-switcher-hidden {
-      .grw-subnav-fixed-container {
-        transition: unset;
-        transform: translateY(-100%);
-      }
-    }
-  }
-}

+ 0 - 97
apps/app/_obsolete/src/components/Navbar/GrowiSubNavigationSwitcher.tsx

@@ -1,97 +0,0 @@
-import React, {
-  useState, useRef, useEffect, useCallback,
-} from 'react';
-
-import { debounce } from 'throttle-debounce';
-
-import { useSticky } from '~/client/services/side-effects/use-sticky';
-import { useSWRxCurrentPage } from '~/stores/page';
-import { useSidebarCollapsed } from '~/stores/ui';
-import loggerFactory from '~/utils/logger';
-
-import GrowiContextualSubNavigation from './GrowiContextualSubNavigation';
-
-import styles from './GrowiSubNavigationSwitcher.module.scss';
-
-const logger = loggerFactory('growi:cli:GrowiSubNavigationSticky');
-
-export type GrowiSubNavigationSwitcherProps = {
-  isLinkSharingDisabled: boolean,
-}
-
-/**
- * GrowiSubNavigation
- *
- * needs:
- *   #grw-subnav-fixed-container element
- *   #grw-subnav-sticky-trigger element
- */
-export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProps): JSX.Element => {
-  const { isLinkSharingDisabled } = props;
-
-  const { data: currentPage } = useSWRxCurrentPage();
-  const { data: isSidebarCollapsed } = useSidebarCollapsed();
-
-  const [width, setWidth] = useState<number>(0);
-
-  // use more specific type HTMLDivElement for avoid assertion error.
-  // see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement
-  const fixedContainerRef = useRef<HTMLDivElement>(null);
-  const clientWidth = fixedContainerRef.current?.parentElement?.clientWidth;
-
-  // Get sticky status
-  const isSticky = useSticky('#grw-subnav-sticky-trigger');
-
-  // Do not use clientWidth as useCallback deps, resizing events will not work in production builds.
-  const initWidth = useCallback(() => {
-    if (fixedContainerRef.current != null && fixedContainerRef.current.parentElement != null) {
-      // get parent elements width
-      const { clientWidth } = fixedContainerRef.current.parentElement;
-      setWidth(clientWidth);
-    }
-  }, []);
-
-  // setup effect by resizing event
-  useEffect(() => {
-    const resizeHandler = debounce(100, initWidth);
-    window.addEventListener('resize', resizeHandler);
-
-    // return clean up handler
-    return () => {
-      window.removeEventListener('resize', resizeHandler);
-    };
-  }, [initWidth]);
-
-  // update width when sidebar collapsing changed
-  useEffect(() => {
-    if (isSidebarCollapsed != null) {
-      setTimeout(initWidth, 300);
-    }
-  }, [isSidebarCollapsed, initWidth]);
-
-  /*
-   * initialize width.
-   * Since width is not recalculated at production build first rendering,
-   * make initWidth execution dependent on clientWidth.
-   */
-  useEffect(() => {
-    if (clientWidth != null) initWidth();
-  }, [initWidth, clientWidth]);
-
-  if (currentPage == null) {
-    return <></>;
-  }
-
-  return (
-    <div className={`${styles['grw-subnav-switcher']} ${isSticky ? '' : 'grw-subnav-switcher-hidden'}`} data-testid="grw-subnav-switcher">
-      <div
-        id="grw-subnav-fixed-container"
-        className={`grw-subnav-fixed-container ${styles['grw-subnav-fixed-container']} position-fixed grw-subnav-append-shadow-container`}
-        ref={fixedContainerRef}
-        style={{ width }}
-      >
-        <GrowiContextualSubNavigation currentPage={currentPage} isCompactMode isLinkSharingDisabled={isLinkSharingDisabled} />
-      </div>
-    </div>
-  );
-};

+ 0 - 144
apps/app/_obsolete/src/components/PageEditor/AbstractEditor.tsx

@@ -1,144 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import React from 'react';
-
-import { ICodeMirror } from 'react-codemirror2';
-
-
-export interface AbstractEditorProps extends ICodeMirror {
-  value?: string;
-  isGfmMode?: boolean;
-  onScrollCursorIntoView?: (line: number) => void;
-  onSave?: () => Promise<void>;
-  onPasteFiles?: (event: Event) => void;
-  onCtrlEnter?: (event: Event) => void;
-}
-
-interface defaultProps {
-  isGfmMode: true,
-}
-
-export default class AbstractEditor<T extends AbstractEditorProps> extends React.Component<T, Record<string, unknown>> {
-
-  constructor(props: Readonly<T>) {
-    super(props);
-
-    this.forceToFocus = this.forceToFocus.bind(this);
-    this.setCaretLine = this.setCaretLine.bind(this);
-    this.setScrollTopByLine = this.setScrollTopByLine.bind(this);
-
-    this.getStrFromBol = this.getStrFromBol.bind(this);
-    this.getStrToEol = this.getStrToEol.bind(this);
-    this.insertText = this.insertText.bind(this);
-    this.insertLinebreak = this.insertLinebreak.bind(this);
-
-    this.dispatchSave = this.dispatchSave.bind(this);
-  }
-
-  public static defaultProps: defaultProps = {
-    isGfmMode: true,
-  };
-
-  forceToFocus(): void {}
-
-  /**
-   * set new value
-   */
-  setValue(_newValue: string): void {}
-
-  /**
-   * Enable/Disable GFM mode
-   * @param {bool} _bool
-   */
-  setGfmMode(_bool: boolean): void {}
-
-  /**
-   * set caret position of codemirror
-   * @param {string} number
-   */
-  setCaretLine(_line: number): void {}
-
-  /**
-   * scroll
-   * @param {number} _line
-   */
-  setScrollTopByLine(_line: number): void {}
-
-  /**
-   * return strings from BOL(beginning of line) to current position
-   */
-  getStrFromBol(): Error {
-    throw new Error('this method should be impelemented in subclass');
-  }
-
-  /**
-   * return strings from current position to EOL(end of line)
-   */
-  getStrToEol(): Error {
-    throw new Error('this method should be impelemented in subclass');
-  }
-
-  /**
-   * return strings from BOL(beginning of line) to current position
-   */
-  getStrFromBolToSelectedUpperPos(): Error {
-    throw new Error('this method should be impelemented in subclass');
-  }
-
-  /**
-   * replace Beggining Of Line to current position with param 'text'
-   * @param {string} _text
-   */
-  replaceBolToCurrentPos(_text: string): Error {
-    throw new Error('this method should be impelemented in subclass');
-  }
-
-  /**
-   * replace the current line with param 'text'
-   * @param {string} _text
-   */
-  replaceLine(_text: string): Error {
-    throw new Error('this method should be impelemented in subclass');
-  }
-
-  /**
-   * insert text
-   * @param {string} _text
-   */
-  insertText(_text: string): Error {
-    throw new Error('this method should be impelemented in subclass');
-  }
-
-  /**
-   * insert line break to the current position
-   */
-  insertLinebreak(): void {
-    this.insertText('\n');
-  }
-
-  /**
-   * dispatch onSave event
-   */
-  dispatchSave(): void {
-    if (this.props.onSave != null) {
-      this.props.onSave();
-    }
-  }
-
-  /**
-   * dispatch onPasteFiles event
-   * @param {object} event
-   */
-  dispatchPasteFiles(event: Event): void {
-    if (this.props.onPasteFiles != null) {
-      this.props.onPasteFiles(event);
-    }
-  }
-
-  /**
-   * returns items(an array of react elements) in navigation bar for editor
-   */
-  getNavbarItems(): null {
-    return null;
-  }
-
-}

+ 0 - 50
apps/app/_obsolete/src/components/PageEditor/Editor.module.scss

@@ -1,50 +0,0 @@
-@use '~/styles/mixins' as ms;
-@use '@growi/core/scss/bootstrap/init' as bs;
-@use './page-editor-inheritance';
-
-
-.editor-container :global {
-
-  .btn.btn-open-dropzone {
-    z-index: 2;
-    padding-top: 3px;
-    padding-bottom: 3px;
-    font-size: small;
-    border: none;
-    border-top: 1px dotted bs.$gray-300;
-    border-bottom: none;
-
-    &:hover,
-    &:focus {
-      border-bottom: none;
-    }
-  }
-
-  // for Navbar editor
-  .navbar-editor {
-    height: page-editor-inheritance.$navbar-editor-height;
-    padding: 0;
-
-    border-bottom: 1px solid transparent;
-
-    li {
-      display: inline-block;
-      i {
-        font-size: 16px;
-      }
-    }
-
-    button {
-      padding: 0px;
-      margin: 0 2px;
-      font-size: 1rem;
-      line-height: 1;
-      background-color: transparent;
-      border: none;
-    }
-
-    img {
-      vertical-align: bottom;
-    }
-  }
-}

+ 0 - 365
apps/app/_obsolete/src/components/PageEditor/Editor.tsx

@@ -1,365 +0,0 @@
-import type { ForwardRefRenderFunction } from 'react';
-import React, {
-  useState, useRef, useImperativeHandle, useCallback, forwardRef,
-  memo,
-  useEffect,
-} from 'react';
-
-import type { EditorSettings } from '@growi/editor';
-import Dropzone from 'react-dropzone';
-import { useTranslation } from 'react-i18next';
-import {
-  Modal, ModalHeader, ModalBody,
-} from 'reactstrap';
-
-import { toastError, toastSuccess } from '~/client/util/toastr';
-import { useDefaultIndentSize } from '~/stores/context';
-import { useEditorSettings } from '~/stores/editor';
-import { useIsMobile } from '~/stores/ui';
-
-import type { IEditorMethods } from '../../interfaces/editor-methods';
-
-import type AbstractEditor from './AbstractEditor';
-import { Cheatsheet } from './Cheatsheet';
-// import CodeMirrorEditor from './CodeMirrorEditor';
-import pasteHelper from './PasteHelper';
-import TextAreaEditor from './TextAreaEditor';
-
-
-import styles from './Editor.module.scss';
-
-export type EditorPropsType = {
-  value?: string,
-  isGfmMode?: boolean,
-  noCdn?: boolean,
-  isUploadable?: boolean,
-  isUploadAllFileAllowed?: boolean,
-  onChange?: (newValue: string, isClean?: boolean) => void,
-  onUpload?: (file) => void,
-  editorSettings?: EditorSettings,
-  indentSize?: number,
-  onDragEnter?: (event: any) => void,
-  onMarkdownHelpButtonClicked?: () => void,
-  onAddAttachmentButtonClicked?: () => void,
-  onScroll?: (line: { line: number }) => void,
-  onScrollCursorIntoView?: (line: number) => void,
-  onSave?: () => Promise<void>,
-  onPasteFiles?: (event: Event) => void,
-  onCtrlEnter?: (event: Event) => void,
-  isComment?: boolean,
-}
-
-type DropzoneRef = {
-  open: () => void
-}
-
-const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props, ref): JSX.Element => {
-  const {
-    onUpload, isUploadable, isUploadAllFileAllowed, indentSize, isGfmMode = true,
-  } = props;
-
-  const [dropzoneActive, setDropzoneActive] = useState(false);
-  const [isUploading, setIsUploading] = useState(false);
-  const [isCheatsheetModalShown, setIsCheatsheetModalShown] = useState(false);
-
-  const [navBarItems, setNavBarItems] = useState<JSX.Element[]>([]);
-
-  const { t } = useTranslation();
-  const { data: editorSettings } = useEditorSettings();
-  const { data: defaultIndentSize } = useDefaultIndentSize();
-  const { data: isMobile } = useIsMobile();
-
-  const dropzoneRef = useRef<DropzoneRef>(null);
-  // CodeMirrorEditor ref
-  const cmEditorRef = useRef<AbstractEditor<any>>(null);
-  const taEditorRef = useRef<TextAreaEditor>(null);
-
-  const editorSubstance = useCallback(() => {
-    return isMobile ? taEditorRef.current : cmEditorRef.current;
-  }, [isMobile]);
-
-  // methods for ref
-  useImperativeHandle(ref, () => ({
-    forceToFocus: () => {
-      editorSubstance()?.forceToFocus();
-    },
-    setValue: (newValue: string) => {
-      editorSubstance()?.setValue(newValue);
-    },
-    setGfmMode: (bool: boolean) => {
-      editorSubstance()?.setGfmMode(bool);
-    },
-    setCaretLine: (line: number) => {
-      editorSubstance()?.setCaretLine(line);
-    },
-    setScrollTopByLine: (line: number) => {
-      editorSubstance()?.setScrollTopByLine(line);
-    },
-    insertText: (text: string) => {
-      editorSubstance()?.insertText(text);
-    },
-    /**
-     * remove overlay and set isUploading to false
-     */
-    terminateUploadingState: () => {
-      setDropzoneActive(false);
-      setIsUploading(false);
-    },
-  }));
-
-  /**
-   * dispatch onUpload event
-   */
-  const dispatchUpload = useCallback((files) => {
-    if (onUpload != null) {
-      onUpload(files);
-    }
-  }, [onUpload]);
-
-  /**
-   * get acceptable(uploadable) file type
-   */
-  const getAcceptableType = useCallback(() => {
-    let accept = 'null'; // reject all
-    if (isUploadable) {
-      if (!isUploadAllFileAllowed) {
-        accept = 'image/*'; // image only
-      }
-      else {
-        accept = ''; // allow all
-      }
-    }
-
-    return accept;
-  }, [isUploadable, isUploadAllFileAllowed]);
-
-  const pasteFilesHandler = useCallback((event) => {
-    const items = event.clipboardData.items || event.clipboardData.files || [];
-
-    // abort if length is not 1
-    if (items.length < 1) {
-      return;
-    }
-
-    for (let i = 0; i < items.length; i++) {
-      try {
-        const file = items[i].getAsFile();
-        // check file type (the same process as Dropzone)
-        if (file != null && pasteHelper.isAcceptableType(file, getAcceptableType())) {
-          dispatchUpload(file);
-          setIsUploading(true);
-        }
-      }
-      catch (e) {
-        toastError(t('toaster.file_upload_failed'));
-      }
-    }
-  }, [dispatchUpload, getAcceptableType, t]);
-
-  const dragEnterHandler = useCallback((event) => {
-    const dataTransfer = event.dataTransfer;
-
-    // do nothing if contents is not files
-    if (!dataTransfer.types.includes('Files')) {
-      return;
-    }
-
-    setDropzoneActive(true);
-  }, []);
-
-  const dropHandler = useCallback((accepted) => {
-    // rejected
-    if (accepted.length !== 1) { // length should be 0 or 1 because `multiple={false}` is set
-      setDropzoneActive(false);
-      return;
-    }
-
-    const file = accepted[0];
-    dispatchUpload(file);
-    setIsUploading(true);
-  }, [dispatchUpload]);
-
-  const addAttachmentHandler = useCallback(() => {
-    if (dropzoneRef.current == null) { return }
-    dropzoneRef.current.open();
-  }, []);
-
-  const getDropzoneClassName = useCallback((isDragAccept: boolean, isDragReject: boolean) => {
-    let className = 'dropzone';
-    if (!isUploadable) {
-      className += ' dropzone-unuploadable';
-    }
-    else {
-      className += ' dropzone-uploadable';
-
-      if (isUploadAllFileAllowed) {
-        className += ' dropzone-uploadablefile';
-      }
-    }
-
-    // uploading
-    if (isUploading) {
-      className += ' dropzone-uploading';
-    }
-
-    if (isDragAccept) {
-      className += ' dropzone-accepted';
-    }
-
-    if (isDragReject) {
-      className += ' dropzone-rejected';
-    }
-
-    return className;
-  }, [isUploadable, isUploading, isUploadAllFileAllowed]);
-
-  const renderDropzoneOverlay = useCallback(() => {
-    return (
-      <div className="overlay overlay-dropzone-active">
-        {isUploading
-          && (
-            <span className="overlay-content">
-              <div className="speeding-wheel d-inline-block"></div>
-              <span className="visually-hidden">Uploading...</span>
-            </span>
-          )
-        }
-        {!isUploading && <span className="overlay-content"></span>}
-      </div>
-    );
-  }, [isUploading]);
-
-  const renderNavbar = () => {
-    return (
-      <div className="m-0 navbar navbar-default navbar-editor" data-testid="navbar-editor" style={{ minHeight: 'unset' }}>
-        <ul className="ps-2 nav nav-navbar">
-          { navBarItems.map((item, idx) => {
-            // eslint-disable-next-line react/no-array-index-key
-            return <li key={`navbarItem-${idx}`}>{item}</li>;
-          }) }
-        </ul>
-      </div>
-    );
-  };
-
-  const renderCheatsheetModal = useCallback(() => {
-    const hideCheatsheetModal = () => {
-      setIsCheatsheetModalShown(false);
-    };
-
-    return (
-      <Modal isOpen={isCheatsheetModalShown} toggle={hideCheatsheetModal} className={`modal-gfm-cheatsheet ${styles['modal-gfm-cheatsheet']}`} size="lg">
-        <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
-          <span className="material-symbols-outlined me-1">help</span>Markdown help
-        </ModalHeader>
-        <ModalBody>
-          <Cheatsheet />
-        </ModalBody>
-      </Modal>
-    );
-  }, [isCheatsheetModalShown]);
-
-  const isReadyToRenderEditor = editorSettings != null;
-
-  // https://redmine.weseek.co.jp/issues/111731
-  useEffect(() => {
-    const editorRef = editorSubstance();
-    if (isReadyToRenderEditor && editorRef != null) {
-      const editorNavBarItems = editorRef.getNavbarItems() ?? [];
-      setNavBarItems(editorNavBarItems);
-    }
-  }, [editorSubstance, isReadyToRenderEditor]);
-
-  if (!isReadyToRenderEditor) {
-    return <></>;
-  }
-
-  const flexContainer: React.CSSProperties = {
-    height: '100%',
-    display: 'flex',
-    flexDirection: 'column',
-  };
-
-  return (
-    <>
-      <div style={flexContainer} className={`editor-container ${styles['editor-container']}`}>
-        <Dropzone
-          ref={dropzoneRef}
-          accept={getAcceptableType()}
-          noClick
-          noKeyboard
-          multiple={false}
-          onDragLeave={() => { setDropzoneActive(false) }}
-          onDrop={dropHandler}
-        >
-          {({
-            getRootProps,
-            getInputProps,
-            isDragAccept,
-            isDragReject,
-          }) => {
-            return (
-              <div className={getDropzoneClassName(isDragAccept, isDragReject)} {...getRootProps()}>
-                { dropzoneActive && renderDropzoneOverlay() }
-
-                { renderNavbar() }
-
-                {/* for PC */}
-                { !isMobile && (
-                  // <CodeMirrorEditor
-                  //   ref={cmEditorRef}
-                  //   indentSize={indentSize ?? defaultIndentSize}
-                  //   onPasteFiles={pasteFilesHandler}
-                  //   onDragEnter={dragEnterHandler}
-                  //   onMarkdownHelpButtonClicked={() => { setIsCheatsheetModalShown(true) }}
-                  //   onAddAttachmentButtonClicked={addAttachmentHandler}
-                  //   editorSettings={editorSettings}
-                  //   isGfmMode={isGfmMode}
-                  //   {...props}
-                  // />
-                  <></>
-                )}
-
-                {/* for mobile */}
-                { isMobile && (
-                  <TextAreaEditor
-                    ref={taEditorRef}
-                    onPasteFiles={pasteFilesHandler}
-                    onDragEnter={dragEnterHandler}
-                    {...props}
-                  />
-                )}
-
-                <input {...getInputProps()} />
-              </div>
-            );
-          }}
-        </Dropzone>
-
-        { isUploadable
-          && (
-            <button
-              type="button"
-              className="btn btn-outline-secondary btn-open-dropzone"
-              onClick={addAttachmentHandler}
-            >
-              <span className="material-symbols-outlined" aria-hidden="true">attachment</span>&nbsp;
-              Attach files
-              <span className="d-none d-sm-inline">
-              &nbsp;by dragging &amp; dropping,&nbsp;
-                <span className="btn-link">selecting them</span>,&nbsp;
-                or pasting from the clipboard.
-              </span>
-
-            </button>
-          )
-        }
-
-        { renderCheatsheetModal() }
-
-      </div>
-    </>
-  );
-};
-
-export default memo(forwardRef(Editor));

+ 0 - 159
apps/app/_obsolete/src/components/PageEditor/EditorIcon.jsx

@@ -1,159 +0,0 @@
-/* eslint-disable max-len */
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-const EditorIcon = (props) => {
-
-  switch (props.icon) {
-    case 'Bold':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M9.71,21.14V8.86A.84.84,0,0,1,10.59,8h4.46c2.41,0,4.05,1.41,4.05,3.52a3.17,3.17,0,0,1-2.44,3.08v.07a3.39,3.39,0,0,1,3.15,3.47c0,2.48-1.78,4-4.78,4H10.59A.84.84,0,0,1,9.71,21.14ZM14.11,14c2.08,0,3.21-.83,3.21-2.36s-1-2.16-2.67-2.16H11.47V14Zm.66,6.46c2.12,0,3.23-.86,3.23-2.49s-1.15-2.46-3.4-2.46H11.47v4.95Z" />
-        </svg>
-      );
-    case 'Italic':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M18.55,8a.48.48,0,0,1,.45.5.45.45,0,0,1-.39.5H16.75L14.51,21h1.62a.45.45,0,0,1,.46.5.47.47,0,0,1-.54.5h-4.6a.53.53,0,0,1-.47-.5.47.47,0,0,1,.48-.5h2L15.83,9H14a.52.52,0,0,1-.5-.5A.51.51,0,0,1,14,8Z" />
-        </svg>
-      );
-    case 'Strikethrough':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M22.5,14H7.5a.47.47,0,0,0-.5.5.46.46,0,0,0,.5.5h15a.5.5,0,0,0,0-1Z" />
-          <path d="M18,17a2.21,2.21,0,0,1,.6,1.88c-.07.51-.53,2.18-3.31,2.18a5.35,5.35,0,0,1-4.21-1.76L11,18.5c-.05-.3-.21-.5-.5-.5s-.45.17-.5.5v1A5.79,5.79,0,0,0,15,22c3.75,0,4.41-2.11,4.53-2.53A3.12,3.12,0,0,0,19.28,17Z" />
-          <path d="M12.21,13h1.91c-1.27-.44-2.37-1.52-2.1-2.5.18-.65,1-1.59,3.27-1.59a4.21,4.21,0,0,1,3.44,1.41l.07.37a.55.55,0,1,0,1.08-.19l-.09-.5-.08-.2A6.28,6.28,0,0,0,15,8c-3.11,0-3.95,1.74-4,2.33A2.32,2.32,0,0,0,12.21,13Z" />
-        </svg>
-      );
-    case 'Heading':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M10,21V9.11a.61.61,0,1,1,1.22,0v5.24h7.55V9.11a.59.59,0,0,1,.62-.64.58.58,0,0,1,.61.64V21a.58.58,0,0,1-.61.63.59.59,0,0,1-.62-.63V15.46H11.22V21A.61.61,0,1,1,10,21Z" />
-        </svg>
-      );
-    case 'InlineCode':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M11,19.57a.54.54,0,0,1-.36-.14L7.05,15.79a.49.49,0,0,1,0-.67l4.09-5a.5.5,0,0,1,.71-.07.5.5,0,0,1,.07.7L8.08,15.41l3.31,3.31a.5.5,0,0,1,0,.71A.54.54,0,0,1,11,19.57Z" /><path d="M18.5,20a.51.51,0,0,1-.32-.12.5.5,0,0,1-.07-.7l3.81-4.63-3.36-3.36a.5.5,0,0,1,0-.71.51.51,0,0,1,.71,0L23,14.21a.49.49,0,0,1,0,.67l-4.09,5A.52.52,0,0,1,18.5,20Z" /><path d="M13,21.5a.41.41,0,0,1-.16,0,.5.5,0,0,1-.32-.63l4-12a.5.5,0,0,1,.63-.31.49.49,0,0,1,.32.63l-4,12A.49.49,0,0,1,13,21.5Z" />
-        </svg>
-      );
-    case 'Quote':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M11.5,14h9a.5.5,0,0,1,.5.5h0a.5.5,0,0,1-.5.5h-9a.5.5,0,0,1-.5-.5h0A.5.5,0,0,1,11.5,14Z" /><path d="M8.5,9h11a.5.5,0,0,1,.5.5h0a.5.5,0,0,1-.5.5H8.5A.5.5,0,0,1,8,9.5H8A.5.5,0,0,1,8.5,9Z" /><path d="M11.5,19h7a.5.5,0,0,1,.5.5h0a.5.5,0,0,1-.5.5h-7a.5.5,0,0,1-.5-.5h0A.5.5,0,0,1,11.5,19Z" /><path d="M8,20.5v-8a.5.5,0,0,1,.5-.5h0a.5.5,0,0,1,.5.5v8a.5.5,0,0,1-.5.5h0A.5.5,0,0,1,8,20.5Z" />
-        </svg>
-      );
-    case 'List':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <circle cx="8.5" cy="9.5" r="1" /><circle cx="8.5" cy="14.5" r="1" /><circle cx="8.5" cy="19.5" r="1" /><path d="M11.5,9h10a.5.5,0,0,1,.5.5h0a.5.5,0,0,1-.5.5h-10a.5.5,0,0,1-.5-.5h0A.5.5,0,0,1,11.5,9Z" /><path d="M11.5,14h10a.5.5,0,0,1,.5.5h0a.5.5,0,0,1-.5.5h-10a.5.5,0,0,1-.5-.5h0A.5.5,0,0,1,11.5,14Z" /><path d="M11.5,19h10a.5.5,0,0,1,.5.5h0a.5.5,0,0,1-.5.5h-10a.5.5,0,0,1-.5-.5h0A.5.5,0,0,1,11.5,19Z" />
-        </svg>
-      );
-    case 'NumberedList':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M11.5,9h10a.5.5,0,0,1,.5.5h0a.5.5,0,0,1-.5.5h-10a.5.5,0,0,1-.5-.5h0A.5.5,0,0,1,11.5,9Z" /><path d="M11.5,19h10a.5.5,0,0,1,.5.5h0a.5.5,0,0,1-.5.5h-10a.5.5,0,0,1-.5-.5h0A.5.5,0,0,1,11.5,19Z" /><path d="M11.5,14h10a.5.5,0,0,1,.5.5h0a.5.5,0,0,1-.5.5h-10a.5.5,0,0,1-.5-.5h0A.5.5,0,0,1,11.5,14Z" /><path d="M7.44,12h.85V8.62L7.58,9h-.1a.24.24,0,0,1-.29-.19.25.25,0,0,1,.19-.3l.83-.41A.77.77,0,0,1,8.5,8h.08a.29.29,0,0,1,.29.27V12H9.6a.27.27,0,1,1,.1.53H7.44a.27.27,0,0,1,0-.53Z" /><path d="M7.61,17.13a.26.26,0,0,1-.26.26h0c-.14,0-.27-.09-.26-.32v-.4a.48.48,0,0,1,.27-.42,1.93,1.93,0,0,1,1-.25,1.3,1.3,0,0,1,1.42,1.15v.22a2.54,2.54,0,0,1-1,1.74l-1,.94H9.58a.27.27,0,0,1,0,.53H7.32A.32.32,0,0,1,7,20.26v0a.47.47,0,0,1,.2-.34l1.2-1.17a2.12,2.12,0,0,0,.79-1.36.8.8,0,0,0-.75-.85H8.35a1.32,1.32,0,0,0-.7.2Z" />
-        </svg>
-      );
-    case 'CheckList':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M20.5,12.5a.57.57,0,0,1,.5.5v8a1.17,1.17,0,0,1-1,1H10a1.28,1.28,0,0,1-1-1V11a1.28,1.28,0,0,1,1-1h6a.57.57,0,0,1,.5.5.57.57,0,0,1-.5.5H10V21H20V13A.58.58,0,0,1,20.5,12.5ZM12,15.25a.41.41,0,0,0,0,.58L14,18a.78.78,0,0,0,1,0l5.9-9c.09-.16.09-.5-.16-.58a.41.41,0,0,0-.58.08L14.5,17.16l-1.91-1.91A.41.41,0,0,0,12,15.25Z" />
-        </svg>
-      );
-    case 'Link':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M12.12,18a.29.29,0,0,0,.43,0l5.3-5.3c.07-.08.3-.25.06-.51a.32.32,0,0,0-.53,0l-5.22,5.28C12.09,17.57,11.85,17.73,12.12,18Zm2.34-.9a1.74,1.74,0,0,1,0,1,3.69,3.69,0,0,1-.43.78L12.2,20.5a1.69,1.69,0,0,1-1.31.61A1.77,1.77,0,0,1,9.5,20.5a1.65,1.65,0,0,1-.61-1.31A1.74,1.74,0,0,1,9.5,17.8L11.15,16a1.74,1.74,0,0,1,1.92-.43l.69-.7a2.67,2.67,0,0,0-1.21-.26,2.89,2.89,0,0,0-2,.78L8.89,17.19a2.8,2.8,0,0,0-.09,3.92l0,0a2.62,2.62,0,0,0,2.05.83,2.79,2.79,0,0,0,2-.87l1.74-1.66A3.93,3.93,0,0,0,15.42,18a2.56,2.56,0,0,0-.26-1.56Zm6.61-8.18a2.11,2.11,0,0,0-.87-.69,2.69,2.69,0,0,0-3,.69l-1.83,1.66a2.68,2.68,0,0,0-.78,1.56,2.72,2.72,0,0,0,.26,1.66l.69-.7a2,2,0,0,1,0-1,2,2,0,0,1,.44-1l1.83-1.66L18,9.27l.35-.17.34-.18h.44a1.67,1.67,0,0,1,1.3.61,1.74,1.74,0,0,1,.61,1.4,1.62,1.62,0,0,1-.61,1.3l-1.74,1.83-.6.35a1.37,1.37,0,0,1-.79.17H17l-.69.7a8.58,8.58,0,0,0,1,.22,2.9,2.9,0,0,0,1.21-.22l.87-.61,1.74-1.83a2.6,2.6,0,0,0,.23-3.69Z" />
-        </svg>
-      );
-    case 'Image':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M22,8H8A1,1,0,0,0,7,9V21a1,1,0,0,0,1,1H22a1,1,0,0,0,1-1V9A1,1,0,0,0,22,8Zm0,13H8V18l4.07-4.06,4.07,4a.41.41,0,0,0,.33.18.4.4,0,0,0,.32-.18l1.7-1.55,3.17,3.25L22,20Zm0-2.25-3.1-3.34a.89.89,0,0,0-.33-.17.89.89,0,0,0-.28.14l-1.83,1.49-4-3.9a.49.49,0,0,0-.32-.16.5.5,0,0,0-.41.16L8,16.75V9H22ZM19.5,12.5a1,1,0,1,1-1-1A1,1,0,0,1,19.5,12.5Z" />
-        </svg>
-      );
-    case 'Grid':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30">
-          <rect width="30" height="30" fill="none" />
-          <g transform="translate(-375 -415)">
-            <g transform="translate(382 422)">
-              <path d="M5,7H1A.945.945,0,0,1,0,6V1A.945.945,0,0,1,1,0H5A.945.945,0,0,1,6,1V6A.945.945,0,0,1,5,7ZM1,1V6H5V1ZM1,.5V1H1Z" />
-            </g>
-            <g transform="translate(390 422)">
-              <path d="M7,7H1A.945.945,0,0,1,0,6V1A.945.945,0,0,1,1,0H7A.945.945,0,0,1,8,1V6A.945.945,0,0,1,7,7ZM1,1V6H7V1ZM1,.5V1H1Z" />
-            </g>
-            <g transform="translate(382 431)">
-              <path d="M9,7H1A.945.945,0,0,1,0,6V1A.945.945,0,0,1,1,0H9a.945.945,0,0,1,1,1V6A.945.945,0,0,1,9,7ZM1,1V6H9V1ZM1,.5V1H1Z" />
-            </g>
-            <g transform="translate(394 431)">
-              <path d="M3,7H1A.945.945,0,0,1,0,6V1A.945.945,0,0,1,1,0H3A.945.945,0,0,1,4,1V6A.945.945,0,0,1,3,7ZM1,1V6H3V1ZM1,.5V1H1Z" />
-            </g>
-          </g>
-        </svg>
-      );
-    case 'Table':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M8,22a1,1,0,0,1-1-1V9A1,1,0,0,1,8,8H22c1,0,1,.47,1,1V21a1,1,0,0,1-1,1Zm10-4v3h4V18Zm-5,0v3h4V18ZM8,18v3h4V18Zm10-4v3h4V14Zm-5,0v3h4V14ZM8,14v3h4V14Zm10-4v3h4V10Zm-5,0v3h4V10ZM8,10v3h4V10Z" />
-        </svg>
-      );
-    case 'Drawio':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M22.12,17H19.75l-3.12-4H18a1,1,0,0,0,1-1V8a1,1,0,0,0-1-1H12a1,1,0,0,0-1,1v4a1,1,0,0,0,1,1h1.38l-2.92,4H7.88A.94.94,0,0,0,7,18v4a.94.94,0,0,0,.88,1h5.24A.94.94,0,0,0,14,22V18a.94.94,0,0,0-.88-1H11.63l3.13-4h.47l3.13,4H16.88A.94.94,0,0,0,16,18v4a.94.94,0,0,0,.88,1h5.24A.94.94,0,0,0,23,22V18A.94.94,0,0,0,22.12,17ZM13,22H8V18h5ZM12,8h6v4H12ZM22,22H17V18h5Z" />
-        </svg>
-      );
-    case 'Attachment':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-          <rect fillOpacity="0" width="30" height="30" />
-          <path d="M9.71,22.5a2.57,2.57,0,0,1-1.85-.79,2.79,2.79,0,0,1,0-4l9-9.23a3.21,3.21,0,0,1,1.59-.87,3.39,3.39,0,0,1,1.81.1,4.38,4.38,0,0,1,1.7,1.05,4.15,4.15,0,0,1,.46.56,3.73,3.73,0,0,1,.35.65,4.25,4.25,0,0,1,.2.72,3.91,3.91,0,0,1,.07.76,3.71,3.71,0,0,1-1.12,2.67l-6.79,7a.48.48,0,0,1-.34.16.51.51,0,0,1-.35-.13.48.48,0,0,1,0-.7l6.78-7a2.8,2.8,0,0,0,.84-2,2.58,2.58,0,0,0-.79-2,3.63,3.63,0,0,0-1.11-.75,2.41,2.41,0,0,0-1.31-.17,2.19,2.19,0,0,0-1.25.62l-9,9.22A1.8,1.8,0,0,0,8,19.69,1.78,1.78,0,0,0,8.58,21a1.81,1.81,0,0,0,.57.39,1.48,1.48,0,0,0,.66.1,2,2,0,0,0,1.28-.62l7.12-7.35.15-.16a1.15,1.15,0,0,0,.15-.2.9.9,0,0,0,.12-.24,1.17,1.17,0,0,0,.07-.25.52.52,0,0,0-.05-.27.75.75,0,0,0-.19-.26.73.73,0,0,0-.58-.27,1.29,1.29,0,0,0-.67.38l-5.36,5.53a.5.5,0,0,1-.22.13.46.46,0,0,1-.26,0,.48.48,0,0,1-.22-.12A.41.41,0,0,1,11,17.5a.5.5,0,0,1,.14-.35L16.5,11.6a2.19,2.19,0,0,1,1.29-.67,1.69,1.69,0,0,1,1.37.55,1.54,1.54,0,0,1,.53,1.31,2.26,2.26,0,0,1-.76,1.42L11.8,21.58a3.06,3.06,0,0,1-2,.91H9.71Z" />
-        </svg>
-      );
-    case 'Emoji':
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30">
-          <g transform="translate(-435 -392)">
-            <rect width="30" height="30" transform="translate(435 392)" fillOpacity="0" />
-            <path d="M8,1a7,7,0,1,0,7,7A7.008,7.008,0,0,0,8,1M8,0A8,8,0,1,1,0,8,8,8,0,0,1,8,0Z" transform="translate(442 399)" />
-            <circle cx="1" cy="1" r="1" transform="translate(446 403)" />
-            <circle cx="1" cy="1" r="1" transform="translate(452 403)" />
-            <g transform="translate(445 406.5)">
-              <path d="M5,5.5a5.006,5.006,0,0,1-5-5,.5.5,0,1,1,1,0,4,4,0,0,0,8,0,.5.5,0,0,1,1,0A5.006,5.006,0,0,1,5,5.5Z" />
-            </g>
-          </g>
-        </svg>
-      );
-    case 'Template':
-      // TODO: fix
-      return (
-        <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" className="bi bi-filetype-md" viewBox="-2 -3 28 21">
-          <path fillRule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2H9v-1h3a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5L14 4.5ZM.706 13.189v2.66H0V11.85h.806l1.14 2.596h.026l1.14-2.596h.8v3.999h-.716v-2.66h-.038l-.946 2.159h-.516l-.952-2.16H.706Zm3.919 2.66V11.85h1.459c.406 0 .741.078 1.005.234.263.157.46.383.589.68.13.297.196.655.196 1.075 0 .422-.066.784-.196 1.084-.131.301-.33.53-.595.689-.264.158-.597.237-1 .237H4.626Zm1.353-3.354h-.562v2.707h.562c.186 0 .347-.028.484-.082a.8.8 0 0 0 .334-.252 1.14 1.14 0 0 0 .196-.422c.045-.168.067-.365.067-.592a2.1 2.1 0 0 0-.117-.753.89.89 0 0 0-.354-.454c-.159-.102-.362-.152-.61-.152Z" />
-        </svg>
-      );
-  }
-
-
-};
-
-EditorIcon.propTypes = {
-  icon: PropTypes.string.isRequired,
-};
-
-export default EditorIcon;

+ 0 - 62
apps/app/_obsolete/src/components/PageEditor/EmojiPicker.tsx

@@ -1,62 +0,0 @@
-import React, { FC, useCallback } from 'react';
-
-import { Picker } from 'emoji-mart';
-import { Modal } from 'reactstrap';
-
-import { useNextThemes } from '~/stores/use-next-themes';
-
-import EmojiPickerHelper, { getEmojiTranslation } from './EmojiPickerHelper';
-
-
-import 'emoji-mart/css/emoji-mart.css';
-
-
-type Props = {
-  onClose: () => void,
-  onSelected: (emoji: string) => void,
-  emojiSearchText: string,
-  emojiPickerHelper: EmojiPickerHelper,
-  isOpen: boolean
-}
-
-const EmojiPicker: FC<Props> = (props: Props) => {
-
-  const {
-    onClose, onSelected, emojiSearchText, emojiPickerHelper, isOpen,
-  } = props;
-
-  const { resolvedTheme } = useNextThemes();
-
-  // Set search emoji input and trigger search
-  const searchEmoji = useCallback(() => {
-    const input = window.document.querySelector('[id^="emoji-mart-search"]') as HTMLInputElement;
-    const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
-    valueSetter?.call(input, emojiSearchText);
-    const event = new Event('input', { bubbles: true });
-    input.dispatchEvent(event);
-    input.focus();
-  }, [emojiSearchText]);
-
-  const selectEmoji = useCallback((emoji) => {
-    onSelected(emoji);
-    onClose();
-  }, [onClose, onSelected]);
-
-
-  const translation = getEmojiTranslation();
-
-  return (
-    <Modal isOpen={isOpen} toggle={onClose} onOpened={searchEmoji} backdropClassName="emoji-picker-modal" fade={false}>
-      <Picker
-        onSelect={selectEmoji}
-        i18n={translation}
-        title={translation.title}
-        emojiTooltip
-        style={emojiPickerHelper.setStyle()}
-        theme={resolvedTheme}
-      />
-    </Modal>
-  );
-};
-
-export default EmojiPicker;

+ 0 - 116
apps/app/_obsolete/src/components/PageEditor/EmojiPickerHelper.ts

@@ -1,116 +0,0 @@
-import { CSSProperties } from 'react';
-
-import { Position } from 'codemirror';
-import i18n from 'i18next';
-
-// https://regex101.com/r/x5LbOZ/1
-const EMOJI_PATTERN = new RegExp(/^:[a-z0-9-+_]+$/);
-
-export default class EmojiPickerHelper {
-
-  editor;
-
-  pattern: string;
-
-  constructor(editor) {
-    this.editor = editor;
-  }
-
-  setStyle = (): CSSProperties => {
-    const offset = 20;
-    const emojiPickerHeight = 420;
-    const cursorPos = this.editor.cursorCoords(true);
-    const editorPos = this.editor.getWrapperElement().getBoundingClientRect();
-    // Emoji Picker bottom position exceed editor's bottom position
-    if (cursorPos.bottom + emojiPickerHeight > editorPos.bottom) {
-      return {
-        top: editorPos.bottom - emojiPickerHeight,
-        left: cursorPos.left + offset,
-        position: 'fixed',
-      };
-    }
-    return {
-      top: cursorPos.top + offset,
-      left: cursorPos.left + offset,
-      position: 'fixed',
-    };
-  };
-
-  shouldModeTurnOn = (char: string): Position | null | undefined => {
-    if (char !== ':') {
-      return null;
-    }
-
-    const currentPos = this.editor.getCursor();
-    const sc = this.editor.getSearchCursor(':', currentPos, { multiline: false });
-    if (sc.findPrevious()) {
-      return sc.pos.from;
-    }
-  };
-
-  shouldOpen = (startPos: Position): boolean => {
-    const currentPos = this.editor.getCursor();
-    const rangeStr = this.editor.getRange(startPos, currentPos);
-
-    return EMOJI_PATTERN.test(rangeStr);
-  };
-
-  getInitialSearchingText = (startPos: Position): void => {
-    const currentPos = this.editor.getCursor();
-    const rangeStr = this.editor.getRange(startPos, currentPos);
-
-    return rangeStr.slice(1); // return without the heading ':'
-  };
-
-  addEmoji = (emoji: { colons: string }, startPosToReplace: Position|null): void => {
-    const currentPos = this.editor.getCursor();
-
-    const from = startPosToReplace ?? currentPos;
-    const to = currentPos;
-
-    const doc = this.editor.getDoc();
-    doc.replaceRange(`${emoji.colons} `, from, to);
-    this.editor.focus();
-    this.editor.refresh();
-  };
-
-}
-
-export const getEmojiTranslation = () => {
-
-  const categories = {};
-  [
-    'search',
-    'recent',
-    'smileys',
-    'people',
-    'nature',
-    'foods',
-    'activity',
-    'places',
-    'objects',
-    'symbols',
-    'flags',
-    'custom',
-  ].forEach((category) => {
-    categories[category] = i18n.t(`emoji.categories.${category}`);
-  });
-
-  const skintones = {};
-  (Array.from(Array(6).keys())).forEach((tone) => {
-    skintones[tone + 1] = i18n.t(`emoji.skintones.${tone + 1}`);
-  });
-
-  const translation = {
-    search: i18n.t('emoji.search'),
-    clear: i18n.t('emoji.clear'),
-    notfound: i18n.t('emoji.notfound'),
-    skintext: i18n.t('emoji.skintext'),
-    categories,
-    categorieslabel: i18n.t('emoji.categorieslabel'),
-    skintones,
-    title: i18n.t('emoji.title'),
-  };
-
-  return translation;
-};

+ 0 - 46
apps/app/_obsolete/src/components/PageEditor/MarkdownLinkUtil.js

@@ -1,46 +0,0 @@
-import Linker from '@growi/editor/src/services/link-util/Linker';
-/**
- * Utility for markdown link
- */
-class MarkdownLinkUtil {
-
-  constructor() {
-    this.getMarkdownLink = this.getMarkdownLink.bind(this);
-    this.isInLink = this.isInLink.bind(this);
-    this.replaceFocusedMarkdownLinkWithEditor = this.replaceFocusedMarkdownLinkWithEditor.bind(this);
-  }
-
-  // return an instance of Linker from cursor position or selected text.
-  getMarkdownLink(editor) {
-    if (!this.isInLink(editor)) {
-      return Linker.fromMarkdownString(editor.getDoc().getSelection());
-    }
-    const curPos = editor.getCursor();
-    return Linker.fromLineWithIndex(editor.getDoc().getLine(curPos.line), curPos.ch);
-  }
-
-  isInLink(editor) {
-    const curPos = editor.getCursor();
-    const { beginningOfLink, endOfLink } = Linker.getBeginningAndEndIndexOfLink(editor.getDoc().getLine(curPos.line), curPos.ch);
-    return beginningOfLink >= 0 && endOfLink >= 0;
-  }
-
-  // replace link(link is an instance of Linker)
-  replaceFocusedMarkdownLinkWithEditor(editor, linkText) {
-    const curPos = editor.getCursor();
-    if (!this.isInLink(editor)) {
-      editor.getDoc().replaceSelection(linkText);
-    }
-    else {
-      const line = editor.getDoc().getLine(curPos.line);
-      const { beginningOfLink, endOfLink } = Linker.getBeginningAndEndIndexOfLink(line, curPos.ch);
-      editor.getDoc().replaceRange(linkText, { line: curPos.line, ch: beginningOfLink }, { line: curPos.line, ch: endOfLink });
-    }
-  }
-
-}
-
-// singleton pattern
-const instance = new MarkdownLinkUtil();
-Object.freeze(instance);
-export default instance;

+ 0 - 95
apps/app/_obsolete/src/components/PageEditor/MarkdownTableInterceptor.js

@@ -1,95 +0,0 @@
-import { BasicInterceptor } from '@growi/core/dist/utils';
-
-import MarkdownTable from '~/client/models/MarkdownTable';
-
-import {
-  getStrFromBot, addRowToMarkdownTable, getStrToEot, isEndOfLine, mergeMarkdownTable, replaceFocusedMarkdownTableWithEditor,
-  isInTable, emptyLineOfTableRE,
-} from '../../../../src/components/PageEditor/markdown-table-util-for-editor';
-
-/**
- * Interceptor for markdown table
- */
-export default class MarkdownTableInterceptor extends BasicInterceptor {
-
-  /**
-   * @inheritdoc
-   */
-  isInterceptWhen(contextName) {
-    return (
-      contextName === 'preHandleEnter'
-    );
-  }
-
-  /**
-   * return boolean value whether processable parallel
-   */
-  isProcessableParallel() {
-    return false;
-  }
-
-  addRow(cm) {
-    // get lines all of table from current position to beginning of table
-    const strFromBot = getStrFromBot(cm);
-    let table = MarkdownTable.fromMarkdownString(strFromBot);
-
-    addRowToMarkdownTable(table);
-
-    const strToEot = getStrToEot(cm);
-    const tableBottom = MarkdownTable.fromMarkdownString(strToEot);
-    if (tableBottom.table.length > 0) {
-      table = mergeMarkdownTable([table, tableBottom]);
-    }
-
-    replaceFocusedMarkdownTableWithEditor(cm, table);
-  }
-
-  reformTable(cm) {
-    const tableStr = getStrFromBot(cm) + getStrToEot(cm);
-    const table = MarkdownTable.fromMarkdownString(tableStr);
-    replaceFocusedMarkdownTableWithEditor(cm, table);
-  }
-
-  removeRow(editor) {
-    editor.replaceLine('\n');
-  }
-
-  /**
-   * @inheritdoc
-   */
-  async process(contextName, ...args) {
-    const context = Object.assign(args[0]); // clone
-    const editor = context.editor; // AbstractEditor instance
-    // "autoFormatMarkdownTable" may be undefined, so it is compared to true and converted to bool.
-    const noIntercept = (context.autoFormatMarkdownTable === false);
-
-    // do nothing if editor is not a CodeMirrorEditor or no intercept
-    if (editor == null || editor.getCodeMirror() == null || noIntercept) {
-      return context;
-    }
-
-    const cm = editor.getCodeMirror();
-
-    const isLastRow = getStrToEot(cm) === editor.getStrToEol();
-
-    if (isInTable(cm)) {
-      // at EOL in the table
-      if (isEndOfLine(cm)) {
-        this.addRow(cm);
-      }
-      // last empty row
-      else if (isLastRow && emptyLineOfTableRE.test(editor.getStrFromBol() + editor.getStrToEol())) {
-        this.removeRow(editor);
-      }
-      else {
-        this.reformTable(cm);
-      }
-
-      // report to manager that handling was done
-      context.handlers.push(this.className);
-      return context;
-    }
-
-  }
-
-}

+ 0 - 44
apps/app/_obsolete/src/components/PageEditor/PasteHelper.js

@@ -1,44 +0,0 @@
-import accepts from 'attr-accept';
-
-import markdownListUtil from './MarkdownListUtil';
-
-class PasteHelper {
-
-  constructor() {
-    this.pasteText = this.pasteText.bind(this);
-  }
-
-  /**
-   * paste text
-   * @param {any} editor An editor instance of CodeMirror
-   * @param {any} event
-   */
-  pasteText(editor, event) {
-    // get data in clipboard
-    const text = event.clipboardData.getData('text/plain');
-
-    if (text.length === 0) {
-      return;
-    }
-
-    markdownListUtil.pasteText(editor, event, text);
-  }
-
-  // Firefox versions prior to 53 return a bogus MIME type for every file drag, so dragovers with
-  /**
-   * transplanted from react-dropzone
-   * @see https://github.com/react-dropzone/react-dropzone/blob/master/src/utils/index.js
-   *
-   * @param {*} file
-   * @param {*} accept
-   */
-  isAcceptableType(file, accept) {
-    return file.type === 'application/x-moz-file' || accepts(file, accept);
-  }
-
-}
-
-// singleton pattern
-const instance = new PasteHelper();
-Object.freeze(instance);
-export default instance;

+ 0 - 44
apps/app/_obsolete/src/components/PageEditor/PreventMarkdownListInterceptor.js

@@ -1,44 +0,0 @@
-import { BasicInterceptor } from '@growi/core/dist/utils';
-
-import mlu from './MarkdownListUtil';
-
-export default class PreventMarkdownListInterceptor extends BasicInterceptor {
-
-  /**
-   * @inheritdoc
-   */
-  isInterceptWhen(contextName) {
-    return (
-      contextName === 'preHandleEnter'
-    );
-  }
-
-  /**
-   * return boolean value whether processable parallel
-   */
-  isProcessableParallel() {
-    return false;
-  }
-
-  /**
-   * @inheritdoc
-   */
-  process(contextName, ...args) {
-    const context = Object.assign(args[0]); // clone
-    const editor = context.editor; // AbstractEditor instance
-
-    // get strings from current position to EOL(end of line) before break the line
-    const strToEol = editor.getStrToEol();
-    if (mlu.indentAndMarkRE.test(strToEol)) {
-      // newline simply
-      editor.insertLinebreak();
-
-      // report to manager that handling was done
-      context.handlers.push(this.className);
-    }
-
-    // resolve
-    return Promise.resolve(context);
-  }
-
-}

+ 0 - 274
apps/app/_obsolete/src/components/PageEditor/TextAreaEditor.jsx

@@ -1,274 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { Input } from 'reactstrap';
-
-import InterceptorManager from '~/services/interceptor-manager';
-import loggerFactory from '~/utils/logger';
-
-
-import AbstractEditor from './AbstractEditor';
-import mlu from './MarkdownListUtil';
-import pasteHelper from './PasteHelper';
-import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
-
-export default class TextAreaEditor extends AbstractEditor {
-
-  constructor(props) {
-    super(props);
-    this.logger = loggerFactory('growi:PageEditor:TextAreaEditor');
-
-    this.state = {
-      value: this.props.value,
-      isGfmMode: this.props.isGfmMode,
-    };
-
-    this.textarea = React.createRef();
-
-    this.init();
-
-    this.handleEnterKey = this.handleEnterKey.bind(this);
-
-    this.keyPressHandler = this.keyPressHandler.bind(this);
-    this.pasteHandler = this.pasteHandler.bind(this);
-    this.dragEnterHandler = this.dragEnterHandler.bind(this);
-  }
-
-  init() {
-    this.interceptorManager = new InterceptorManager();
-    this.interceptorManager.addInterceptors([
-      new PreventMarkdownListInterceptor(),
-    ]);
-  }
-
-  componentDidMount() {
-    // initialize caret line
-    this.setCaretLine(0);
-
-    // set event handlers
-    this.textarea.addEventListener('keypress', this.keyPressHandler);
-    this.textarea.addEventListener('paste', this.pasteHandler);
-    this.textarea.addEventListener('dragenter', this.dragEnterHandler);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  forceToFocus() {
-    setTimeout(() => {
-      this.textarea.focus();
-    }, 150);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setValue(newValue) {
-    this.setState({ value: newValue });
-    this.textarea.value = newValue;
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setGfmMode(bool) {
-    this.setState({
-      isGfmMode: bool,
-    });
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setCaretLine(line) {
-    if (Number.isNaN(line)) {
-      return;
-    }
-
-    // scroll to bottom
-    this.textarea.scrollTop = this.textarea.scrollHeight;
-
-    const lines = this.textarea.value.split('\n').slice(0, line);
-    /* eslint-disable no-param-reassign, no-return-assign */
-    const pos = lines
-      .map((lineStr) => { return lineStr.length + 1 }) // correct length+1 of each lines
-      .reduce((a, x) => { return a += x }, 0) //          sum
-        - 1; //                                           -1
-    /* eslint-enable no-param-reassign, no-return-assign */
-
-    this.textarea.setSelectionRange(pos, pos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setScrollTopByLine(line) {
-    // do nothing
-  }
-
-  /**
-   * @inheritDoc
-   */
-  insertText(text) {
-    const startPos = this.textarea.selectionStart;
-    const endPos = this.textarea.selectionEnd;
-    this.replaceValue(text, startPos, endPos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  getStrFromBol() {
-    const currentPos = this.textarea.selectionStart;
-    return this.textarea.value.substring(this.getBolPos(), currentPos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  getStrToEol() {
-    const currentPos = this.textarea.selectionStart;
-    return this.textarea.value.substring(currentPos, this.getEolPos());
-  }
-
-  /**
-   * @inheritDoc
-   */
-  getStrFromBolToSelectedUpperPos() {
-    const startPos = this.textarea.selectionStart;
-    const endPos = this.textarea.selectionEnd;
-    const upperPos = (startPos < endPos) ? startPos : endPos;
-    return this.textarea.value.substring(this.getBolPos(), upperPos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  replaceBolToCurrentPos(text) {
-    const startPos = this.textarea.selectionStart;
-    const endPos = this.textarea.selectionEnd;
-    const lowerPos = (startPos < endPos) ? endPos : startPos;
-    this.replaceValue(text, this.getBolPos(), lowerPos);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  replaceLine(text) {
-    this.replaceValue(text, this.getBolPos(), this.getEolPos());
-  }
-
-  getBolPos() {
-    const currentPos = this.textarea.selectionStart;
-    return this.textarea.value.lastIndexOf('\n', currentPos - 1) + 1;
-  }
-
-  getEolPos() {
-    const currentPos = this.textarea.selectionStart;
-    const pos = this.textarea.value.indexOf('\n', currentPos);
-    if (pos < 0) { // not found but EOF
-      return this.textarea.value.length;
-    }
-    return pos;
-  }
-
-  replaceValue(text, startPos, endPos) {
-    // create new value
-    const value = this.textarea.value;
-    const newValue = value.substring(0, startPos) + text + value.substring(endPos, value.length);
-    // calculate new position
-    const newPos = startPos + text.length;
-
-    this.textarea.value = newValue;
-    this.textarea.setSelectionRange(newPos, newPos);
-  }
-
-  /**
-   * keypress event handler
-   * @param {string} event
-   */
-  keyPressHandler(event) {
-    const key = event.key.toLowerCase();
-    if (key === 'enter') {
-      if (event.ctrlKey || event.altKey || event.metaKey) {
-        return;
-      }
-
-      this.handleEnterKey(event);
-    }
-  }
-
-  /**
-   * handle ENTER key
-   * @param {string} event
-   */
-  handleEnterKey(event) {
-    if (!this.state.isGfmMode) {
-      return; // do nothing
-    }
-
-    const context = {
-      handlers: [], // list of handlers which process enter key
-      editor: this,
-    };
-
-    const interceptorManager = this.interceptorManager;
-    interceptorManager.process('preHandleEnter', context)
-      .then(() => {
-        event.preventDefault();
-        if (context.handlers.length === 0) {
-          mlu.newlineAndIndentContinueMarkdownList(this);
-        }
-      });
-  }
-
-  /**
-   * paste event handler
-   * @param {any} event
-   */
-  pasteHandler(event) {
-    const types = event.clipboardData.types;
-
-    // files
-    if (types.includes('Files')) {
-      event.preventDefault();
-      this.dispatchPasteFiles(event);
-    }
-    // text
-    else if (types.includes('text/plain')) {
-      pasteHelper.pasteText(this, event);
-    }
-  }
-
-  dragEnterHandler(event) {
-    this.dispatchDragEnter(event);
-  }
-
-  dispatchDragEnter(event) {
-    if (this.props.onDragEnter != null) {
-      this.props.onDragEnter(event);
-    }
-  }
-
-  render() {
-    return (
-      <React.Fragment>
-        <Input
-          type="textarea"
-          className="textarea-editor shadow-none"
-          innerRef={(c) => { this.textarea = c }}
-          defaultValue={this.state.value}
-          onChange={(e) => {
-            if (this.props.onChange != null) {
-              this.props.onChange(e.target.value);
-            }
-          }}
-        />
-      </React.Fragment>
-    );
-  }
-
-}
-
-TextAreaEditor.propTypes = Object.assign({
-}, AbstractEditor.propTypes);

+ 0 - 521
apps/app/_obsolete/src/components/PageEditorByHackmd.tsx

@@ -1,521 +0,0 @@
-import React, {
-  useCallback, useRef, useState, useEffect, useMemo,
-} from 'react';
-
-import EventEmitter from 'events';
-
-import { pathUtils } from '@growi/core/dist/utils';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import { useTranslation } from 'react-i18next';
-import urljoin from 'url-join';
-
-import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
-import { apiPost } from '~/client/util/apiv1-client';
-import { toastError, toastSuccess } from '~/client/util/toastr';
-import { IResHackmdIntegrated, IResHackmdDiscard } from '~/interfaces/hackmd';
-import { OptionsToSave } from '~/interfaces/page-operation';
-import {
-  useCurrentPathname, useHackmdUri,
-} from '~/stores/context';
-import {
-  useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning, useWaitingSaveProcessing,
-} from '~/stores/editor';
-import {
-  usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
-} from '~/stores/hackmd';
-import {
-  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound,
-} from '~/stores/page';
-import { mutatePageTree } from '~/stores/page-listing';
-import { useRemoteRevisionId } from '~/stores/remote-latest-page';
-import {
-  EditorMode,
-  useEditorMode, useSelectedGrant,
-} from '~/stores/ui';
-import loggerFactory from '~/utils/logger';
-
-import HackmdEditor from './PageEditorByHackmd/HackmdEditor';
-
-const logger = loggerFactory('growi:PageEditorByHackmd');
-
-
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
-
-
-type HackEditorRef = {
-  getValue: () => Promise<string>
-};
-
-export const PageEditorByHackmd = (): JSX.Element => {
-
-  const { t } = useTranslation();
-  const router = useRouter();
-
-  const { data: isNotFound } = useIsNotFound();
-  const { mutate: mutateWaitingSaveProcessing } = useWaitingSaveProcessing();
-  const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: currentPathname } = useCurrentPathname();
-  const { data: isSlackEnabled } = useIsSlackEnabled();
-  const { data: pageId } = useCurrentPageId();
-  const { data: pageTags } = usePageTagsForEditors(pageId);
-  const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
-  const { data: grantData } = useSelectedGrant();
-  const { data: hackmdUri } = useHackmdUri();
-  const saveOrUpdate = useSaveOrUpdate();
-
-  const { returnPathForURL } = pathUtils;
-
-  // pageData
-  const { data: pageData } = useSWRxCurrentPage();
-  const { trigger: mutatePageData } = useSWRMUTxCurrentPage();
-  const revision = pageData?.revision;
-
-  const [isInitialized, setIsInitialized] = useState(false);
-  const [isInitializing, setIsInitializing] = useState(false);
-  // for error
-  const [hasError, setHasError] = useState(false);
-  const [errorMessage, setErrorMessage] = useState('');
-  const [errorReason, setErrorReason] = useState('');
-
-  // state from pageContainer
-  const { data: pageIdOnHackmd, mutate: mutatePageIdOnHackmd } = usePageIdOnHackmd();
-  const { data: hasDraftOnHackmd, mutate: mutateHasDraftOnHackmd } = useHasDraftOnHackmd();
-  const { data: revisionIdHackmdSynced, mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
-  const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
-  const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
-  const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
-
-  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
-
-  const hackmdEditorRef = useRef<HackEditorRef>(null);
-
-  const optionsToSave = useMemo((): OptionsToSave | undefined => {
-    if (grantData == null) {
-      return;
-    }
-    const userRelatedGrantedGroups = grantData.userRelatedGrantedGroups?.map((group) => {
-      return { item: group.id, type: group.type };
-    });
-    const optionsToSave = {
-      isSlackEnabled: isSlackEnabled ?? false,
-      slackChannels: '', // set in save method by opts in SavePageControlls.tsx
-      grant: grantData.grant,
-      pageTags: pageTags ?? [],
-      userRelatedGrantUserGroupIds: userRelatedGrantedGroups,
-    };
-    return optionsToSave;
-  }, [grantData, isSlackEnabled, pageTags]);
-
-  const saveAndReturnToViewHandler = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
-    if (editorMode !== EditorMode.HackMD) { return }
-
-    try {
-      if (currentPathname == null || revision == null || hackmdEditorRef.current == null || revisionIdHackmdSynced == null || optionsToSave == null) {
-        throw new Error('Some materials to save are invalid');
-      }
-
-      mutateWaitingSaveProcessing(true);
-
-      const options = Object.assign(optionsToSave, opts, { isSyncRevisionToHackmd: true });
-
-      const markdown = await hackmdEditorRef.current.getValue();
-
-      const { page } = await saveOrUpdate(markdown, { pageId, path: currentPagePath || currentPathname, revisionId: revisionIdHackmdSynced }, options);
-
-      if (page == null) {
-        return;
-      }
-      if (isNotFound) {
-        await router.push(`/${page._id}`);
-      }
-      else {
-        updateStateAfterSave?.();
-        mutateIsHackmdDraftUpdatingInRealtime(false);
-
-        // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-        mutatePageTree();
-      }
-      setIsInitialized(false);
-      mutateEditorMode(EditorMode.View);
-    }
-    catch (error) {
-      logger.error('failed to save', error);
-      toastError(error.message);
-    }
-    finally {
-      mutateWaitingSaveProcessing(false);
-    }
-
-  // eslint-disable-next-line max-len
-  }, [
-    pageId, currentPagePath, isNotFound, router,
-    editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave,
-    saveOrUpdate, mutateEditorMode, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime, mutateWaitingSaveProcessing,
-  ]);
-
-  // set handler to save and reload Page
-  useEffect(() => {
-    globalEmitter.on('saveAndReturnToView', saveAndReturnToViewHandler);
-
-    return function cleanup() {
-      globalEmitter.removeListener('saveAndReturnToView', saveAndReturnToViewHandler);
-    };
-  }, [saveAndReturnToViewHandler]);
-
-  const resetInitializedStatusHandler = useCallback(() => {
-    setIsInitialized(false);
-  }, []);
-
-
-  // set handler to save and reload Page
-  useEffect(() => {
-    globalEmitter.on('resetInitializedHackMdStatus', resetInitializedStatusHandler);
-
-    return function cleanup() {
-      globalEmitter.removeListener('resetInitializedHackMdStatus', resetInitializedStatusHandler);
-    };
-  }, [resetInitializedStatusHandler]);
-
-  useEffect(() => {
-    // for page translation: https://github.com/weseek/growi/pull/7100
-    setIsInitialized(false);
-  }, [pageId]);
-
-
-  const isResume = useCallback(() => {
-    const isPageExistsOnHackmd = (pageIdOnHackmd != null);
-    return (isPageExistsOnHackmd && hasDraftOnHackmd) || isHackmdDraftUpdatingInRealtime;
-  }, [hasDraftOnHackmd, isHackmdDraftUpdatingInRealtime, pageIdOnHackmd]);
-
-  const startToEdit = useCallback(async() => {
-
-    if (hackmdUri == null) {
-      // do nothing
-      return;
-    }
-
-    setIsInitialized(false);
-    setIsInitializing(true);
-
-    try {
-      const res = await apiPost<IResHackmdIntegrated>('/hackmd.integrate', { pageId });
-
-      if (!res.ok) {
-        throw new Error(res.error);
-      }
-
-      mutatePageIdOnHackmd(res.pageIdOnHackmd);
-      mutateRevisionIdHackmdSynced(res.revisionIdHackmdSynced);
-    }
-    catch (err) {
-      toastError(err.message);
-
-      setHasError(true);
-      setErrorMessage('GROWI server failed to connect to HackMD.');
-      setErrorReason(err.toString());
-    }
-
-    setIsInitialized(true);
-    setIsInitializing(false);
-  }, [pageId, hackmdUri, mutatePageIdOnHackmd, mutateRevisionIdHackmdSynced]);
-
-  /**
-   * Start to edit w/o any api request
-   */
-  const resumeToEdit = useCallback(() => {
-    setIsInitialized(true);
-  }, []);
-
-  const discardChanges = useCallback(async() => {
-
-    if (pageId == null) { return }
-
-    try {
-      const res = await apiPost<IResHackmdDiscard>('/hackmd.discard', { pageId });
-
-      if (!res.ok) {
-        throw new Error(res.error);
-      }
-
-      mutateIsHackmdDraftUpdatingInRealtime(false);
-      mutateHasDraftOnHackmd(false);
-      mutatePageIdOnHackmd(res.pageIdOnHackmd);
-      mutateRemoteRevisionId(res.revisionIdHackmdSynced);
-      mutateRevisionIdHackmdSynced(res.revisionIdHackmdSynced);
-
-
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(err.message);
-    }
-  }, [mutateIsHackmdDraftUpdatingInRealtime, mutateHasDraftOnHackmd, mutatePageIdOnHackmd, mutateRevisionIdHackmdSynced, mutateRemoteRevisionId, pageId]);
-
-  /**
-   * save and update state of containers
-   * @param {string} markdown
-   */
-  const onSaveWithShortcut = useCallback(async(markdown) => {
-    try {
-      mutateWaitingSaveProcessing(true);
-
-      const currentPagePathOrPathname = currentPagePath || currentPathname;
-      if (
-        pageId == null || revisionIdHackmdSynced == null || currentPagePathOrPathname == null || optionsToSave == null
-      ) { throw new Error('Some materials to save are invalid') }
-
-      const options = Object.assign(optionsToSave, { isSyncRevisionToHackmd: true });
-
-      const res = await saveOrUpdate(markdown, { pageId, path: currentPagePathOrPathname, revisionId: revisionIdHackmdSynced }, options);
-
-      // update pageData
-      mutatePageData(res);
-
-      // set updated data
-      updateStateAfterSave?.();
-      mutateTagsInfo();
-
-      // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-      mutatePageTree();
-
-      mutateIsEnabledUnsavedWarning(false);
-
-      logger.debug('success to save');
-
-      toastSuccess(t('successfully_saved_the_page'));
-    }
-    catch (error) {
-      logger.error('failed to save', error);
-      toastError(error.message);
-    }
-    finally {
-      mutateWaitingSaveProcessing(false);
-    }
-
-  // eslint-disable-next-line max-len
-  }, [
-    currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave,
-    saveOrUpdate,
-    mutateWaitingSaveProcessing, mutatePageData, updateStateAfterSave, mutateTagsInfo, mutateIsEnabledUnsavedWarning, t,
-  ]);
-
-  /**
-   * onChange event of HackmdEditor handler
-   */
-  const hackmdEditorChangeHandler = useCallback(async(body) => {
-
-    if (hackmdUri == null || pageId == null) {
-      // do nothing
-      return;
-    }
-
-    if (revision?.body === body) {
-      return;
-    }
-
-    mutateIsEnabledUnsavedWarning(true);
-
-    try {
-      await apiPost('/hackmd.saveOnHackmd', { pageId });
-    }
-    catch (err) {
-      logger.error(err);
-    }
-  }, [hackmdUri, pageId, revision?.body, mutateIsEnabledUnsavedWarning]);
-
-  const penpalErrorOccuredHandler = useCallback((error) => {
-    toastError(error.message);
-
-    setHasError(true);
-    setErrorMessage(t('hackmd.fail_to_connect'));
-    setErrorReason(error.toString());
-  }, [t]);
-
-  const renderPreInitContent = useCallback(() => {
-    const isPageNotFound = pageId == null;
-
-    let content;
-
-    /*
-     * HackMD is not setup
-     */
-    if (hackmdUri == null) {
-      content = (
-        <div>
-          <p className="text-center hackmd-status-label"><span className="material-symbols-outlined">description</span> { t('hackmd.not_set_up')}</p>
-          {/* eslint-disable-next-line react/no-danger */}
-          <p dangerouslySetInnerHTML={{ __html: t('hackmd.need_to_associate_with_growi_to_use_hackmd_refer_to_this') }} />
-        </div>
-      );
-    }
-
-    /*
-    * used HackMD from NotFound Page
-    */
-    else if (isPageNotFound) {
-      content = (
-        <div className="text-center">
-          <p className="hackmd-status-label">
-            <span className="material-symbols-outlined">description</span>
-            { t('hackmd.used_for_not_found') }
-          </p>
-          {/* eslint-disable-next-line react/no-danger */}
-          <p dangerouslySetInnerHTML={{ __html: t('hackmd.need_to_make_page') }} />
-        </div>
-      );
-    }
-    /*
-     * Resume to edit or discard changes
-     */
-    else if (isResume()) {
-      const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
-
-      content = (
-        <div>
-          <p className="text-center hackmd-status-label"><span className="material-symbols-outlined">description</span> HackMD is READY!</p>
-          <p className="text-center"><strong>{t('hackmd.unsaved_draft')}</strong></p>
-
-          { isHackmdDocumentOutdated && (
-            <div className="card border-warning">
-              <div className="card-header bg-warning text-dark"><span className="material-symbols-outlined">info</span> {t('hackmd.draft_outdated')}</div>
-              <div className="card-body text-center">
-                {t('hackmd.based_on_revision')}&nbsp;
-                { pageData != null && (
-                  <Link href={urljoin(returnPathForURL(pageData.path, pageData._id), `?revisionId=${revisionIdHackmdSynced}`)} prefetch={false}>
-                    <span className="badge bg-primary">{revisionIdHackmdSynced?.substr(-8)}</span>
-                  </Link>
-                )}
-                <div className="text-center mt-3">
-                  <button
-                    className="btn btn-link btn-view-outdated-draft p-0"
-                    type="button"
-                    disabled={isInitializing}
-                    onClick={resumeToEdit}
-                  >
-                    {t('hackmd.view_outdated_draft')}
-                  </button>
-                </div>
-              </div>
-            </div>
-          ) }
-
-          { !isHackmdDocumentOutdated && (
-            <div className="text-center hackmd-resume-button-container mb-3">
-              <button
-                className="btn btn-success btn-lg waves-effect waves-light"
-                type="button"
-                disabled={isInitializing}
-                onClick={resumeToEdit}
-              >
-                <span className="btn-label"></span><span className="material-symbols-outlined">skip_next</span>
-                <span className="btn-text">{t('hackmd.resume_to_edit')}</span>
-              </button>
-            </div>
-          ) }
-
-          <div className="text-center hackmd-discard-button-container mb-3">
-            <button
-              className="btn btn-outline-secondary btn-lg waves-effect waves-light"
-              type="button"
-              onClick={discardChanges}
-            >
-              <span className="btn-label"></span><span className="material-symbols-outlined">play_arrow</span>
-              <span className="btn-text">{t('hackmd.discard_changes')}</span>
-            </button>
-          </div>
-
-        </div>
-      );
-    }
-    /*
-     * Start to edit
-     */
-    else {
-      const isRevisionOutdated = revision?._id !== remoteRevisionId;
-
-      content = (
-        <div>
-          <p className="text-muted text-center hackmd-status-label"><span className="material-symbols-outlined">description</span> HackMD is READY!</p>
-          <div className="text-center hackmd-start-button-container mb-3">
-            <button
-              className="btn btn-info btn-lg waves-effect waves-light"
-              type="button"
-              disabled={isRevisionOutdated || isInitializing}
-              onClick={startToEdit}
-            >
-              <span className="btn-label"></span><span className="material-symbols-outlined">send</span>
-              {t('hackmd.start_to_edit')}
-            </button>
-          </div>
-          <p className="text-center">{t('hackmd.clone_page_content')}</p>
-        </div>
-      );
-    }
-
-    return (
-      <div className="hackmd-preinit d-flex justify-content-center align-items-center">
-        {content}
-      </div>
-    );
-  // eslint-disable-next-line max-len
-  }, [pageId, hackmdUri, isResume, t, revisionIdHackmdSynced, remoteRevisionId, pageData, returnPathForURL, isInitializing, resumeToEdit, discardChanges, revision?._id, startToEdit]);
-
-  if (editorMode == null || revision == null) {
-    return <></>;
-  }
-
-  let content;
-
-  // TODO: typescriptize
-  // using any because ref cann't used between FC and class conponent with type safe
-  const AnyEditor = HackmdEditor as any;
-
-  if (isInitialized && hackmdUri != null) {
-    content = (
-      <AnyEditor
-        ref={hackmdEditorRef}
-        hackmdUri={hackmdUri}
-        pageIdOnHackmd={pageIdOnHackmd}
-        initializationMarkdown={isResume() ? null : revision.body}
-        onChange={hackmdEditorChangeHandler}
-        onSaveWithShortcut={(document) => {
-          onSaveWithShortcut(document);
-        }}
-        onPenpalErrorOccured={penpalErrorOccuredHandler}
-      >
-      </AnyEditor>
-    );
-  }
-  else {
-    content = renderPreInitContent();
-  }
-
-
-  return (
-    <div className="position-relative">
-
-      {content}
-
-      { hasError && (
-        <div className="hackmd-error position-absolute d-flex flex-column justify-content-center align-items-center">
-          <div className="bg-box p-5 text-center">
-            <h2 className="text-warning"><span className="material-symbols-outlined">error</span> {t('hackmd.integration_failed')}</h2>
-            <h4>{errorMessage}</h4>
-            <p className="card custom-card text-danger">
-              {errorReason}
-            </p>
-            {/* eslint-disable-next-line react/no-danger */}
-            <p dangerouslySetInnerHTML={{ __html: t('hackmd.check_configuration') }} />
-          </div>
-        </div>
-      ) }
-
-    </div>
-  );
-
-};

+ 0 - 115
apps/app/_obsolete/src/components/PageEditorByHackmd/HackmdEditor.jsx

@@ -1,115 +0,0 @@
-import React from 'react';
-
-import connectToChild from 'penpal/lib/connectToChild';
-import PropTypes from 'prop-types';
-
-import loggerFactory from '~/utils/logger';
-
-
-const DEBUG_PENPAL = false;
-
-const logger = loggerFactory('growi:HackmdEditor');
-
-export default class HackmdEditor extends React.PureComponent {
-
-  constructor(props) {
-    super(props);
-
-    this.hackmd = null;
-
-    this.initHackmdWithPenpal = this.initHackmdWithPenpal.bind(this);
-
-    this.notifyBodyChangesHandler = this.notifyBodyChangesHandler.bind(this);
-    this.saveWithShortcutHandler = this.saveWithShortcutHandler.bind(this);
-  }
-
-  componentDidMount() {
-    // append iframe with penpal
-    this.initHackmdWithPenpal();
-  }
-
-  async initHackmdWithPenpal() {
-    const shouldInit = document.getElementById('iframe-hackmd') != null;
-    if (shouldInit) {
-      return;
-    }
-
-    // eslint-disable-next-line @typescript-eslint/no-this-alias
-    const _this = this; // for in methods scope
-    const iframe = document.createElement('iframe');
-    iframe.src = `${this.props.hackmdUri}/${this.props.pageIdOnHackmd}?both`;
-    iframe.id = 'iframe-hackmd';
-    this.iframeContainer.appendChild(iframe);
-
-    const connection = connectToChild({
-      iframe,
-      methods: { // expose methods to HackMD
-        notifyBodyChanges(document) {
-          _this.notifyBodyChangesHandler(document);
-        },
-        saveWithShortcut(document) {
-          _this.saveWithShortcutHandler(document);
-        },
-      },
-      timeout: 15000,
-      debug: DEBUG_PENPAL,
-    });
-
-    try {
-      const child = await connection.promise;
-      this.hackmd = child;
-      if (this.props.initializationMarkdown != null) {
-        child.setValueOnInit(this.props.initializationMarkdown);
-      }
-    }
-    catch (err) {
-      logger.error(err);
-
-      if (this.props.onPenpalErrorOccured != null) {
-        this.props.onPenpalErrorOccured(err);
-      }
-    }
-  }
-
-  /**
-   * return markdown document of HackMD
-   * @return {Promise<string>}
-   */
-  getValue() {
-    return this.hackmd.getValue();
-  }
-
-  setValue(newValue) {
-    this.hackmd.setValue(newValue);
-  }
-
-  notifyBodyChangesHandler(body) {
-    // dispatch onChange() when there is difference from 'initializationMarkdown' props
-    if (this.props.onChange != null && body !== this.props.initializationMarkdown) {
-      this.props.onChange(body);
-    }
-  }
-
-  saveWithShortcutHandler(document) {
-    if (this.props.onSaveWithShortcut != null) {
-      this.props.onSaveWithShortcut(document);
-    }
-  }
-
-  render() {
-    return (
-      // will be rendered in componentDidMount
-      <div id="iframe-hackmd-container" ref={(c) => { this.iframeContainer = c }}></div>
-    );
-  }
-
-}
-
-HackmdEditor.propTypes = {
-  hackmdUri: PropTypes.string.isRequired,
-  pageIdOnHackmd: PropTypes.string.isRequired,
-  initializationMarkdown: PropTypes.string,
-  onChange: PropTypes.func,
-  onSaveWithShortcut: PropTypes.func,
-  onPenpalErrorOccured: PropTypes.func,
-};

+ 0 - 101
apps/app/_obsolete/src/components/UncontrolledCodeMirror.tsx

@@ -1,101 +0,0 @@
-import React, {
-  useCallback, useRef, MutableRefObject,
-} from 'react';
-
-import codemirror, { commands, Editor } from 'codemirror';
-import { type ICodeMirror, UnControlled as CodeMirror } from 'react-codemirror2';
-
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var CodeMirror: ICodeMirror;
-}
-
-// set save handler
-// CommandActions in @types/codemirror does not include 'save' but actualy exists
-// https://codemirror.net/5/doc/manual.html#commands
-(commands as any).save = (instance) => {
-  if (instance.codeMirrorEditor != null) {
-    instance.codeMirrorEditor.dispatchSave();
-  }
-};
-
-window.CodeMirror = codemirror;
-require('codemirror/addon/display/placeholder');
-require('~/client/util/codemirror/gfm-growi.mode');
-
-export interface UncontrolledCodeMirrorProps extends ICodeMirror {
-  value: string;
-  isGfmMode?: boolean;
-  lineNumbers?: boolean;
-  onSave?: () => Promise<void>;
-  onCtrlEnter?: (event: Event) => void;
-  onPasteFiles?: (editor: any, event: Event) => void;
-  onScrollCursorIntoView?: (editor: any, event: Event) => void;
-}
-
-export const UncontrolledCodeMirror = React.forwardRef<CodeMirror|null, UncontrolledCodeMirrorProps>((props, forwardedRef): JSX.Element => {
-
-  const {
-    value, lineNumbers, options,
-    onPasteFiles, onScrollCursorIntoView,
-    ...rest
-  } = props;
-
-  const editorRef = useRef<Editor>();
-
-  const wrapperRef = useRef<CodeMirror|null>();
-
-  const editorDidMountHandler = useCallback((editor: Editor): void => {
-    editorRef.current = editor;
-    if (onPasteFiles != null) {
-      editor.on('paste', onPasteFiles);
-    }
-    if (onScrollCursorIntoView != null) {
-      editor.on('scrollCursorIntoView', onScrollCursorIntoView);
-    }
-  }, [onPasteFiles, onScrollCursorIntoView]);
-
-  const editorWillUnmountHandler = useCallback((): void => {
-    // workaround to fix editor duplicating by https://github.com/scniro/react-codemirror2/issues/284#issuecomment-1155928554
-    if (editorRef.current != null) {
-      (editorRef.current as any).display.wrapper.remove();
-    }
-    if (wrapperRef.current != null) {
-      (wrapperRef.current as any).hydrated = false;
-    }
-  }, []);
-
-  // default true
-  const isGfmMode = rest.isGfmMode ?? true;
-
-  return (
-    <CodeMirror
-      ref={(elem) => {
-        // register to wrapperRef
-        wrapperRef.current = elem;
-        // register to forwardedRef
-        if (forwardedRef != null) {
-          if (typeof forwardedRef === 'function') {
-            forwardedRef(elem);
-          }
-          else {
-            (forwardedRef as MutableRefObject<CodeMirror|null>).current = elem;
-          }
-        }
-      }}
-      value={value}
-      options={{
-        lineNumbers: lineNumbers ?? true,
-        mode: isGfmMode ? 'gfm-growi' : undefined,
-        tabSize: 4,
-        ...options,
-      }}
-      editorDidMount={editorDidMountHandler}
-      editorWillUnmount={editorWillUnmountHandler}
-      {...rest}
-    />
-  );
-
-});
-
-UncontrolledCodeMirror.displayName = 'UncontrolledCodeMirror';

+ 0 - 16
apps/app/_obsolete/src/interfaces/hackmd.ts

@@ -1,16 +0,0 @@
-
-export interface IResHackmdIntegrated {
-  ok: boolean,
-  error?: any,
-  pageIdOnHackmd?: string,
-  revisionIdHackmdSynced?: string,
-  hasDraftOnHackmd?: string
-}
-
-export interface IResHackmdDiscard {
-  ok: boolean,
-  error?: any,
-  pageIdOnHackmd?: string,
-  revisionIdHackmdSynced?: string,
-  hasDraftOnHackmd?: string
-}

+ 0 - 346
apps/app/_obsolete/src/server/routes/hackmd.js

@@ -1,346 +0,0 @@
-import * as hackmdFiles from '@growi/hackmd';
-
-import loggerFactory from '~/utils/logger';
-
-/* eslint-disable no-use-before-define */
-
-const logger = loggerFactory('growi:routes:hackmd');
-
-const axios = require('axios');
-const ejs = require('ejs');
-
-const ApiResponse = require('../util/apiResponse');
-
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *      Hackmd:
- *        description: Hackmd
- *        type: object
- *        properties:
- *          pageIdOnHackmd:
- *            type: string
- *            description: page ID on HackMD
- *            example: qLnodHLxT6C3hVEVczvbDQ
- *          revisionIdHackmdSynced:
- *            $ref: '#/components/schemas/Revision/properties/_id'
- *          hasDraftOnHackmd:
- *            type: boolean
- *            description: has draft on HackMD
- *            example: false
- */
-module.exports = function(crowi, app) {
-  const Page = crowi.models.Page;
-  const pageEvent = crowi.event('page');
-
-  /**
-   * GET /_hackmd/load-agent
-   *
-   * loadAgent action
-   * This should be access from HackMD and send agent script
-   *
-   * @param {object} req
-   * @param {object} res
-   */
-  const loadAgent = function(req, res) {
-
-    const origin = crowi.appService.getSiteUrl();
-
-    // generate definitions to replace
-    const definitions = {
-      origin,
-    };
-
-    // inject origin to script
-    const script = ejs.render(hackmdFiles.agentJS, definitions);
-
-    res.set('Content-Type', 'application/javascript');
-    res.send(script);
-  };
-
-  /**
-   * GET /_hackmd/load-styles
-   *
-   * loadStyles action
-   * This should be access from HackMD and send script to insert styles
-   *
-   * @param {object} req
-   * @param {object} res
-   */
-  const loadStyles = function(req, res) {
-
-    // generate definitions to replace
-    const definitions = {
-      styles: hackmdFiles.stylesCSS,
-    };
-    // inject styles to script
-    const script = ejs.render(hackmdFiles.stylesJS, definitions);
-
-    res.set('Content-Type', 'application/javascript');
-    res.send(script);
-  };
-
-  const validateForApi = async function(req, res, next) {
-    // validate process.env.HACKMD_URI
-    const hackmdUri = process.env.HACKMD_URI;
-    if (hackmdUri == null) {
-      return res.json(ApiResponse.error('HackMD for GROWI has not been setup'));
-    }
-    // validate pageId
-    const pageId = req.body.pageId;
-    if (pageId == null) {
-      return res.json(ApiResponse.error('pageId required'));
-    }
-    // validate page
-    const page = await Page.findOne({ _id: pageId });
-    if (page == null) {
-      return res.json(ApiResponse.error(`Page('${pageId}') does not exist`));
-    }
-
-    req.page = page;
-    next();
-  };
-
-  /**
-   * @swagger
-   *
-   *    /hackmd.integrate:
-   *      post:
-   *        tags: [Hackmd]
-   *        operationId: integrateHackmd
-   *        summary: /hackmd.integrate
-   *        description: Integrate hackmd
-   *        requestBody:
-   *          content:
-   *            application/json:
-   *              schema:
-   *                properties:
-   *                  pageId:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
-   *                  page:
-   *                    $ref: '#/components/schemas/Hackmd'
-   *        responses:
-   *          200:
-   *            description: Succeeded to integrate HackMD.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    pageIdOnHackmd:
-   *                      $ref: '#/components/schemas/Hackmd/properties/pageIdOnHackmd'
-   *                    revisionIdHackmdSynced:
-   *                      $ref: '#/components/schemas/Hackmd/properties/revisionIdHackmdSynced'
-   *                    hasDraftOnHackmd:
-   *                      $ref: '#/components/schemas/Hackmd/properties/hasDraftOnHackmd'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * POST /_api/hackmd.integrate
-   *
-   * Create page on HackMD and start to integrate
-   * @param {object} req
-   * @param {object} res
-   */
-  const integrate = async function(req, res) {
-    const hackmdUri = process.env.HACKMD_URI_FOR_SERVER || process.env.HACKMD_URI;
-    let page = req.page;
-
-    const hackmdPageUri = (page.pageIdOnHackmd != null)
-      ? `${hackmdUri}/${page.pageIdOnHackmd}`
-      : `${hackmdUri}/new`;
-
-    let hackmdResponse;
-    try {
-      // check if page is found or created in HackMD
-      hackmdResponse = await axios.get(hackmdPageUri, {
-        maxRedirects: 0,
-        // validate HTTP status is 200 or 302 or 404
-        validateStatus: (status) => {
-          return status === 200 || status === 302 || status === 404;
-        },
-      });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.json(ApiResponse.error(err));
-    }
-
-    const { status, headers } = hackmdResponse;
-
-    // validate HackMD/CodiMD/HedgeDoc specific header
-    if (headers['codimd-version'] == null && headers['hackmd-version'] == null && headers['hedgedoc-version'] == null) {
-      const message = 'Connecting to a non-HackMD server.';
-      logger.error(message);
-      return res.json(ApiResponse.error(message));
-    }
-
-    try {
-      // when page is not found
-      if (status === 404) {
-        // reset registered data
-        page = await Page.registerHackmdPage(page, undefined);
-        // re-invoke
-        return integrate(req, res);
-      }
-
-      // when redirect
-      if (status === 302) {
-        // extract page id on HackMD
-        const pathnameOnHackmd = new URL(headers.location, hackmdUri).pathname; // e.g. '/NC7bSRraT1CQO1TO7wjCPw'
-        const pageIdOnHackmd = pathnameOnHackmd.substr(1); //                      strip the head '/'
-
-        page = await Page.registerHackmdPage(page, pageIdOnHackmd);
-      }
-      // when page is found
-      else {
-        page = await Page.syncRevisionToHackmd(page);
-      }
-
-      const data = {
-        pageIdOnHackmd: page.pageIdOnHackmd,
-        revisionIdHackmdSynced: page.revisionHackmdSynced,
-        hasDraftOnHackmd: page.hasDraftOnHackmd,
-      };
-      return res.json(ApiResponse.success(data));
-    }
-    catch (err) {
-      logger.error(err);
-      return res.json(ApiResponse.error('Integration with HackMD process failed'));
-    }
-  };
-
-  /**
-   * @swagger
-   *
-   *    /hackmd.discard:
-   *      post:
-   *        tags: [Hackmd]
-   *        operationId: discardHackmd
-   *        summary: /hackmd.discard
-   *        description: Discard hackmd
-   *        requestBody:
-   *          content:
-   *            application/json:
-   *              schema:
-   *                properties:
-   *                  pageId:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
-   *                  page:
-   *                    $ref: '#/components/schemas/Hackmd'
-   *        responses:
-   *          200:
-   *            description: Succeeded to integrate HackMD.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    pageIdOnHackmd:
-   *                      $ref: '#/components/schemas/Hackmd/properties/pageIdOnHackmd'
-   *                    revisionIdHackmdSynced:
-   *                      $ref: '#/components/schemas/Hackmd/properties/revisionIdHackmdSynced'
-   *                    hasDraftOnHackmd:
-   *                      $ref: '#/components/schemas/Hackmd/properties/hasDraftOnHackmd'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * POST /_api/hackmd.discard
-   *
-   * Create page on HackMD and start to integrate
-   * @param {object} req
-   * @param {object} res
-   */
-  const discard = async function(req, res) {
-    let page = req.page;
-
-    try {
-      page = await Page.syncRevisionToHackmd(page);
-
-      const data = {
-        pageIdOnHackmd: page.pageIdOnHackmd,
-        revisionIdHackmdSynced: page.revisionHackmdSynced,
-        hasDraftOnHackmd: page.hasDraftOnHackmd,
-      };
-      return res.json(ApiResponse.success(data));
-    }
-    catch (err) {
-      logger.error(err);
-      return res.json(ApiResponse.error('discard process failed'));
-    }
-  };
-
-  /**
-   * @swagger
-   *
-   *    /hackmd.saveOnHackmd:
-   *      post:
-   *        tags: [Hackmd]
-   *        operationId: saveOnHackmd
-   *        summary: /hackmd.saveOnHackmd
-   *        description: Receive when save operation triggered on HackMD
-   *        requestBody:
-   *          content:
-   *            application/json:
-   *              schema:
-   *                properties:
-   *                  pageId:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
-   *                  page:
-   *                    $ref: '#/components/schemas/Hackmd'
-   *        responses:
-   *          200:
-   *            description: Succeeded to receive when save operation triggered on HackMD.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * POST /_api/hackmd.saveOnHackmd
-   *
-   * receive when save operation triggered on HackMD
-   * !! This will be invoked many time from many people !!
-   *
-   * @param {object} req
-   * @param {object} res
-   */
-  const saveOnHackmd = async function(req, res) {
-    const { page, user } = req;
-
-    try {
-      await Page.updateHasDraftOnHackmd(page, true);
-      pageEvent.emit('saveOnHackmd', page, user);
-      return res.json(ApiResponse.success());
-    }
-    catch (err) {
-      logger.error(err);
-      return res.json(ApiResponse.error('saveOnHackmd process failed'));
-    }
-  };
-
-  return {
-    loadAgent,
-    loadStyles,
-    validateForApi,
-    integrate,
-    discard,
-    saveOnHackmd,
-  };
-};

+ 0 - 22
apps/app/_obsolete/src/stores/hackmd.ts

@@ -1,22 +0,0 @@
-import { SWRResponse } from 'swr';
-
-import { useStaticSWR } from './use-static-swr';
-
-type Nullable<T> = T | null;
-
-export const usePageIdOnHackmd = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('pageIdOnHackmd', initialData);
-};
-
-
-export const useHasDraftOnHackmd = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('hasDraftOnHackmd', initialData);
-};
-
-export const useRevisionIdHackmdSynced = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('revisionIdHackmdSynced', initialData);
-};
-
-export const useIsHackmdDraftUpdatingInRealtime = (initialData?: Nullable<boolean>): SWRResponse<Nullable<boolean>, Error> => {
-  return useStaticSWR<Nullable<boolean>, Error>('isHackmdDraftUpdatingInRealtime', initialData);
-};

+ 0 - 170
apps/app/_obsolete/src/styles/_override.scss

@@ -1,170 +0,0 @@
-// TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-
-// * {
-//   outline: none !important;
-// }
-
-// .container,
-// .container-sm,
-// .container-md,
-// .container-lg,
-// .container-xl,
-// .container-fluid {
-//   @include media-breakpoint-down(xs) {
-//     padding-right: 10px;
-//     padding-left: 10px;
-//   }
-//   @include media-breakpoint-up(md) {
-//     padding-right: 30px;
-//     padding-left: 30px;
-//   }
-// }
-
-// h1 {
-//   font-size: 36px;
-//   line-height: 48px;
-// }
-
-// h2 {
-//   font-size: 24px;
-//   line-height: 36px;
-// }
-
-// h3 {
-//   font-size: 21px;
-//   line-height: 30px;
-// }
-
-// h4 {
-//   font-size: 18px;
-//   line-height: 22px;
-// }
-
-// h5 {
-//   font-size: 16px;
-//   line-height: 18px;
-// }
-
-// h6 {
-//   font-size: 12px;
-//   line-height: 18px;
-// }
-
-// // Navs
-// .nav-tabs {
-//   .nav-item {
-//     margin-right: 0.15rem;
-//     a.active {
-//       cursor: default;
-//     }
-//   }
-// }
-
-// // Custom Control
-// .form-check {
-//   .form-check-input,
-//   .form-check-input + .form-check-label {
-//     cursor: pointer;
-//   }
-// }
-
-// // card (substitute panel of bootstrap3)
-// .card {
-//   margin-bottom: 20px;
-// }
-
-// .card-header {
-//   font-weight: 700;
-//   text-transform: none;
-// }
-
-// .card-header:first-child {
-// }
-
-// .card.custom-card {
-//   min-height: 20px;
-//   padding: $card-spacer-y $card-spacer-x;
-// }
-
-// // Dropdowns
-// .dropdown-toggle {
-//   &.btn.disabled {
-//     pointer-events: auto;
-//     cursor: not-allowed;
-//   }
-
-//   // hide caret
-//   &.dropdown-toggle-no-caret::after {
-//     content: none;
-//   }
-// }
-
-// //Modals
-// .modal-open {
-//   width: 100%;
-//   padding-right: 0 !important;
-// }
-
-// .modal-content {
-//   box-shadow: 0 0.3rem 1rem rgba(0, 0, 0, 0.1);
-// }
-
-// .modal-header {
-//   border-bottom: 1px solid #e5e5e5;
-// }
-
-// .modal-footer {
-//   border-top: 1px solid #e5e5e5;
-// }
-
-// // When fading in the modal, animate it to slide down
-// .modal.fade .modal-dialog {
-//   @include transition($modal-transition);
-//   transform: $modal-fade-transform;
-// }
-
-// .modal.show .modal-dialog {
-//   transform: $modal-show-transform;
-// }
-
-// // When trying to close, animate focus to scale
-// .modal.modal-static .modal-dialog {
-//   transform: $modal-scale-transform;
-// }
-
-// // col-form-label (substitute for control-label of bootstrap3)
-// .col-form-label {
-//   text-align: right;
-// }
-
-// // label
-// label {
-//   // add with-no-font-weight class in case you do not want to apply font-weight 700 to label
-//   :not(.with-no-font-weight) {
-//     font-weight: 700;
-//   }
-// }
-
-// // disabled button (reproduction from bootstrap3.)
-// // see https://cccabinet.jpn.org/bootstrap4/components/buttons#disabled-state
-// .btn.disabled,
-// .btn[disabled],
-// fieldset[disabled] .btn {
-//   cursor: not-allowed;
-// }
-
-// // progress bar
-// .progress {
-//   margin-bottom: 18px;
-//   overflow: hidden;
-// }
-
-// .text-break {
-//   word-break: break-word;
-//   overflow-wrap: break-word;
-// }
-
-// // prevent tooltip flickering (flashing) on hover
-// .tooltip {
-//   pointer-events: none;
-// }

+ 0 - 32
apps/app/_obsolete/src/styles/theme/_hsl-functions.scss

@@ -1,32 +0,0 @@
-@use 'bootstrap/scss/functions' as bs;
-
-@function contrast($color, $darken-degrees: 0%, $alpha-degrees: 100%) {
-  $color: bs.str-replace($color, 'var(');
-  $color: bs.str-replace($color, ')');
-  $color-hs: var(#{$color+'-hs'});
-  $color-l: var(#{$color+'-l'});
-  // @return hsl($color-hs, clamp(10%, calc((100% - $color-l - 51% ) * 1000), 95%));
-  @return hsla($color-hs, clamp(10%, calc((100% - $color-l - $darken-degrees - 51% ) * 1000), 95%), $alpha-degrees);
-}
-
-@function lighten($color, $degrees) {
-  $color: bs.str-replace($color, 'var(');
-  $color: bs.str-replace($color, ')');
-  $color-hs: var(#{$color+'-hs'});
-  $color-l: var(#{$color+'-l'});
-  @return hsl($color-hs, calc($color-l + $degrees));
-}
-@function darken($color, $degrees) {
-  $color: bs.str-replace($color, 'var(');
-  $color: bs.str-replace($color, ')');
-  $color-hs: var(#{$color+'-hs'});
-  $color-l: var(#{$color+'-l'});
-  @return hsl($color-hs, calc($color-l - $degrees));
-}
-@function alpha($color, $degrees) {
-  $color: bs.str-replace($color, 'var(');
-  $color: bs.str-replace($color, ')');
-  $color-hs: var(#{$color+'-hs'});
-  $color-l: var(#{$color+'-l'});
-  @return hsla($color-hs,$color-l,$degrees);
-}

+ 0 - 108
apps/app/_obsolete/src/styles/theme/_hsl-reboot-bootstrap-theme-colors.scss

@@ -1,108 +0,0 @@
-@use '../bootstrap/init' as *;
-@use '../atoms/mixins/buttons' as mixins-buttons;
-@use './mixins/hsl-button';
-@use './mixins/hsl-badge';
-@use './hsl-functions' as hsl;
-
-$hsl-colors: (
-  'primary': var(--primary),
-  'secondary': var(--secondary)
-);
-
-@each $color, $value in $hsl-colors {
-  .bg-#{$color} {
-    background-color: $value !important;
-  }
-}
-
-@each $color, $value in $hsl-colors {
-  .border-#{$color} {
-    border-color: $value !important;
-  }
-}
-
-// TODO: hover-focus() dropped in bootstrap v5
-// https://redmine.weseek.co.jp/issues/128307
-// @each $color, $value in $hsl-colors {
-//   .text-#{$color} {
-//     color: $value !important;
-//     @if $emphasized-link-hover-darken-percentage != 0 {
-//       a {
-//         @include hover-focus() {
-//           color: hsl.darken($value, $emphasized-link-hover-darken-percentage) !important;
-//         }
-//       }
-//     }
-//   }
-// }
-
-@each $color, $value in $hsl-colors {
-  .btn-#{$color} {
-    @include hsl-button.button-variant($value, $value);
-    @include hsl-button.button-svg-icon-variant($value, $value);
-    box-shadow: none !important;
-  }
-}
-
-@each $color, $value in $hsl-colors {
-  .btn-outline-#{$color} {
-    @include hsl-button.button-outline-variant($value, $value, hsl.alpha($value, 10%), $value);
-    @include mixins-buttons.button-outline-svg-icon-variant($value, $value);
-
-    &:not(:disabled):not(.disabled):active,
-    &:not(:disabled):not(.disabled).active {
-      color: $value;
-      background-color: hsl.alpha($value, 10%);
-      border-color: $value;
-    }
-
-    box-shadow: none !important;
-  }
-
-  // separate style: https://github.com/weseek/growi/pull/6804
-  .show > .btn-outline-#{$color}.dropdown-toggle {
-    color: $value;
-    background-color: hsl.alpha($value, 10%);
-    border-color: $value;
-  }
-}
-
-@each $color, $value in $hsl-colors {
-  .alert-#{$color} {
-    $alert-color: rgba(white,90%);
-    $alert-bg-color: hsl.darken($value, calc($alert-bg-level + 0.95) * $theme-color-interval);
-    $alert-border-color: hsl.darken($value, $alert-border-level * $theme-color-interval);
-
-    color: $alert-color;
-    @include gradient-bg($alert-bg-color);
-    border-color: $alert-border-color;
-
-    hr {
-      border-top-color: hsl.darken($value, calc($alert-border-level + 5) * $theme-color-interval);
-    }
-
-    .alert-link {
-      color: hsl.contrast($value);
-    }
-  }
-
-  // Alert link
-  :root, .wiki {
-    .alert.alert-primary {
-      a,
-      a:hover {
-        color: hsl.darken($value, calc($alert-bg-level - 7.7) * $theme-color-interval);
-      }
-    }
-  }
-}
-
-@each $color, $value in $hsl-colors {
-  .bg-#{$color} {
-    @include hsl-badge.badge-variant($value);
-  }
-
-  a.bg-#{$color}  {
-    @include hsl-badge.badge-variant($value);
-  }
-}

+ 0 - 29
apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-border-colors.scss

@@ -1,29 +0,0 @@
-@use '../bootstrap/init' as *;
-
-//
-// Border
-//
-
-.border {
-  border: $border-width solid $border-color !important;
-}
-
-.border-top {
-  border-top: $border-width solid $border-color !important;
-}
-
-.border-end {
-  border-right: $border-width solid $border-color !important;
-}
-
-.border-bottom {
-  border-bottom: $border-width solid $border-color !important;
-}
-
-.border-start {
-  border-left: $border-width solid $border-color !important;
-}
-
-.border-info {
-  border-color: $info !important;
-}

+ 0 - 22
apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-buttons.scss

@@ -1,22 +0,0 @@
-.btn-link {
-  color: var(--color-link);
-  svg {
-    fill: var(--color-link);
-  }
-
-  &:hover {
-    color: var(--color-link-hover);
-    svg {
-      fill: var(--color-link-hover);
-    }
-  }
-
-  &:disabled,
-  &.disabled {
-    color: var(--color-link-disabled, #{$gray-500});
-    svg {
-      fill: var(--color-link-disabled, #{$gray-500});
-    }
-  }
-}
-

+ 0 - 60
apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-colors.scss

@@ -1,60 +0,0 @@
-//
-//
-// Apply partially
-//   https://github.com/twbs/bootstrap/blob/v4.5.0/scss/_reboot.scss
-//
-//
-
-// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix
-
-// Body
-//
-// 1. Remove the margin in all browsers.
-// 2. As a best practice, apply a default `background-color`.
-// 3. Set an explicit initial text-align value so that we can later use
-//    the `inherit` value on things like `<th>` elements.
-
-html, body {
-  color: var(--color-global);
-  background-color: var(--bgcolor-global); // 2
-
-  svg {
-    fill: var(--color-global);
-  }
-}
-
-// Links
-
-a {
-  color: var(--color-link);
-  text-decoration: $link-decoration;
-  background-color: transparent; // Remove the gray background on active links in IE 10.
-
-  svg {
-    fill: var(--color-link);
-  }
-
-  &:hover {
-    color: var(--color-link-hover);
-    text-decoration: $link-hover-decoration;
-
-    svg {
-      fill: var(--color-link-hover);
-    }
-  }
-}
-
-// And undo these styles for placeholder links/named anchors (without href).
-// It would be more straightforward to just use a[href] in previous block, but that
-// causes specificity issues in many other styles that are too complex to fix.
-// See https://github.com/twbs/bootstrap/issues/19402
-
-// a:not([href]) {
-//   color: inherit;
-//   text-decoration: none;
-
-//   &:hover {
-//     color: inherit;
-//     text-decoration: none;
-//   }
-// }

+ 0 - 38
apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-dropdown.scss

@@ -1,38 +0,0 @@
-@use '../bootstrap/init' as *;
-
-.dropdown-menu {
-  color: $color-dropdown;
-  svg {
-    fill: $color-dropdown;
-  }
-
-  background-color: $bgcolor-dropdown;
-}
-
-.dropdown-item {
-  color: $color-dropdown-link;
-  svg {
-    fill: $color-dropdown-link;
-  }
-
-  // TODO: hover-focus() dropped in bootstrap v5
-  @include hover-focus() {
-    color: $color-dropdown-link;
-    svg {
-      fill: $color-dropdown-link-hover;
-    }
-
-    @include gradient-bg($bgcolor-dropdown-link-hover);
-  }
-
-  &:active,
-  &.active,
-  &:active:hover,
-  &.active:hover {
-    color: $color-dropdown-link-active;
-    background-color:  $bgcolor-dropdown-link-active;
-    svg {
-      fill: $color-dropdown-link-active;
-    }
-  }
-}

+ 0 - 52
apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-nav.scss

@@ -1,52 +0,0 @@
-//
-//
-// Apply partially
-//   https://github.com/twbs/bootstrap/blob/v4.5.0/scss/_nav.scss
-//
-//
-
-.nav-link {
-  // Disabled state lightens text
-  &.disabled {
-    color: $nav-link-disabled-color;
-    svg {
-      fill: $nav-link-disabled-color;
-    }
-  }
-}
-
-//
-// Tabs
-//
-
-.nav-tabs {
-  border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color;
-
-  .nav-link {
-    border: $nav-tabs-border-width solid transparent;
-    @include border-top-radius($nav-tabs-border-radius);
-
-    // TODO: hover-focus() dropped in bootstrap v5
-    @include hover-focus() {
-      border-color: $nav-tabs-link-hover-border-color;
-    }
-
-    &.disabled {
-      color: $nav-link-disabled-color;
-      background-color: transparent;
-      border-color: transparent;
-    }
-  }
-
-  .nav-link.active,
-  .nav-item.show .nav-link {
-    color: $nav-tabs-link-active-color;
-    background-color: $nav-tabs-link-active-bg;
-    border-color: $nav-tabs-link-active-border-color;
-  }
-
-  .dropdown-menu {
-    // Remove the top rounded corners here since there is a hard edge above the menu
-    @include border-top-radius(0);
-  }
-}

+ 0 - 74
apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-tables.scss

@@ -1,74 +0,0 @@
-@use '../bootstrap/init' as *;
-
-//
-//
-// Apply partially
-//   https://github.com/twbs/bootstrap/blob/v4.5.0/scss/_tables.scss
-//
-//
-
-.table {
-  color: $color-table;
-  background-color: $bgcolor-table; // Reset for nesting within parents with `background-color`.
-
-  th,
-  td {
-    border-top-color: $border-color-table;
-  }
-
-  thead th {
-    border-bottom-color: $border-color-table;
-  }
-
-  tbody + tbody {
-    border-top-color: $border-color-table;
-  }
-}
-
-.table-bordered {
-  border-color: $border-color-table;
-
-  th,
-  td {
-    border-color: $border-color-table;
-  }
-}
-
-.table-hover {
-  tbody tr {
-    &:hover {
-      color: $color-table-hover;
-      background-color: $bgcolor-table-hover;
-    }
-  }
-}
-
-.table-dark {
-  color: $table-dark-color;
-  background-color: $table-dark-bg;
-
-  th,
-  td,
-  thead th {
-    border-color: $table-dark-border-color;
-  }
-
-  &.table-bordered {
-    border: 0;
-  }
-
-  &.table-striped {
-    tbody tr:nth-of-type(#{$table-striped-order}) {
-      background-color: $table-dark-accent-bg;
-    }
-  }
-
-  &.table-hover {
-    tbody tr {
-      &:hover {
-        color: $table-dark-hover-color;
-        background-color: $table-dark-hover-bg;
-      }
-    }
-  }
-}

+ 0 - 3
apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-text.scss

@@ -1,3 +0,0 @@
-.text-muted {
-  color: $text-muted !important;
-}

+ 0 - 103
apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-theme-colors.scss

@@ -1,103 +0,0 @@
-@use '../bootstrap/init' as *;
-@use '../atoms/mixins/buttons' as mixins-buttons;
-
-@each $color, $value in $theme-colors {
-  @include bg-variant('.bg-#{$color}', $value);
-}
-
-@each $color, $value in $theme-colors {
-  .border-#{$color} {
-    border-color: $value;
-  }
-}
-
-@each $color, $value in $theme-colors {
-  @include text-emphasis-variant('.text-#{$color}', $value, true);
-}
-
-@each $color, $value in $theme-colors {
-  .btn-#{$color} {
-    @include button-variant($value, $value);
-    @include mixins-buttons.button-svg-icon-variant($value, $value);
-    box-shadow: none !important;
-  }
-}
-
-@each $color, $value in $theme-colors {
-  .btn-outline-#{$color} {
-    @include button-outline-variant($value, $color-hover: $value, $active-background: rgba($value, 0.1), $active-border: $value);
-    @include mixins-buttons.button-outline-svg-icon-variant($value, $color-hover: $value);
-
-    &:not(:disabled):not(.disabled):active,
-    &:not(:disabled):not(.disabled).active {
-      color: $value;
-      background-color: rgba($value, 0.1);
-      border-color: $value;
-    }
-
-    box-shadow: none !important;
-  }
-
-  // separate style: https://github.com/weseek/growi/pull/6804
-  .show > .btn-outline-#{$color}.dropdown-toggle {
-    color: $value;
-    background-color: rgba($value, 0.1);
-    border-color: $value;
-  }
-}
-
-@each $theme-color, $color in $theme-colors {
-  .form-check-#{$theme-color} {
-    .form-check-label::before {
-      border-color: $input-border-color;
-      transition: 0.3s ease-in-out;
-    }
-    .form-check-input:checked + .form-check-label::before {
-      background-color: $color;
-      border-color: $color;
-    }
-    .form-check-input:checked + .form-check-label::after {
-      color: var(--bgcolor-global);
-    }
-    .form-check-input:not(:disabled):active ~ .form-check-label::before {
-      color: var(--bgcolor-global);
-      background-color: $color;
-      border-color: $color;
-    }
-    .form-check-input:focus:not(:checked) ~ .form-check-label::before {
-      color: var(--bgcolor-global);
-      background-color: var(--bgcolor-global);
-      border-color: $input-focus-border-color;
-    }
-  }
-}
-
-// TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-// theme-color-level() dropped in bootstrap v5
-// @each $color, $value in $theme-colors {
-//   .alert-#{$color} {
-//     @include alert-variant(
-//       theme-color-level($color, $alert-bg-level),
-//       theme-color-level($color, $alert-border-level),
-//       theme-color-level($color, $alert-color-level)
-//     );
-//   }
-//   // Alert link
-//   :root, .wiki {
-//     .alert.alert-#{$color} {
-//       a,
-//       a:hover {
-//         color: theme-color-level($color, $alert-color-level - 2);
-//       }
-//     }
-//   }
-// }
-
-@each $color, $value in $theme-colors {
-  .bg-#{$color} {
-    @include badge-variant($value);
-  }
-  a.bg-#{$color} {
-    @include badge-variant($value);
-  }
-}

+ 0 - 9
apps/app/_obsolete/src/styles/theme/mixins/_count-badge.scss

@@ -1,9 +0,0 @@
-@mixin count-badge($color, $bg-color, $min-width: initial) {
-  min-width: $min-width;
-  padding: 0.1rem 0.5rem;
-  font-family: var(--font-family-monospace);
-  font-size: 12px;
-  font-weight: 500;
-  color: $color;
-  background-color: $bg-color;
-}

+ 0 - 23
apps/app/_obsolete/src/styles/theme/mixins/_hsl-badge.scss

@@ -1,23 +0,0 @@
-@use '../../bootstrap/init' as bs;
-@use '../hsl-functions' as hsl;
-
-// @mixin badge-variant($bg) {
-@mixin badge-variant($bg) {
-  color: hsl.contrast($bg);
-  background-color: $bg;
-
-  @at-root a#{&} {
-    // TODO: hover-focus() dropped in bootstrap v5
-    // https://redmine.weseek.co.jp/issues/128307
-    // @include bs.hover-focus() {
-    //   color: hsl.contrast($bg);
-    //   background-color: hsl.darken($bg, 10%);
-    // }
-
-    &:focus,
-    &.focus {
-      outline: 0;
-      // box-shadow: 0 0 0 $badge-focus-width hsl.alpha($bg, 50%);
-    }
-  }
-}

+ 0 - 146
apps/app/_obsolete/src/styles/theme/mixins/_hsl-button.scss

@@ -1,146 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-@use '../hsl-functions' as hsl;
-
-// @mixin button-variant($background, $border, $hover-background: darken($background, 7.5%), $hover-border: darken($border, 10%), $active-background: darken($background, 10%), $active-border: darken($border, 12.5%)) {
-@mixin button-variant($background, $border, $hover-background-darken-degrees: 7.5%, $hover-border-darken-degrees: 10%, $active-background-darken-degrees: 10%, $active-border-darken-degrees: 12.5%) {
-  $hover-background: hsl.darken($background, $hover-background-darken-degrees);  // DO NOT ... twice
-  $hover-border: hsl.darken($border, $hover-border-darken-degrees);  // DO NOT ... twice
-
-  color: hsl.contrast($background);
-  @include bs.gradient-bg($background);
-  border-color: $border;
-  // @include box-shadow($btn-box-shadow);
-
-  &:hover {
-    color: hsl.contrast($background);
-    @include bs.gradient-bg($hover-background);
-    border-color: $hover-border;
-  }
-
-  &:focus,
-  &.focus {
-    color: hsl.contrast($background);
-    @include bs.gradient-bg($hover-background);
-    border-color: $hover-border;
-    // TODO: color-yiq() to color-contrast()
-    // https://redmine.weseek.co.jp/issues/128307
-    // @if $enable-shadows {
-    //   @include box-shadow($btn-box-shadow, 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5));
-    // } @else {
-    //   // Avoid using mixin so we can pass custom focus shadow properly
-    //   box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);
-    // }
-  }
-
-  // // Disabled comes first so active can properly restyle
-  &.disabled,
-  &:disabled {
-    color: hsl.contrast($background);
-    @include bs.gradient-bg($background);
-    border-color: $border;
-    // Remove CSS gradients if they're enabled
-    @if bs.$enable-gradients {
-      background-image: none;
-    }
-  }
-
-  &:not(:disabled):not(.disabled):active,
-  &:not(:disabled):not(.disabled).active,
-  .show > &.dropdown-toggle {
-    color: hsl.contrast($background);
-    background-color: $hover-background;
-    border-color: $hover-border;
-  }
-  //   @if $enable-gradients {
-  //     background-image: none; // Remove the gradient for the pressed/active state
-  //   }
-  //   border-color: $active-border;
-
-  // TODO: color-yiq() to color-contrast()
-  // https://redmine.weseek.co.jp/issues/128307
-  //   &:focus {
-  //     // @if $enable-shadows and $btn-active-box-shadow != none {
-  //     //   @include box-shadow($btn-active-box-shadow, 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5));
-  //     // } @else {
-  //     //   // Avoid using mixin so we can pass custom focus shadow properly
-  //     //   box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);
-  //     // }
-  //   }
-  // }
-}
-
-// @mixin button-outline-variant($color, $color-hover: color-yiq($color), $active-background: $color, $active-border: $color) {
-@mixin button-outline-variant($color, $color-hover: hsl.contrast($color), $active-background: $color, $active-border: $color) {
-  color: $color;
-  border-color: $color;
-
-  &:hover {
-    color: $color-hover;
-    background-color: $active-background;
-    border-color: $active-border;
-  }
-
-  // &:focus,
-  // &.focus {
-  //   box-shadow: 0 0 0 bs.$btn-focus-width hsl.alpha($color,50%);
-  // }
-
-  &.disabled,
-  &:disabled {
-    color: $color;
-    background-color: transparent;
-  }
-
-  // &:not(:disabled):not(.disabled):active,
-  // &:not(:disabled):not(.disabled).active,
-  // .show > &.dropdown-toggle {
-  //   color: hsl.contrast($active-background);
-  //   background-color: $active-background;
-  //   border-color: $active-border;
-
-  // &:focus {
-  //   @if $enable-shadows and $btn-active-box-shadow != none {
-  //     @include bs.box-shadow($btn-active-box-shadow, 0 0 0 $btn-focus-width hsl.alpha($color,50%));
-  //   } @else {
-  //     // Avoid using mixin so we can pass custom focus shadow properly
-  //     box-shadow: 0 0 0 $btn-focus-width hsl.alpha($color,50%);
-  //   }
-  // }
-  // }
-}
-
-// @mixin button-svg-icon-variant($background, $hover-background: darken($background, 7.5%), $active-background: darken($background, 10%)) {
-@mixin button-svg-icon-variant($background, $hover-background-darken-degrees: 7.5%, $active-background-darken-degrees: 10%) {
-  svg {
-    fill: hsl.contrast($background);
-  }
-
-  &:hover {
-    svg {
-      fill: hsl.contrast($background);
-    }
-  }
-
-  &:focus,
-  &.focus {
-    svg {
-      fill: hsl.contrast($background);
-    }
-  }
-
-  // Disabled comes first so active can properly restyle
-  &.disabled,
-  &:disabled {
-    svg {
-      fill: hsl.contrast($background);
-    }
-  }
-
-  &:not(:disabled):not(.disabled):active,
-  &:not(:disabled):not(.disabled).active,
-  .show > &.dropdown-toggle {
-    svg {
-      fill: hsl.contrast($background);
-    }
-  }
-}

+ 0 - 72
apps/app/_obsolete/src/styles/theme/mixins/_list-group.scss

@@ -1,72 +0,0 @@
-@use '@growi/core/scss/bootstrap/init' as bs;
-
-@use '../../mixins';
-@use './count-badge';
-
-@mixin override-list-group-item($color, $bgcolor, $color-hover: $color, $bgcolor-hover: $bgcolor, $color-active: $color, $bgcolor-active: $bgcolor) {
-  .list-group {
-    .list-group-item {
-      color: $color;
-      background-color: $bgcolor;
-      border-color: $border-color-global;
-
-      &.list-group-item-action {
-        &:hover {
-          background-color: $bgcolor-hover;
-        }
-        &.active {
-          color: $color-active;
-          background-color: $bgcolor-active;
-        }
-      }
-    }
-  }
-}
-
-@mixin override-list-group-item-for-pagetree($color, $bgcolor-hover, $bgcolor-active, $btn-color, $btn-color-hover, $btn-bgcolor-hover, $btn-bgcolor-active) {
-  .grw-pagetree-is-over {
-    background: $bgcolor-hover;
-  }
-  .list-group-item {
-    color: $color;
-    background-color: transparent;
-    border-color: $border-color-global;
-
-    .grw-count-badge {
-      @include count-badge.count-badge($btn-color, $bgcolor-hover, 28px);
-    }
-
-    .btn.btn-page-item-control {
-      color: $btn-color;
-      background-color: transparent;
-      &:hover {
-        color: $btn-color-hover;
-        background-color: $btn-bgcolor-hover;
-      }
-      &:not(:disabled):not(.disabled):active,
-      &:not(:disabled):not(.disabled).active {
-        color: $btn-color-hover;
-        background-color: $btn-bgcolor-active;
-      }
-    }
-
-    &.grw-pagetree-current-page-item {
-      background: $bgcolor-hover;
-    }
-
-    &.list-group-item-action {
-      &:hover {
-        background-color: $bgcolor-hover;
-      }
-      &:active {
-        background-color: $bgcolor-active;
-      }
-    }
-    .grw-pagetree-title-anchor,
-    .grw-foldertree-title-anchor {
-      .grw-sidebar-text-muted {
-        // color: rgba(desaturate($color, 50%), 0.6);
-      }
-    }
-  }
-}

+ 0 - 22
apps/app/_obsolete/src/styles/theme/mixins/_page-editor-mode-manager.scss

@@ -1,22 +0,0 @@
-@mixin btn-page-editor-mode-manager($textColor, $borderColor, $bgColorHoverAndActive, $bgColor: white) {
-  color: $textColor;
-  background-color: $bgColor;
-  border-color: $borderColor;
-
-  &:not(:first-child) {
-    &::before {
-      border-left-color: $borderColor;
-    }
-  }
-
-  &:hover,
-  &:active,
-  &.active {
-    color: $textColor;
-    background-color: $bgColorHoverAndActive;
-    border-color: $borderColor;
-    &::after {
-      border-color: $bgColorHoverAndActive;
-    }
-  }
-}

+ 0 - 159
apps/app/bin/cdn/cdn-resources-downloader.ts

@@ -1,159 +0,0 @@
-import path from 'path';
-import { URL } from 'url';
-import urljoin from 'url-join';
-import { Transform } from 'stream';
-import replaceStream from 'replacestream';
-
-import { cdnLocalScriptRoot, cdnLocalStyleRoot, cdnLocalStyleWebRoot } from '^/config/cdn';
-import * as cdnManifests from '^/resource/cdn-manifests';
-
-import { CdnResource, CdnManifest } from '~/interfaces/cdn';
-import loggerFactory from '~/utils/logger';
-import { downloadTo } from '~/utils/download';
-
-const logger = loggerFactory('growi:service:CdnResourcesDownloader');
-
-export default class CdnResourcesDownloader {
-
-  async downloadAndWriteAll(): Promise<any> {
-    const cdnScriptResources: CdnResource[] = cdnManifests.js.map((manifest: CdnManifest) => {
-      return { manifest, outDir: cdnLocalScriptRoot };
-    });
-
-    const cdnStyleResources: CdnResource[] = cdnManifests.style.map((manifest) => {
-      return { manifest, outDir: cdnLocalStyleRoot };
-    });
-
-    const dlStylesOptions = {
-      replaceUrl: {
-        webroot: cdnLocalStyleWebRoot,
-      },
-    };
-
-    return Promise.all([
-      this.downloadScripts(cdnScriptResources),
-      this.downloadStyles(cdnStyleResources, dlStylesOptions),
-    ]);
-  }
-
-  /**
-   * Download script files from CDN
-   * @param cdnResources JavaScript resource data
-   * @param options
-   */
-  private async downloadScripts(cdnResources: CdnResource[], options?: any): Promise<any> {
-    logger.debug('Downloading scripts', cdnResources);
-
-    const opts = Object.assign({}, options);
-    const ext = opts.ext || 'js';
-
-    const promises = cdnResources.map((cdnResource) => {
-      const { manifest } = cdnResource;
-
-      logger.info(`Processing CdnResource '${manifest.name}'`);
-
-      return downloadTo(
-        manifest.url,
-        cdnResource.outDir,
-        `${manifest.name}.${ext}`,
-      );
-    });
-
-    return Promise.all(promises);
-  }
-
-  /**
-   * Download style sheet file from CDN
-   *  Assets in CSS is also downloaded
-   * @param cdnResources CSS resource data
-   * @param options
-   */
-  private async downloadStyles(cdnResources: CdnResource[], options?: any): Promise<any> {
-    logger.debug('Downloading styles', cdnResources);
-
-    const opts = Object.assign({}, options);
-    const ext = opts.ext || 'css';
-
-    // styles
-    const assetsResourcesStore: CdnResource[] = [];
-    const promisesForStyle = cdnResources.map((cdnResource) => {
-      const { manifest } = cdnResource;
-
-      logger.info(`Processing CdnResource '${manifest.name}'`);
-
-      let urlReplacer: Transform|null = null;
-
-      // generate replaceStream instance
-      if (opts.replaceUrl != null) {
-        urlReplacer = this.generateReplaceUrlInCssStream(cdnResource, assetsResourcesStore, opts.replaceUrl.webroot);
-      }
-
-      return downloadTo(
-        manifest.url,
-        cdnResource.outDir,
-        `${manifest.name}.${ext}`,
-        urlReplacer,
-      );
-    });
-
-    // wait until all styles are downloaded
-    await Promise.all(promisesForStyle);
-
-    logger.debug('Downloading assets', assetsResourcesStore);
-
-    // assets in css
-    const promisesForAssets = assetsResourcesStore.map((cdnResource) => {
-      const { manifest } = cdnResource;
-
-      logger.info(`Processing assts in css '${manifest.name}'`);
-
-      return downloadTo(
-        manifest.url,
-        cdnResource.outDir,
-        manifest.name,
-      );
-    });
-
-    return Promise.all(promisesForAssets);
-  }
-
-  /**
-   * Generate replaceStream instance to replace 'url(..)'
-   *
-   * e.g.
-   *  Before  : url(../images/logo.svg)
-   *  After   : url(/path/to/webroot/${cdnResources.name}/logo.svg)
-   *
-   * @param cdnResource CSS resource data
-   * @param assetsResourcesStore An array to store CdnResource that is detected by 'url()' in CSS
-   * @param webroot
-   */
-  private generateReplaceUrlInCssStream(cdnResource: CdnResource, assetsResourcesStore: CdnResource[], webroot: string): Transform {
-    return replaceStream(
-      /url\((?!['"]?data:)["']?(.+?)["']?\)/g, // https://regex101.com/r/Sds38A/3
-      (match, url) => {
-        // generate URL Object
-        const parsedUrl = url.startsWith('http')
-          ? new URL(url) // when url is fqcn
-          : new URL(url, cdnResource.manifest.url); // when url is relative
-        const basename = path.basename(parsedUrl.pathname);
-
-        logger.debug(`${cdnResource.manifest.name} has ${parsedUrl.toString()}`);
-
-        // add assets metadata to download later
-        const replacedCdnResource = {
-          manifest: {
-            name: basename,
-            url: parsedUrl.toString(),
-          },
-          outDir: path.join(cdnResource.outDir, cdnResource.manifest.name),
-        };
-        assetsResourcesStore.push(replacedCdnResource);
-
-        const replaceUrl = urljoin(webroot, cdnResource.manifest.name, basename);
-        return `url(${replaceUrl})`;
-      },
-    );
-  }
-
-}

+ 0 - 33
apps/app/bin/download-cdn-resources.ts

@@ -1,33 +0,0 @@
-/**
- * the tool for download CDN resources and save as file
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- */
-import { envUtils } from '@growi/core/dist/utils';
-
-import loggerFactory from '../src/utils/logger';
-
-import CdnResourcesDownloader from './cdn/cdn-resources-downloader';
-
-const logger = loggerFactory('growi:bin:download-cdn-resources');
-
-// check env var
-const noCdn: boolean = envUtils.toBoolean(process.env.NO_CDN);
-if (!noCdn) {
-  logger.info('Using CDN. No resources are downloaded.');
-  // exit
-  process.exit(0);
-}
-
-logger.info('This is NO_CDN mode. Start to download resources.');
-
-
-const downloader = new CdnResourcesDownloader();
-
-downloader.downloadAndWriteAll()
-  .then(() => {
-    logger.info('Download is completed successfully');
-  })
-  .catch((err) => {
-    logger.error(err);
-  });

+ 0 - 1
apps/app/config/logger/config.dev.js

@@ -25,7 +25,6 @@ module.exports = {
   // 'growi:lib:importer': 'debug',
   // 'growi:routes:page': 'debug',
   'growi-plugin:*': 'debug',
-  // 'growi:InterceptorManager': 'debug',
   'growi:service:search-delegator:elasticsearch': 'debug',
   'growi:service:g2g-transfer': 'debug',
   'growi:service:questionnaire': 'debug',

+ 0 - 1
apps/app/package.json

@@ -246,7 +246,6 @@
     "diff2html": "^3.4.35",
     "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
-    "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-regex": "^1.8.0",

+ 0 - 221
apps/app/resource/cdn-manifests.js

@@ -1,221 +0,0 @@
-module.exports = {
-  js: [
-    {
-      name: 'basis',
-      // eslint-disable-next-line max-len
-      url: 'https://cdn.jsdelivr.net/combine/npm/jquery@3.4.0,npm/popper.js@1.15.0,npm/bootstrap@4.5.0/dist/js/bootstrap.min.js,npm/scrollpos-styler@0.7.1,npm/jquery-slimscroll@1.3.8/jquery.slimscroll.min.js',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'highlight',
-      url: 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/highlight.min.js',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'highlight-addons',
-      url: 'https://cdn.jsdelivr.net/combine/'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/dockerfile.min.js,'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/go.min.js,'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/gradle.min.js,'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/json.min.js,'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/less.min.js,'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/plaintext.min.js,'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/scss.min.js,'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/typescript.min.js,'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js,'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/swift.min.js,'
-        + 'gh/highlightjs/cdn-release@9.13.0/build/languages/kotlin.min.js,'
-        + 'npm/highlightjs-line-numbers.js@2.6.0/dist/highlightjs-line-numbers.min.js',
-      args: {
-        async: true,
-        integrity: '',
-      },
-    },
-    {
-      name: 'mathjax',
-      url: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js',
-      args: {
-        async: true,
-        integrity: '',
-      },
-    },
-    {
-      name: 'drawio-viewer',
-      url: 'https://jgraph.github.io/drawio/src/main/webapp/js/viewer.min.js',
-      args: {
-        async: true,
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-dialog',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/addon/dialog/dialog.min.js',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-keymap-vim',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/keymap/vim.min.js',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-keymap-emacs',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/keymap/emacs.min.js',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-keymap-sublime',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/keymap/sublime.min.js',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'redoc-standalone',
-      url: 'https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js',
-      args: {
-        integrity: '',
-      },
-    },
-  ],
-  style: [
-    {
-      name: 'lato',
-      url: 'https://fonts.googleapis.com/css?family=Lato:400,700',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'Press Start 2P',
-      url: 'https://fonts.googleapis.com/css?family=Press+Start+2P',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'font-awesome',
-      url: 'https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'themify-icons',
-      url: 'https://cdn.jsdelivr.net/npm/cd-themify-icons@0.0.1/index.min.css',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'simple-line-icons',
-      url: 'https://cdn.jsdelivr.net/npm/simple-line-icons@2.4.1/css/simple-line-icons.min.css',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'material-icons',
-      url: 'https://cdn.jsdelivr.net/npm/material-icons@0.3.1/iconfont/material-icons.min.css',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
-
-    {
-      name: 'animate.css',
-      url: 'https://cdn.jsdelivr.net/npm/animate.css@3.7.2/animate.min.css',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'highlight-theme-github',
-      url: 'https://cdn.jsdelivr.net/npm/highlight.js@9.13.0/styles/github.css',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-dialog',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/addon/dialog/dialog.min.css',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-theme-eclipse',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/eclipse.min.css',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-theme-elegant',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/elegant.min.css',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-theme-neo',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/neo.min.css',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-theme-mdn-like',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/mdn-like.min.css',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-theme-material',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/material.min.css',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-theme-dracula',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/dracula.min.css',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-theme-monokai',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/monokai.min.css',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'codemirror-theme-twilight',
-      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.64.0/theme/twilight.min.css',
-      args: {
-        integrity: '',
-      },
-    },
-  ],
-};

+ 30 - 9
apps/app/src/client/services/side-effects/page-updated.ts

@@ -3,17 +3,24 @@ import { useCallback, useEffect } from 'react';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 
 import { SocketEventName } from '~/interfaces/websocket';
-import { useCurrentPageId } from '~/stores/page';
+import { usePageStatusAlert } from '~/stores/alert';
+import { useSWRxCurrentPage, useSWRMUTxCurrentPage } from '~/stores/page';
 import { useSetRemoteLatestPageData, type RemoteRevisionData } from '~/stores/remote-latest-page';
+import { useEditorMode, EditorMode } from '~/stores/ui';
+
 
 export const usePageUpdatedEffect = (): void => {
 
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
 
   const { data: socket } = useGlobalSocket();
-  const { data: currentPageId } = useCurrentPageId();
+  const { data: editorMode } = useEditorMode();
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
+  const { open: openPageStatusAlert, close: closePageStatusAlert } = usePageStatusAlert();
 
-  const setLatestRemotePageData = useCallback((data) => {
+  const remotePageDataUpdateHandler = useCallback((data) => {
+    // Set remote page data
     const { s2cMessagePageUpdated } = data;
 
     const remoteData: RemoteRevisionData = {
@@ -23,22 +30,36 @@ export const usePageUpdatedEffect = (): void => {
       remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
     };
 
-    if (currentPageId != null && currentPageId === s2cMessagePageUpdated.pageId) {
+    if (currentPage?._id != null && currentPage._id === s2cMessagePageUpdated.pageId) {
       setRemoteLatestPageData(remoteData);
-    }
 
-  }, [currentPageId, setRemoteLatestPageData]);
+      // Open PageStatusAlert
+      const currentRevisionId = currentPage?.revision?._id;
+      const remoteRevisionId = s2cMessagePageUpdated.revisionId;
+      const isRevisionOutdated = (currentRevisionId != null || remoteRevisionId != null) && currentRevisionId !== remoteRevisionId;
+
+      // !!CAUTION!! Timing of calling openPageStatusAlert may clash with components/PageEditor/conflict.tsx
+      if (isRevisionOutdated && editorMode === EditorMode.View) {
+        openPageStatusAlert({ hideEditorMode: EditorMode.Editor, onRefleshPage: mutateCurrentPage });
+      }
+
+      // Clear cache
+      if (!isRevisionOutdated) {
+        closePageStatusAlert();
+      }
+    }
+  }, [currentPage?._id, currentPage?.revision?._id, editorMode, mutateCurrentPage, openPageStatusAlert, closePageStatusAlert, setRemoteLatestPageData]);
 
   // listen socket for someone updating this page
   useEffect(() => {
 
     if (socket == null) { return }
 
-    socket.on(SocketEventName.PageUpdated, setLatestRemotePageData);
+    socket.on(SocketEventName.PageUpdated, remotePageDataUpdateHandler);
 
     return () => {
-      socket.off(SocketEventName.PageUpdated, setLatestRemotePageData);
+      socket.off(SocketEventName.PageUpdated, remotePageDataUpdateHandler);
     };
 
-  }, [setLatestRemotePageData, socket]);
+  }, [remotePageDataUpdateHandler, socket]);
 };

+ 0 - 0
apps/app/src/client/services/update-page/conflict.ts → apps/app/src/client/services/update-page/conflict.tsx


+ 0 - 6
apps/app/src/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -49,12 +49,6 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
               defaultValue={adminCustomizeContainer.state.currentCustomizeCss || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeCss(e.target.value) }}
             />
-            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
-            <p className="form-text text-muted text-end">
-              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-              {t('admin:customize_settings.ctrl_space')}
-            </p>
-            */}
           </div>
 
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />

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

@@ -53,12 +53,6 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
               defaultValue={adminCustomizeContainer.state.currentCustomizeNoscript || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeNoscript(e.target.value) }}
             />
-            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
-            <span className="form-text text-muted text-end">
-              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
-              {t('admin:customize_settings.ctrl_space')}
-            </span>
-            */}
           </div>
 
           <a

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

@@ -50,12 +50,6 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
               defaultValue={adminCustomizeContainer.state.currentCustomizeScript || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeScript(e.target.value) }}
             />
-            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
-            <span className="form-text text-muted text-end">
-              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-              {t('admin:customize_settings.ctrl_space')}
-            </span>
-            */}
           </div>
 
           <a

+ 4 - 4
apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -7,9 +7,9 @@ import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option
 
 
 const MODE_ATTR_MAP = {
-  insert: { color: 'info', icon: 'icon-plus', label: 'Insert' },
-  upsert: { color: 'success', icon: 'icon-plus', label: 'Upsert' },
-  flushAndInsert: { color: 'danger', icon: 'icon-refresh', label: 'Flush and Insert' },
+  insert: { color: 'info', icon: 'add_circle', label: 'Insert' },
+  upsert: { color: 'success', icon: 'add_circle', label: 'Upsert' },
+  flushAndInsert: { color: 'danger', icon: 'autorenew', label: 'Flush and Insert' },
 };
 
 export const DEFAULT_MODE = 'insert';
@@ -72,7 +72,7 @@ export default class ImportCollectionItem extends React.Component {
   renderModeLabel(mode, isColorized = false) {
     const attrMap = MODE_ATTR_MAP[mode];
     const className = isColorized ? `text-${attrMap.color}` : '';
-    return <span className={`text-nowrap ${className}`}><i className={attrMap.icon}></i> {attrMap.label}</span>;
+    return <span className={`text-nowrap ${className}`}><span className="material-symbols-outlined">{attrMap.icon}</span> {attrMap.label}</span>;
   }
 
   renderCheckbox() {

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

@@ -93,10 +93,7 @@ const CustomBotWithProxySettings = (props) => {
     <>
       <h2 className="admin-setting-header mb-2">{t('admin:slack_integration.custom_bot_with_proxy_integration')}
         <a href={t('admin:slack_integration.docs_url.custom_bot_with_proxy')} target="_blank" rel="noopener noreferrer">
-          <i
-            className="fa fa-external-link btn-link ms-2"
-            aria-hidden="true"
-          />
+          <span className="growi-custom-icons btn-link ms-2 fs-3">external_link</span>
         </a>
       </h2>
 

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

@@ -24,10 +24,7 @@ const CustomBotWithoutProxySettings = (props) => {
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_integration')}
         <a href={t('admin:slack_integration.docs_url.custom_bot_without_proxy')} target="_blank" rel="noopener noreferrer">
-          <i
-            className="fa fa-external-link btn-link ms-2"
-            aria-hidden="true"
-          />
+          <span className="growi-custom-icons btn-link ms-2 fs-3">external_link</span>
         </a>
       </h2>
 

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

@@ -78,7 +78,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
         <div className="my-5 d-flex flex-column align-items-center">
           <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
             {t('admin:slack_integration.accordion.create_bot')}
-            <i className="fa fa-external-link ms-2" aria-hidden="true" />
+            <span className="growi-custom-icons ms-2"><small>external_link</small></span>
           </button>
           <a
             href={t('admin:slack_integration.docs_url.custom_bot_without_proxy_setting')}
@@ -88,7 +88,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
             <p className="text-center mt-1">
               <small>
                 {t('admin:slack_integration.accordion.how_to_create_a_bot')}
-                <i className="fa fa-external-link ms-2" aria-hidden="true" />
+                <span className="growi-custom-icons ms-2"><small><small>external_link</small></small></span>
               </small>
             </p>
           </a>

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

@@ -77,11 +77,12 @@ const OfficialBotSettings = (props) => {
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_integration')}
         <a href={t('admin:slack_integration.docs_url.official_bot')} target="_blank" rel="noopener noreferrer">
-          <i
-            className="fa fa-external-link btn-link ms-2"
-            aria-hidden="true"
+          <span
+            className="growi-custom-icons btn-link ms-2 fs-3"
             onClick={() => window.open(`${t('admin:slack_integration.docs_url.official_bot')}`, '_blank')}
-          />
+          >
+            external_link
+          </span>
         </a>
       </h2>
 

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

@@ -25,7 +25,7 @@ const BotCreateProcess = () => {
     <div className="my-5 d-flex flex-column align-items-center">
       <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
         {t('admin:slack_integration.accordion.create_bot')}
-        <i className="fa fa-external-link ms-2" aria-hidden="true" />
+        <span className="growi-custom-icons ms-2"><small>external_link</small></span>
       </button>
       <a
         href={t('admin:slack_integration.docs_url.custom_bot_with_proxy_setting')}
@@ -35,7 +35,7 @@ const BotCreateProcess = () => {
         <p className="text-center mt-1">
           <small>
             {t('admin:slack_integration.accordion.how_to_create_a_bot')}
-            <i className="fa fa-external-link ms-2" aria-hidden="true" />
+            <span className="growi-custom-icons ms-2"><small><small>external_link</small></small></span>
           </small>
         </p>
       </a>
@@ -49,7 +49,7 @@ const BotInstallProcessForOfficialBot = () => {
     <div className="my-5 d-flex flex-column align-items-center">
       <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://slackbot-proxy.growi.org/', '_blank')}>
         {t('admin:slack_integration.accordion.install_now')}
-        <i className="fa fa-external-link ms-2" aria-hidden="true" />
+        <span className="growi-custom-icons ms-2"><small>external_link</small></span>
       </button>
       <a
         href={t('admin:slack_integration.docs_url.official_bot_setting')}
@@ -59,7 +59,7 @@ const BotInstallProcessForOfficialBot = () => {
         <p className="text-center mt-1">
           <small>
             {t('admin:slack_integration.accordion.how_to_install')}
-            <i className="fa fa-external-link ms-2" aria-hidden="true" />
+            <span className="growi-custom-icons ms-2"><small><small>external_link</small></small></span>
           </small>
         </p>
       </a>

+ 2 - 8
apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -30,8 +30,6 @@ type Props = {
 type AvailableOption = {
   id: number,
   actionForPages: PageActionOnGroupDelete,
-  iconClass: string,
-  styleClass: string,
   label: string,
 };
 
@@ -48,22 +46,16 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
       {
         id: 1,
         actionForPages: PageActionOnGroupDelete.publicize,
-        iconClass: 'icon-people',
-        styleClass: '',
         label: t('admin:user_group_management.delete_modal.publish_pages'),
       },
       {
         id: 2,
         actionForPages: PageActionOnGroupDelete.delete,
-        iconClass: 'icon-trash',
-        styleClass: 'text-danger',
         label: t('admin:user_group_management.delete_modal.delete_pages'),
       },
       {
         id: 3,
         actionForPages: PageActionOnGroupDelete.transfer,
-        iconClass: 'icon-options',
-        styleClass: '',
         label: t('admin:user_group_management.delete_modal.transfer_pages'),
       },
     ];
@@ -122,6 +114,8 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
       return <option key={opt.id} value={opt.actionForPages}>{opt.label}</option>;
     });
 
+    // TODO: Use GROWI original dropdown.
+    // refs: https://redmine.weseek.co.jp/issues/142614
     return (
       <select
         name="actionName"

+ 1 - 1
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -77,7 +77,7 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
             <td></td>
             <td className="text-center">
               <button className="btn btn-outline-secondary" type="button" onClick={props.onClickPlusBtn}>
-                <i className="ti ti-plus"></i>
+                <span className="material-symbols-outlined">add</span>
               </button>
             </td>
             <td></td>

+ 1 - 1
apps/app/src/components/Admin/Users/PasswordResetModal.jsx

@@ -126,7 +126,7 @@ class PasswordResetModal extends React.Component {
           </code>
           <CopyToClipboard text={temporaryPassword} onCopy={() => this.setState({ showTooltip: true })}>
             <button id="copy-tooltip" type="button" className="btn btn-outline-secondary border-0">
-              <i className="fa fa-clone" aria-hidden="true"></i>
+              <span className="material-symbols-outlined" aria-hidden="true">content_copy</span>
             </button>
           </CopyToClipboard>
           <Tooltip

+ 9 - 0
apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss

@@ -20,6 +20,15 @@
       font-size: 1.75rem !important;
     }
   }
+  // avoid sticky-top nav to turnate page path
+  .is-collapse-with-top {
+    @include bs.media-breakpoint-down(md) {
+      max-width: calc(100% - 350px);
+    }
+    @include bs.media-breakpoint-up(md) {
+      max-width: calc(100% - 500px);
+    }
+  }
 }
 
 .grw-page-path-nav :global {

+ 4 - 4
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -85,7 +85,7 @@ export const PagePathNav: FC<Props> = (props: Props) => {
     <div>
       <span className={formerLinkClassName}>{formerLink}</span>
       <div className="d-flex align-items-center">
-        <h1 className={`m-0 text-truncate ${latterLinkClassName}`}>
+        <h1 className={`m-0 ${latterLinkClassName}`}>
           {latterLink}
         </h1>
         { pageId != null && !isNotFound && (
@@ -94,7 +94,7 @@ export const PagePathNav: FC<Props> = (props: Props) => {
               <span className="badge rounded-pill text-bg-secondary ms-1 me-1">WIP</span>
             )}
             <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
-              <i className="ti ti-clipboard"></i>
+              <span className="material-symbols-outlined">content_paste</span>
             </CopyDropdown>
           </div>
         ) }
@@ -117,8 +117,8 @@ export const PagePathNavSticky = (props: PagePathNavStickyProps): JSX.Element =>
           // Controlling pointer-events
           //  2. enable pointer-events with 'pe-auto' only against the children
           //      which width is minimized by 'd-inline-block'
-          <div className="d-inline-block pe-auto">
-            <PagePathNav {...props} isCollapseParents={isCollapseParents} latterLinkClassName={isCollapseParents ? 'fs-3' : 'fs-2'} />
+          <div className={`d-inline-block pe-auto ${isCollapseParents ? 'is-collapse-with-top' : ''}`}>
+            <PagePathNav {...props} isCollapseParents={isCollapseParents} latterLinkClassName={isCollapseParents ? 'fs-3  text-truncate' : 'fs-2'} />
           </div>
         );
       }}

+ 3 - 2
apps/app/src/components/InAppNotification/PageNotification/ModelNotification.tsx

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 import type { HasObjectId } from '@growi/core';
 import { PagePathLabel } from '@growi/ui/dist/components';
@@ -26,7 +27,7 @@ export const ModelNotification: FC<Props> = (props) => {
         {actionMsg}
         <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
       </div>
-      <i className={`${actionIcon} me-2`} />
+      <span className="material-symbols-outlined me-2">{actionIcon}</span>
       <FormattedDistanceDate
         id={notification._id}
         date={notification.createdAt}

+ 14 - 14
apps/app/src/components/InAppNotification/PageNotification/useActionAndMsg.ts

@@ -16,59 +16,59 @@ export const useActionMsgAndIconForModelNotification = (notification: IInAppNoti
   switch (actionType) {
     case SupportedAction.ACTION_PAGE_LIKE:
       actionMsg = 'liked';
-      actionIcon = 'icon-like';
+      actionIcon = 'favorite';
       break;
     case SupportedAction.ACTION_PAGE_BOOKMARK:
       actionMsg = 'bookmarked on';
-      actionIcon = 'icon-star';
+      actionIcon = 'bookmark_add';
       break;
     case SupportedAction.ACTION_PAGE_UPDATE:
       actionMsg = 'updated on';
-      actionIcon = 'ti ti-agenda';
+      actionIcon = 'update';
       break;
     case SupportedAction.ACTION_PAGE_RENAME:
       actionMsg = 'renamed';
-      actionIcon = 'icon-action-redo';
+      actionIcon = 'redo';
       break;
     case SupportedAction.ACTION_PAGE_DUPLICATE:
       actionMsg = 'duplicated';
-      actionIcon = 'icon-docs';
+      actionIcon = 'file_copy';
       break;
     case SupportedAction.ACTION_PAGE_DELETE:
       actionMsg = 'deleted';
-      actionIcon = 'icon-trash';
+      actionIcon = 'delete';
       break;
     case SupportedAction.ACTION_PAGE_DELETE_COMPLETELY:
       actionMsg = 'completely deleted';
-      actionIcon = 'icon-fire';
+      actionIcon = 'delete_forever';
       break;
     case SupportedAction.ACTION_PAGE_REVERT:
       actionMsg = 'reverted';
-      actionIcon = 'icon-action-undo';
+      actionIcon = 'undo';
       break;
     case SupportedAction.ACTION_PAGE_RECURSIVELY_RENAME:
       actionMsg = 'renamed under';
-      actionIcon = 'icon-action-redo';
+      actionIcon = 'redo';
       break;
     case SupportedAction.ACTION_PAGE_RECURSIVELY_DELETE:
       actionMsg = 'deleted under';
-      actionIcon = 'icon-trash';
+      actionIcon = 'delete_forever';
       break;
     case SupportedAction.ACTION_PAGE_RECURSIVELY_DELETE_COMPLETELY:
       actionMsg = 'deleted completely under';
-      actionIcon = 'icon-fire';
+      actionIcon = 'delete_forever';
       break;
     case SupportedAction.ACTION_PAGE_RECURSIVELY_REVERT:
       actionMsg = 'reverted under';
-      actionIcon = 'icon-action-undo';
+      actionIcon = 'undo';
       break;
     case SupportedAction.ACTION_COMMENT_CREATE:
       actionMsg = 'commented on';
-      actionIcon = 'icon-bubble';
+      actionIcon = 'comment';
       break;
     case SupportedAction.ACTION_USER_REGISTRATION_APPROVAL_REQUEST:
       actionMsg = 'requested registration approval';
-      actionIcon = 'icon-bubble';
+      actionIcon = 'add_comment';
       break;
     default:
       actionMsg = '';

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

@@ -208,7 +208,7 @@ const InstallerForm = memo((): JSX.Element => {
                 {isLoading ? (
                   <LoadingSpinner />
                 ) : (
-                  <i className="icon-user-follow" />
+                  <span className="material-symbols-outlined">person_add</span>
                 )}
               </span>
               <span className="btn-label-text">{ t('Create') }</span>

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

@@ -147,7 +147,7 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
               {isLoading ? (
                 <LoadingSpinner />
               ) : (
-                <i className="icon-user-follow" />
+                <span className="material-symbols-outlined">person_add</span>
               )}
             </span>
             <span className="btn-label-text">{t('Create')}</span>

+ 1 - 1
apps/app/src/components/Me/DisassociateModal.tsx

@@ -57,7 +57,7 @@ const DisassociateModal = (props: Props): JSX.Element => {
           { t('Cancel') }
         </button>
         <button type="button" className="btn btn-sm btn-danger" onClick={disassociateAccountHandler}>
-          <i className="ti ti-unlink"></i>
+          <span className="material-symbols-outlined">link_off</span>
           { t('Disassociate') }
         </button>
       </ModalFooter>

+ 1 - 1
apps/app/src/components/Me/ExternalAccountRow.jsx

@@ -22,7 +22,7 @@ const ExternalAccountRow = (props) => {
           className="btn btn-sm btn-danger"
           onClick={() => props.openDisassociateModal(account)}
         >
-          <i className="ti ti-unlink"></i>
+          <span className="material-symbols-outlined">link_off</span>
           { t('Disassociate') }
         </button>
       </td>

+ 0 - 2
apps/app/src/components/Navbar/PageEditorModeManager.module.scss

@@ -14,8 +14,6 @@
       width: 70px;
       height: 30px;
     }
-
-    @include mixins.border-vertical('before', 70%, 1, true);
   }
 }
 

+ 1 - 1
apps/app/src/components/PageAlert/PageRedirectedAlert.tsx

@@ -45,7 +45,7 @@ export const PageRedirectedAlert = React.memo((): JSX.Element => {
         <strong>{ t('Redirected') }:</strong> { t('page_page.notice.redirected')} <code>{redirectFrom}</code> {t('page_page.notice.redirected_period')}
       </span>
       <button type="button" id="unlink-page-button" onClick={unlinkButtonClickHandler} className="btn btn-outline-dark btn-sm float-end">
-        <i className="ti ti-unlink" aria-hidden="true"></i>
+        <span className="material-symbols-outlined" aria-hidden="true">link_off</span>
         {t('unlink_redirection')}
       </button>
     </div>

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

@@ -13,6 +13,7 @@ import { toastSuccess, toastError } from '~/client/util/toastr';
 import { useDeleteAttachmentModal } from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 
+import { LoadingSpinner } from '../LoadingSpinner';
 import { Username } from '../User/Username';
 
 import styles from './DeleteAttachmentModal.module.scss';
@@ -85,7 +86,7 @@ export const DeleteAttachmentModal: React.FC = () => {
 
   const deletingIndicator = useMemo(() => {
     if (deleting) {
-      return <div className="speeding-wheel-sm"></div>;
+      return <LoadingSpinner />;
     }
     if (deleteError) {
       return <span>{deleteError}</span>;

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

@@ -22,7 +22,7 @@ export const CommentControl = (props: CommentControlProps): JSX.Element => {
         className="btn btn-link p-2 me-2"
         onClick={onClickDeleteBtn}
       >
-        <i className="ti ti-close"></i>
+        <span className="material-symbols-outlined">close</span>
       </button>
     </div>
   );

+ 3 - 2
apps/app/src/components/PageDeleteModal.tsx

@@ -1,5 +1,6 @@
+import type { FC } from 'react';
 import React, {
-  useState, FC, useMemo, useEffect,
+  useState, useMemo, useEffect,
 } from 'react';
 
 import type {
@@ -289,7 +290,7 @@ const PageDeleteModal: FC = () => {
           onClick={deleteButtonHandler}
           data-testid="delete-page-button"
         >
-          <i className={`me-1 icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
+          <span className="material-symbols-outlined me-1" aria-hidden="true">{deleteIconAndKey[deleteMode].icon}</span>
           { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
         </button>
       </>

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

@@ -475,13 +475,13 @@ export const HandsontableModal = (): JSX.Element => {
           </button>
           <div role="group" className="btn-group">
             <button type="button" className="btn btn-secondary" onClick={() => { alignButtonHandler('l') }}>
-              <i className="ti ti-align-left"></i>
+              <span className="material-symbols-outlined">format_align_left</span>
             </button>
             <button type="button" className="btn btn-secondary" onClick={() => { alignButtonHandler('c') }}>
-              <i className="ti ti-align-center"></i>
+              <span className="material-symbols-outlined">format_align_center</span>
             </button>
             <button type="button" className="btn btn-secondary" onClick={() => { alignButtonHandler('r') }}>
-              <i className="ti ti-align-right"></i>
+              <span className="material-symbols-outlined">format_align_right</span>
             </button>
           </div>
           <Collapse isOpen={isDataImportAreaExpanded}>

+ 45 - 85
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -3,11 +3,11 @@ import React, {
   useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
 } from 'react';
 
+
 import type EventEmitter from 'events';
 import nodePath from 'path';
 
 import { type IPageHasId, Origin } from '@growi/core';
-import { useGlobalSocket } from '@growi/core/dist/swr';
 import { pathUtils } from '@growi/core/dist/utils';
 import {
   CodeMirrorEditorMain, GlobalCodeMirrorEditorKey,
@@ -20,10 +20,9 @@ import { throttle, debounce } from 'throttle-debounce';
 
 import { useShouldExpandContent } from '~/client/services/layout';
 import { useUpdateStateAfterSave } from '~/client/services/page-operation';
-import { updatePage } from '~/client/services/update-page';
+import { updatePage, extractRemoteRevisionDataFromErrorObj } from '~/client/services/update-page';
 import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
-import { toastError, toastSuccess } from '~/client/util/toastr';
-import { SocketEventName } from '~/interfaces/websocket';
+import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import {
   useDefaultIndentSize, useCurrentUser,
   useCurrentPathname, useIsEnabledAttachTitleHeader,
@@ -32,22 +31,14 @@ import {
 } from '~/stores/context';
 import {
   useEditorSettings,
-  useCurrentIndentSize, usePageTagsForEditors,
-  useIsConflict,
+  useCurrentIndentSize,
   useEditingMarkdown,
   useWaitingSaveProcessing,
 } from '~/stores/editor';
-import { useConflictDiffModal } from '~/stores/modal';
 import {
-  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound, useIsLatestRevision, useTemplateBodyData,
+  useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId, useIsNotFound, useTemplateBodyData,
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
-import {
-  useRemoteRevisionId,
-  useRemoteRevisionBody,
-  useRemoteRevisionLastUpdatedAt,
-  useRemoteRevisionLastUpdateUser,
-} from '~/stores/remote-latest-page';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
   EditorMode,
@@ -61,6 +52,7 @@ import { EditorNavbar } from './EditorNavbar';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import Preview from './Preview';
 import { scrollEditor, scrollPreview } from './ScrollSyncHelper';
+import { useConflictResolver, useConflictEffect, type ConflictHandler } from './conflict';
 
 import '@growi/editor/dist/style.css';
 
@@ -77,6 +69,17 @@ declare global {
 let isOriginOfScrollSyncEditor = false;
 let isOriginOfScrollSyncPreview = false;
 
+export type SaveOptions = {
+  slackChannels: string,
+  overwriteScopesOfDescendants?: boolean
+}
+export type Save = (
+  revisionId?: string,
+  requestMarkdown?: string,
+  opts?: SaveOptions,
+  onConflict?: ConflictHandler
+) => Promise<IPageHasId | null>
+
 type Props = {
   visibility?: boolean,
 }
@@ -93,11 +96,8 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPage } = useSWRxCurrentPage();
-  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { data: grantData } = useSelectedGrant();
-  const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
-  const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
-  const { data: editingMarkdown, mutate: mutateEditingMarkdown } = useEditingMarkdown();
+  const { data: editingMarkdown } = useEditingMarkdown();
   const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { data: templateBodyData } = useTemplateBodyData();
   const { data: isEditable } = useIsEditable();
@@ -107,20 +107,12 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { data: defaultIndentSize } = useDefaultIndentSize();
   const { data: acceptedUploadFileType } = useAcceptedUploadFileType();
-  const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
   const { data: editorSettings } = useEditorSettings();
-  const { mutate: mutateIsLatestRevision } = useIsLatestRevision();
-  const { mutate: mutateRemotePageId } = useRemoteRevisionId();
-  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionBody();
-  const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
-  const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
   const { data: user } = useCurrentUser();
   const { onEditorsUpdated } = useEditingUsers();
-
-  const { data: socket } = useGlobalSocket();
+  const onConflict = useConflictResolver();
 
   const { data: rendererOptions } = usePreviewOptions();
-  const { mutate: mutateIsConflict } = useIsConflict();
 
   const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor();
 
@@ -128,10 +120,13 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   const updateStateAfterSave = useUpdateStateAfterSave(pageId, { supressEditingMarkdownMutation: true });
 
+  useConflictEffect();
+
   const { resolvedTheme } = useNextThemes();
   mutateResolvedTheme({ themeData: resolvedTheme });
 
   const currentRevisionId = currentPage?.revision?._id;
+  const isRevisionIdRequiredForPageUpdate = currentPage?.revision?.origin === undefined;
 
   const initialValueRef = useRef('');
   const initialValue = useMemo(() => {
@@ -168,43 +163,21 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
-
-  const checkIsConflict = useCallback((data) => {
-    const { s2cMessagePageUpdated } = data;
-
-    const isConflict = markdownToPreview !== s2cMessagePageUpdated.revisionBody;
-
-    mutateIsConflict(isConflict);
-
-  }, [markdownToPreview, mutateIsConflict]);
-
-  useEffect(() => {
-    if (socket == null) { return }
-
-    socket.on(SocketEventName.PageUpdated, checkIsConflict);
-
-    return () => {
-      socket.off(SocketEventName.PageUpdated, checkIsConflict);
-    };
-
-  }, [socket, checkIsConflict]);
-
-  const save = useCallback(async(opts?: {slackChannels: string, overwriteScopesOfDescendants?: boolean}): Promise<IPageHasId | null> => {
-    if (pageId == null || currentRevisionId == null || grantData == null) {
+  const save: Save = useCallback(async(revisionId, markdown, opts, onConflict) => {
+    if (pageId == null || grantData == null) {
       logger.error('Some materials to save are invalid', {
-        pageId, currentRevisionId, grantData,
+        pageId, grantData,
       });
       throw new Error('Some materials to save are invalid');
     }
 
     try {
       mutateWaitingSaveProcessing(true);
-      const isRevisionIdRequiredForPageUpdate = currentPage?.revision?.origin === undefined;
 
       const { page } = await updatePage({
         pageId,
-        revisionId: isRevisionIdRequiredForPageUpdate ? currentRevisionId : undefined,
-        body: codeMirrorEditor?.getDoc() ?? '',
+        revisionId,
+        body: markdown ?? '',
         grant: grantData?.grant,
         origin: Origin.Editor,
         userRelatedGrantUserGroupIds: grantData?.userRelatedGrantedGroups?.map((group) => {
@@ -220,41 +193,45 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     }
     catch (error) {
       logger.error('failed to save', error);
-      toastError(error);
-      if (error.code === 'conflict') {
-        mutateRemotePageId(error.data.revisionId);
-        mutateRemoteRevisionId(error.data.revisionBody);
-        mutateRemoteRevisionLastUpdatedAt(error.data.createdAt);
-        mutateRemoteRevisionLastUpdateUser(error.data.user);
+
+      const remoteRevisionData = extractRemoteRevisionDataFromErrorObj(error);
+      if (remoteRevisionData != null) {
+        onConflict?.(remoteRevisionData, markdown ?? '', save, opts);
+        toastWarning(t('modal_resolve_conflict.conflicts_with_new_body_on_server_side'));
+        return null;
       }
+
+      toastError(error);
       return null;
     }
     finally {
       mutateWaitingSaveProcessing(false);
     }
+  }, [pageId, grantData, mutateWaitingSaveProcessing, t]);
 
-  // eslint-disable-next-line max-len
-  }, [codeMirrorEditor, grantData, pageId, currentRevisionId, mutateWaitingSaveProcessing, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser]);
-
-  const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
-    const page = await save(opts);
+  const saveAndReturnToViewHandler = useCallback(async(opts: SaveOptions) => {
+    const markdown = codeMirrorEditor?.getDoc();
+    const revisionId = isRevisionIdRequiredForPageUpdate ? currentRevisionId : undefined;
+    const page = await save(revisionId, markdown, opts, onConflict);
     if (page == null) {
       return;
     }
 
     mutateEditorMode(EditorMode.View);
     updateStateAfterSave?.();
-  }, [mutateEditorMode, save, updateStateAfterSave]);
+  }, [codeMirrorEditor, currentRevisionId, isRevisionIdRequiredForPageUpdate, mutateEditorMode, onConflict, save, updateStateAfterSave]);
 
   const saveWithShortcut = useCallback(async() => {
-    const page = await save();
+    const markdown = codeMirrorEditor?.getDoc();
+    const revisionId = isRevisionIdRequiredForPageUpdate ? currentRevisionId : undefined;
+    const page = await save(revisionId, markdown, undefined, onConflict);
     if (page == null) {
       return;
     }
 
     toastSuccess(t('toaster.save_succeeded'));
     updateStateAfterSave?.();
-  }, [save, t, updateStateAfterSave]);
+  }, [codeMirrorEditor, currentRevisionId, isRevisionIdRequiredForPageUpdate, onConflict, save, t, updateStateAfterSave]);
 
 
   // the upload event handler
@@ -327,23 +304,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   const scrollPreviewHandlerThrottle = useMemo(() => throttle(25, scrollPreviewHandler), [scrollPreviewHandler]);
 
-  const afterResolvedHandler = useCallback(async() => {
-    // get page data from db
-    const pageData = await mutateCurrentPage();
-
-    // update tag
-    await mutateTagsInfo(); // get from DB
-    syncTagsInfoForEditor(); // sync global state for client
-
-    // clear isConflict
-    mutateIsConflict(false);
-
-    // set resolved markdown in editing markdown
-    const markdown = pageData?.revision?.body ?? '';
-    mutateEditingMarkdown(markdown);
-
-  }, [mutateCurrentPage, mutateEditingMarkdown, mutateIsConflict, mutateTagsInfo, syncTagsInfoForEditor]);
-
   // initial caret line
   useEffect(() => {
     codeMirrorEditor?.setCaretLine();

+ 135 - 0
apps/app/src/components/PageEditor/conflict.tsx

@@ -0,0 +1,135 @@
+import { useCallback, useEffect } from 'react';
+
+import { Origin } from '@growi/core';
+import { useGlobalSocket } from '@growi/core/dist/swr';
+import { GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated } from '@growi/editor';
+import { useTranslation } from 'react-i18next';
+
+import { useUpdateStateAfterSave } from '~/client/services/page-operation';
+import { toastSuccess } from '~/client/util/toastr';
+import type { Save, SaveOptions } from '~/components/PageEditor/PageEditor';
+import { SocketEventName } from '~/interfaces/websocket';
+import { usePageStatusAlert } from '~/stores/alert';
+import { useConflictDiffModal } from '~/stores/modal';
+import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
+import { type RemoteRevisionData, useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
+import { EditorMode, useEditorMode } from '~/stores/ui';
+
+
+export type ConflictHandler = (
+  remoteRevisionData: RemoteRevisionData,
+  requestMarkdown: string,
+  save: Save,
+  saveOptions?: SaveOptions,
+) => void;
+
+type GenerateResolveConflicthandler = () => (
+  revisionId: string,
+  save: Save,
+  saveOptions?: SaveOptions,
+  onConflict?: () => void
+) => (newMarkdown: string) => Promise<void>
+
+const useGenerateResolveConflictHandler: GenerateResolveConflicthandler = () => {
+  const { t } = useTranslation();
+
+  const { data: pageId } = useCurrentPageId();
+  const { close: closePageStatusAlert } = usePageStatusAlert();
+  const { close: closeConflictDiffModal } = useConflictDiffModal();
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId, { supressEditingMarkdownMutation: true });
+
+  return useCallback((revisionId, save, saveOptions, onConflict) => {
+    return async(newMarkdown) => {
+      const page = await save(revisionId, newMarkdown, saveOptions, onConflict);
+      if (page == null) {
+        return;
+      }
+
+      // Reflect conflict resolution results in CodeMirrorEditor
+      codeMirrorEditor?.initDoc(newMarkdown);
+
+      closePageStatusAlert();
+      closeConflictDiffModal();
+
+      toastSuccess(t('toaster.save_succeeded'));
+      updateStateAfterSave?.();
+    };
+  }, [closeConflictDiffModal, closePageStatusAlert, codeMirrorEditor, t, updateStateAfterSave]);
+};
+
+
+type ConflictResolver = () => ConflictHandler;
+
+export const useConflictResolver: ConflictResolver = () => {
+  const { open: openPageStatusAlert } = usePageStatusAlert();
+  const { open: openConflictDiffModal } = useConflictDiffModal();
+  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+  const generateResolveConflictHandler = useGenerateResolveConflictHandler();
+
+  return useCallback((remoteRevidsionData, requestMarkdown, save, saveOptions) => {
+    const conflictHandler = () => {
+      const resolveConflictHandler = generateResolveConflictHandler(remoteRevidsionData.remoteRevisionId, save, saveOptions, conflictHandler);
+      openPageStatusAlert({ onResolveConflict: () => openConflictDiffModal(requestMarkdown, resolveConflictHandler) });
+      setRemoteLatestPageData(remoteRevidsionData);
+    };
+
+    conflictHandler();
+  }, [generateResolveConflictHandler, openConflictDiffModal, openPageStatusAlert, setRemoteLatestPageData]);
+};
+
+export const useConflictEffect = (): void => {
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { close: closePageStatusAlert } = usePageStatusAlert();
+  const { close: closeConflictDiffModal } = useConflictDiffModal();
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
+  const { open: openPageStatusAlert } = usePageStatusAlert();
+  const { open: openConflictDiffModal } = useConflictDiffModal();
+  const { data: socket } = useGlobalSocket();
+  const { data: editorMode } = useEditorMode();
+
+  const conflictHandler = useCallback(() => {
+    const onResolveConflict = () => {
+      const resolveConflictHandler = (newMarkdown: string) => {
+        codeMirrorEditor?.initDoc(newMarkdown);
+        closeConflictDiffModal();
+        closePageStatusAlert();
+      };
+
+      const markdown = codeMirrorEditor?.getDoc();
+      openConflictDiffModal(markdown ?? '', resolveConflictHandler);
+    };
+
+    openPageStatusAlert({ onResolveConflict });
+  }, [closeConflictDiffModal, closePageStatusAlert, codeMirrorEditor, openConflictDiffModal, openPageStatusAlert]);
+
+  const updateRemotePageDataHandler = useCallback((data) => {
+    const { s2cMessagePageUpdated } = data;
+
+    const remoteRevisionId = s2cMessagePageUpdated.revisionId;
+    const remoteRevisionOrigin = s2cMessagePageUpdated.revisionOrigin;
+    const currentRevisionId = currentPage?.revision?._id;
+    const isRevisionOutdated = (currentRevisionId != null || remoteRevisionId != null) && currentRevisionId !== remoteRevisionId;
+
+    // !!CAUTION!! Timing of calling openPageStatusAlert may clash with client/services/side-effects/page-updated.ts
+    if (isRevisionOutdated && editorMode === EditorMode.Editor && (remoteRevisionOrigin === Origin.View || remoteRevisionOrigin === undefined)) {
+      conflictHandler();
+    }
+
+    // Clear cache
+    if (!isRevisionOutdated) {
+      closePageStatusAlert();
+    }
+  }, [closePageStatusAlert, currentPage?.revision?._id, editorMode, conflictHandler]);
+
+  useEffect(() => {
+    if (socket == null) { return }
+
+    socket.on(SocketEventName.PageUpdated, updateRemotePageDataHandler);
+
+    return () => {
+      socket.off(SocketEventName.PageUpdated, updateRemotePageDataHandler);
+    };
+
+  }, [socket, updateRemotePageDataHandler]);
+};

+ 37 - 98
apps/app/src/components/PageStatusAlert.tsx

@@ -1,133 +1,72 @@
-import React, { useCallback, useMemo } from 'react';
+import React, { useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
-import * as ReactDOMServer from 'react-dom/server';
 
+import { usePageStatusAlert } from '~/stores/alert';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
-import { useEditingMarkdown, useIsConflict } from '~/stores/editor';
-import { useConflictDiffModal } from '~/stores/modal';
-import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
+import { useSWRxCurrentPage } from '~/stores/page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
+import { useEditorMode } from '~/stores/ui';
 
 import { Username } from './User/Username';
 
 import styles from './PageStatusAlert.module.scss';
 
-type AlertComponentContents = {
-  additionalClasses: string[],
-  label: JSX.Element,
-  btn: JSX.Element
-}
-
 export const PageStatusAlert = (): JSX.Element => {
-
   const { t } = useTranslation();
-  const { data: isConflict } = useIsConflict();
-  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
-  const { open: openConflictDiffModal } = useConflictDiffModal();
+
+  const { data: editorMode } = useEditorMode();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
-
-  // store remote latest page data
+  const { data: pageStatusAlertData } = usePageStatusAlert();
   const { data: remoteRevisionId } = useRemoteRevisionId();
   const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
-
   const { data: pageData } = useSWRxCurrentPage();
-  const { trigger: mutatePageData } = useSWRMUTxCurrentPage();
-  const revision = pageData?.revision;
 
-  const refreshPage = useCallback(async() => {
-    const updatedPageData = await mutatePageData();
-    mutateEditingMarkdown(updatedPageData?.revision?.body);
-  }, [mutateEditingMarkdown, mutatePageData]);
+  const onClickRefreshPage = useCallback(() => {
+    pageStatusAlertData?.onRefleshPage?.();
+  }, [pageStatusAlertData]);
 
   const onClickResolveConflict = useCallback(() => {
-    // openConflictDiffModal();
-  }, [openConflictDiffModal]);
-
-  // TODO: re-impl for builtin editor
-  //
-  // const getContentsForSomeoneEditingAlert = useCallback((): AlertComponentContents => {
-  //   return {
-  //     additionalClasses: ['bg-success', 'd-hackmd-none'],
-  //     label:
-  // <>
-  //   <span className="material-symbols-outlined">person</span>
-  //   {t('hackmd.someone_editing')}
-  // </>,
-  //     btn:
-  // <a href="#hackmd" key="btnOpenHackmdSomeoneEditing" className="btn btn-outline-white">
-  //   <span class="material-symbols-outlined">description</span>
-  //   Open HackMD Editor
-  // </a>,
-  //   };
-  // }, [t]);
-
-  const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
+    pageStatusAlertData?.onResolveConflict?.();
+  }, [pageStatusAlertData]);
 
-    const usernameComponentToString = ReactDOMServer.renderToString(<Username user={remoteRevisionLastUpdateUser} />);
+  const hasResolveConflictHandler = pageStatusAlertData?.onResolveConflict != null;
+  const hasRefreshPageHandler = pageStatusAlertData?.onRefleshPage != null;
 
-    const label1 = isConflict
-      ? t('modal_resolve_conflict.file_conflicting_with_newer_remote')
-      // eslint-disable-next-line react/no-danger
-      : <span dangerouslySetInnerHTML={{ __html: `${usernameComponentToString} ${t('edited this page')}` }} />;
+  const currentRevisionId = pageData?.revision?._id;
+  const isRevisionOutdated = (currentRevisionId != null || remoteRevisionId != null) && currentRevisionId !== remoteRevisionId;
 
-    return {
-      additionalClasses: ['bg-warning text-dark'],
-      label:
-  <>
-    <span className="material-symbols-outlined">lightbulb</span>
-    {label1}
-  </>,
-      btn:
-  <>
-    <button type="button" onClick={() => refreshPage()} className="btn btn-outline-white me-4">
-      <span className="material-symbols-outlined">refresh</span>
-      {t('Load latest')}
-    </button>
-    { isConflict && (
-      <button
-        type="button"
-        onClick={onClickResolveConflict}
-        className="btn btn-outline-white"
-      >
-        <span className="material-symbols-outlined">description</span>
-        {t('modal_resolve_conflict.resolve_conflict')}
-      </button>
-    )}
-  </>,
-    };
-  }, [remoteRevisionLastUpdateUser, isConflict, t, onClickResolveConflict, refreshPage]);
+  if (!pageStatusAlertData?.isOpen || !!isGuestUser || !!isReadOnlyUser || !isRevisionOutdated) {
+    return <></>;
+  }
 
-  const alertComponentContents = useMemo(() => {
-    const isRevisionOutdated = revision?._id !== remoteRevisionId;
-
-    // 'revision?._id' and 'remoteRevisionId' are can not be undefined
-    if (revision?._id == null || remoteRevisionId == null) { return }
-
-    // when remote revision is newer than both
-    if (isRevisionOutdated) {
-      return getContentsForUpdatedAlert();
-    }
-
-    return null;
-  }, [revision?._id, remoteRevisionId, getContentsForUpdatedAlert]);
-
-  if (!!isGuestUser || !!isReadOnlyUser || alertComponentContents == null) { return <></> }
-
-  const { additionalClasses, label, btn } = alertComponentContents;
+  if (editorMode === pageStatusAlertData?.hideEditorMode) {
+    return <></>;
+  }
 
   return (
-    <div className={`${styles['grw-page-status-alert']} card text-white fixed-bottom animated fadeInUp faster ${additionalClasses.join(' ')}`}>
+    <div className={`${styles['grw-page-status-alert']} card fixed-bottom animated fadeInUp faster text-bg-warning`}>
       <div className="card-body">
         <p className="card-text grw-card-label-container">
-          {label}
+          { hasResolveConflictHandler
+            ? <>{t('modal_resolve_conflict.file_conflicting_with_newer_remote')}</>
+            : <><Username user={remoteRevisionLastUpdateUser} /> {t('edited this page')}</>
+          }
         </p>
         <p className="card-text grw-card-btn-container">
-          {btn}
+          {hasRefreshPageHandler && (
+            <button type="button" onClick={onClickRefreshPage} className="btn btn-outline-white">
+              <span className="material-symbols-outlined">refresh</span>{t('Load latest')}
+            </button>
+          )}
+          {hasResolveConflictHandler && (
+            <button type="button" onClick={onClickResolveConflict} className="btn btn-outline-white">
+              <span className="material-symbols-outlined">description</span>{t('modal_resolve_conflict.resolve_conflict')}
+            </button>
+          )}
         </p>
       </div>
     </div>
   );
-
 };

+ 4 - 1
apps/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx

@@ -14,6 +14,8 @@ import {
 } from '~/stores/context';
 
 import '@growi/remark-drawio/dist/style.css';
+import { useIsRevisionOutdated } from '~/stores/page';
+
 import styles from './DrawioViewerWithEditButton.module.scss';
 
 
@@ -32,6 +34,7 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
+  const { data: isRevisionOutdated } = useIsRevisionOutdated();
 
   const [isRendered, setRendered] = useState(false);
   const [mxfile, setMxfile] = useState('');
@@ -55,7 +58,7 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
     }
   }, []);
 
-  const showEditButton = isRendered && !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
+  const showEditButton = !isRevisionOutdated && isRendered && !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
 
   return (
     <div className={`drawio-viewer-with-edit-button ${styles['drawio-viewer-with-edit-button']}`}>

+ 25 - 0
apps/app/src/components/ReactMarkdownComponents/Header.module.scss

@@ -17,3 +17,28 @@
     opacity: 1 !important;
   }
 }
+
+// blink
+@keyframes fadeout {
+  100% {
+    opacity: 0;
+  }
+}
+
+.blink {
+  position: relative;
+  z-index: 1;
+
+  &::after {
+    position: absolute;
+    top: 15%;
+    left: 0;
+    z-index: -1;
+    width: 100%;
+    height: 70%;
+    content: '';
+    background-color: var(--bs-highlight-bg);
+    border-radius: 2px;
+    animation: fadeout 1s ease-in 1.5s forwards;
+  }
+}

+ 1 - 1
apps/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -113,7 +113,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
   const showEditButton = !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
 
   return (
-    <CustomTag id={id} className={`revision-head ${styles['revision-head']} ${isActive ? 'blink' : ''}`}>
+    <CustomTag id={id} className={`${styles['revision-head']} ${isActive ? styles.blink : ''}`}>
       {children}
       <NextLink href={`#${id}`} className="revision-head-link">
         <span className="material-symbols-outlined">link</span>

+ 3 - 1
apps/app/src/components/ReactMarkdownComponents/TableWithEditButton.tsx

@@ -7,6 +7,7 @@ import type { Element } from 'react-markdown/lib/rehype-filter';
 import {
   useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
 } from '~/stores/context';
+import { useIsRevisionOutdated } from '~/stores/page';
 
 import styles from './TableWithEditButton.module.scss';
 
@@ -29,6 +30,7 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
+  const { data: isRevisionOutdated } = useIsRevisionOutdated();
 
   const bol = node.position?.start.line;
   const eol = node.position?.end.line;
@@ -37,7 +39,7 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
     globalEmitter.emit('launchHandsonTableModal', bol, eol);
   }, [bol, eol]);
 
-  const showEditButton = !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
+  const showEditButton = !isRevisionOutdated && !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
 
   return (
     <div className={`editable-with-handsontable ${styles['editable-with-handsontable']}`}>

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

@@ -75,7 +75,7 @@ export const RevisionComparer = (props: RevisionComparerProps): JSX.Element => {
             caret
             className="d-block text-muted bg-transparent btn-copy border-0 py-0"
           >
-            <i className="ti ti-clipboard"></i>
+            <span className="material-symbols-outlined">content_paste</span>
           </DropdownToggle>
           <DropdownMenu strategy="fixed" end>
             {/* Page path URL */}

+ 1 - 1
apps/app/src/components/SearchPage/SearchResultContent.tsx

@@ -219,7 +219,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       <RightComponent />
 
       <div className="container-lg grw-container-convertible pt-2 pb-2">
-        <PagePathNav pageId={page._id} pagePath={page.path} formerLinkClassName="small" latterLinkClassName="fs-3" />
+        <PagePathNav pageId={page._id} pagePath={page.path} formerLinkClassName="small" latterLinkClassName="fs-3 text-truncate" />
       </div>
 
       <div

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

@@ -38,7 +38,7 @@ export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Elemen
         </div>
       </div>
       <div className="grw-user-page-list-m mt-5 d-edit-none">
-        <h2 id="recently-created-list" className="grw-user-page-header border-bottom pb-2 mb-3">
+        <h2 id="recently-created-list" className="grw-user-page-header border-bottom pb-2 mb-3 d-flex">
           <span className="growi-custom-icons me-1">recently_created</span>
           {t('footer.recently_created')}
         </h2>

+ 0 - 15
apps/app/src/interfaces/interceptor-manager.ts

@@ -1,15 +0,0 @@
-interface BasicInterceptor {
-  getId: () => string,
-  isInterceptWhen: (contextName: string) => boolean,
-  isProcessableParallel: () => boolean,
-  process: (contextName: string, args: any) => Promise<any>
-}
-
-export interface IInterceptorManager {
-  interceptorAndOrders: {interceptor: BasicInterceptor, order: number}[],
-  interceptors: BasicInterceptor[],
-  addInterceptor: (inerceptor: BasicInterceptor, order: number) => void,
-  addInterceptors: (inerceptors: BasicInterceptor[], order: number) => void,
-  process: (contextName: string, args: any) => Promise<void>,
-  doProcess: (inerceptor: BasicInterceptor, contextName: string, args: any) => Promise<void>
-}

Некоторые файлы не были показаны из-за большого количества измененных файлов