Browse Source

Merge pull request #10270 from weseek/support/156162-170908-mermaid-search-plantuml-feature-biome

support: Configure biome for mermaid search plantuml features
Yuki Takei 7 months ago
parent
commit
820a9e2bc3

+ 3 - 0
apps/app/.eslintrc.js

@@ -33,6 +33,9 @@ module.exports = {
     'src/features/callout/**',
     'src/features/callout/**',
     'src/features/comment/**',
     'src/features/comment/**',
     'src/features/templates/**',
     'src/features/templates/**',
+    'src/features/mermaid/**',
+    'src/features/search/**',
+    'src/features/plantuml/**',
     'src/features/external-user-group/**',
     'src/features/external-user-group/**',
   ],
   ],
   settings: {
   settings: {

+ 42 - 44
apps/app/src/features/mermaid/components/MermaidViewer.tsx

@@ -1,6 +1,5 @@
-import React, { useRef, useEffect, type JSX } from 'react';
-
 import mermaid from 'mermaid';
 import mermaid from 'mermaid';
+import React, { type JSX, useEffect, useRef } from 'react';
 import { v7 as uuidV7 } from 'uuid';
 import { v7 as uuidV7 } from 'uuid';
 
 
 import { useNextThemes } from '~/stores-universal/use-next-themes';
 import { useNextThemes } from '~/stores-universal/use-next-themes';
@@ -9,48 +8,47 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:features:mermaid:MermaidViewer');
 const logger = loggerFactory('growi:features:mermaid:MermaidViewer');
 
 
 type MermaidViewerProps = {
 type MermaidViewerProps = {
-  value: string
-}
-
-export const MermaidViewer = React.memo((props: MermaidViewerProps): JSX.Element => {
-  const { value } = props;
-
-  const { isDarkMode } = useNextThemes();
-
-  const ref = useRef<HTMLDivElement>(null);
-
-  useEffect(() => {
-    (async() => {
-      if (ref.current != null && value != null) {
-        mermaid.initialize({
-          theme: isDarkMode ? 'dark' : undefined,
-        });
-        try {
-          // Attempting to render multiple Mermaid diagrams using `mermaid.run` can cause duplicate SVG IDs.
-          // This is because it uses `Date.now()` for ID generation.
-          // ID generation logic: https://github.com/mermaid-js/mermaid/blob/5b241bbb97f81d37df8a84da523dfa53ac13bfd1/packages/mermaid/src/utils.ts#L755-L764
-          // Related issue: https://github.com/mermaid-js/mermaid/issues/4650
-          // Instead of `mermaid.run`, we use `mermaid.render` which allows us to assign a unique ID.
-          const id = `mermaid-${uuidV7()}`;
-          const { svg } = await mermaid.render(id, value, ref.current);
-          ref.current.innerHTML = svg;
-        }
-        catch (err) {
-          logger.error(err);
+  value: string;
+};
+
+export const MermaidViewer = React.memo(
+  (props: MermaidViewerProps): JSX.Element => {
+    const { value } = props;
+
+    const { isDarkMode } = useNextThemes();
+
+    const ref = useRef<HTMLDivElement>(null);
+
+    useEffect(() => {
+      (async () => {
+        if (ref.current != null && value != null) {
+          mermaid.initialize({
+            theme: isDarkMode ? 'dark' : undefined,
+          });
+          try {
+            // Attempting to render multiple Mermaid diagrams using `mermaid.run` can cause duplicate SVG IDs.
+            // This is because it uses `Date.now()` for ID generation.
+            // ID generation logic: https://github.com/mermaid-js/mermaid/blob/5b241bbb97f81d37df8a84da523dfa53ac13bfd1/packages/mermaid/src/utils.ts#L755-L764
+            // Related issue: https://github.com/mermaid-js/mermaid/issues/4650
+            // Instead of `mermaid.run`, we use `mermaid.render` which allows us to assign a unique ID.
+            const id = `mermaid-${uuidV7()}`;
+            const { svg } = await mermaid.render(id, value, ref.current);
+            ref.current.innerHTML = svg;
+          } catch (err) {
+            logger.error(err);
+          }
         }
         }
-      }
-    })();
-  }, [isDarkMode, value]);
-
-  return (
-    value
-      ? (
-        <div ref={ref} key={value}>
-          {value}
-        </div>
-      )
-      : <div key={value}></div>
-  );
-});
+      })();
+    }, [isDarkMode, value]);
+
+    return value ? (
+      <div ref={ref} key={value}>
+        {value}
+      </div>
+    ) : (
+      <div key={value}></div>
+    );
+  },
+);
 
 
 MermaidViewer.displayName = 'MermaidViewer';
 MermaidViewer.displayName = 'MermaidViewer';

+ 10 - 9
apps/app/src/features/mermaid/services/mermaid.ts

@@ -5,21 +5,22 @@ import { visit } from 'unist-util-visit';
 
 
 function rewriteNode(node: Code) {
 function rewriteNode(node: Code) {
   // replace node
   // replace node
-  const data = node.data ?? (node.data = {});
+  if (node.data == null) {
+    node.data = {};
+  }
+  const data = node.data;
   data.hName = 'mermaid';
   data.hName = 'mermaid';
   data.hProperties = {
   data.hProperties = {
     value: node.value,
     value: node.value,
   };
   };
 }
 }
 
 
-export const remarkPlugin: Plugin = function() {
-  return (tree) => {
-    visit(tree, 'code', (node: Code) => {
-      if (node.lang === 'mermaid') {
-        rewriteNode(node);
-      }
-    });
-  };
+export const remarkPlugin: Plugin = () => (tree) => {
+  visit(tree, 'code', (node: Code) => {
+    if (node.lang === 'mermaid') {
+      rewriteNode(node);
+    }
+  });
 };
 };
 
 
 export const sanitizeOption: SanitizeOption = {
 export const sanitizeOption: SanitizeOption = {

+ 6 - 4
apps/app/src/features/plantuml/services/plantuml.ts

@@ -8,9 +8,9 @@ import carbonGrayDarkStyles from '../themes/carbon-gray-dark.puml';
 import carbonGrayLightStyles from '../themes/carbon-gray-light.puml';
 import carbonGrayLightStyles from '../themes/carbon-gray-light.puml';
 
 
 type PlantUMLPluginParams = {
 type PlantUMLPluginParams = {
-  plantumlUri: string,
-  isDarkMode?: boolean,
-}
+  plantumlUri: string;
+  isDarkMode?: boolean;
+};
 
 
 export const remarkPlugin: Plugin<[PlantUMLPluginParams]> = (options) => {
 export const remarkPlugin: Plugin<[PlantUMLPluginParams]> = (options) => {
   const { plantumlUri, isDarkMode } = options;
   const { plantumlUri, isDarkMode } = options;
@@ -21,7 +21,9 @@ export const remarkPlugin: Plugin<[PlantUMLPluginParams]> = (options) => {
   return (tree, file) => {
   return (tree, file) => {
     visit(tree, 'code', (node: Code) => {
     visit(tree, 'code', (node: Code) => {
       if (node.lang === 'plantuml') {
       if (node.lang === 'plantuml') {
-        const themeStyles = isDarkMode ? carbonGrayDarkStyles : carbonGrayLightStyles;
+        const themeStyles = isDarkMode
+          ? carbonGrayDarkStyles
+          : carbonGrayLightStyles;
         node.value = `${themeStyles}\n${node.value}`;
         node.value = `${themeStyles}\n${node.value}`;
       }
       }
     });
     });

+ 31 - 21
apps/app/src/features/search/client/components/SearchForm.tsx

@@ -1,37 +1,45 @@
 import React, {
 import React, {
-  useCallback, useRef, useEffect, useMemo, type JSX,
+  type JSX,
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
 } from 'react';
 } from 'react';
 
 
 import type { GetInputProps } from '../interfaces/downshift';
 import type { GetInputProps } from '../interfaces/downshift';
 
 
 type Props = {
 type Props = {
-  searchKeyword: string,
-  onChange?: (text: string) => void,
-  onSubmit?: () => void,
-  getInputProps: GetInputProps,
-}
+  searchKeyword: string;
+  onChange?: (text: string) => void;
+  onSubmit?: () => void;
+  getInputProps: GetInputProps;
+};
 
 
 export const SearchForm = (props: Props): JSX.Element => {
 export const SearchForm = (props: Props): JSX.Element => {
-  const {
-    searchKeyword, onChange, onSubmit, getInputProps,
-  } = props;
+  const { searchKeyword, onChange, onSubmit, getInputProps } = props;
 
 
   const inputRef = useRef<HTMLInputElement>(null);
   const inputRef = useRef<HTMLInputElement>(null);
 
 
-  const changeSearchTextHandler = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
-    onChange?.(e.target.value);
-  }, [onChange]);
+  const changeSearchTextHandler = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>) => {
+      onChange?.(e.target.value);
+    },
+    [onChange],
+  );
 
 
-  const submitHandler = useCallback((e: React.FormEvent<HTMLFormElement>) => {
-    e.preventDefault();
+  const submitHandler = useCallback(
+    (e: React.FormEvent<HTMLFormElement>) => {
+      e.preventDefault();
 
 
-    const isEmptyKeyword = searchKeyword.trim().length === 0;
-    if (isEmptyKeyword) {
-      return;
-    }
+      const isEmptyKeyword = searchKeyword.trim().length === 0;
+      if (isEmptyKeyword) {
+        return;
+      }
 
 
-    onSubmit?.();
-  }, [searchKeyword, onSubmit]);
+      onSubmit?.();
+    },
+    [searchKeyword, onSubmit],
+  );
 
 
   const inputOptions = useMemo(() => {
   const inputOptions = useMemo(() => {
     return getInputProps({
     return getInputProps({
@@ -60,7 +68,9 @@ export const SearchForm = (props: Props): JSX.Element => {
       <button
       <button
         type="button"
         type="button"
         className="btn btn-neutral-secondary text-muted position-absolute bottom-0 end-0 w-auto h-100 border-0"
         className="btn btn-neutral-secondary text-muted position-absolute bottom-0 end-0 w-auto h-100 border-0"
-        onClick={() => { onChange?.('') }}
+        onClick={() => {
+          onChange?.('');
+        }}
       >
       >
         <span className="material-symbols-outlined p-0">cancel</span>
         <span className="material-symbols-outlined p-0">cancel</span>
       </button>
       </button>

+ 72 - 20
apps/app/src/features/search/client/components/SearchHelp.tsx

@@ -1,6 +1,5 @@
-import React, { useState, type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import React, { type JSX, useState } from 'react';
 import { Collapse } from 'reactstrap';
 import { Collapse } from 'reactstrap';
 
 
 export const SearchHelp = (): JSX.Element => {
 export const SearchHelp = (): JSX.Element => {
@@ -10,47 +9,100 @@ export const SearchHelp = (): JSX.Element => {
 
 
   return (
   return (
     <>
     <>
-      <button type="button" className="btn border-0 text-muted d-flex justify-content-center align-items-center ms-1 p-0" onClick={() => setIsOpen(!isOpen)}>
+      <button
+        type="button"
+        className="btn border-0 text-muted d-flex justify-content-center align-items-center ms-1 p-0"
+        onClick={() => setIsOpen(!isOpen)}
+      >
         <span className="material-symbols-outlined me-2 p-0">help</span>
         <span className="material-symbols-outlined me-2 p-0">help</span>
         <span>{t('search_help.title')}</span>
         <span>{t('search_help.title')}</span>
-        <span className="material-symbols-outlined ms-2 p-0">{isOpen ? 'expand_less' : 'expand_more'}</span>
+        <span className="material-symbols-outlined ms-2 p-0">
+          {isOpen ? 'expand_less' : 'expand_more'}
+        </span>
       </button>
       </button>
       <Collapse isOpen={isOpen}>
       <Collapse isOpen={isOpen}>
         <table className="table table-borderless m-0">
         <table className="table table-borderless m-0">
           <tbody>
           <tbody>
             <tr className="border-bottom">
             <tr className="border-bottom">
               <th className="py-2">
               <th className="py-2">
-                <code>word1</code> <code>word2</code><br />
-                <small className="text-muted">({ t('search_help.and.syntax help') })</small>
+                <code>word1</code> <code>word2</code>
+                <br />
+                <small className="text-muted">
+                  ({t('search_help.and.syntax help')})
+                </small>
               </th>
               </th>
-              <td><h6 className="m-0 text-muted">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
+              <td>
+                <h6 className="m-0 text-muted">
+                  {t('search_help.and.desc', {
+                    word1: 'word1',
+                    word2: 'word2',
+                  })}
+                </h6>
+              </td>
             </tr>
             </tr>
             <tr className="border-bottom">
             <tr className="border-bottom">
               <th className="py-2">
               <th className="py-2">
-                <code>&quot;This is GROWI&quot;</code><br />
-                <small className="text-muted">({ t('search_help.phrase.syntax help') })</small>
+                <code>&quot;This is GROWI&quot;</code>
+                <br />
+                <small className="text-muted">
+                  ({t('search_help.phrase.syntax help')})
+                </small>
               </th>
               </th>
-              <td><h6 className="m-0 text-muted">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
+              <td>
+                <h6 className="m-0 text-muted">
+                  {t('search_help.phrase.desc', { phrase: 'This is GROWI' })}
+                </h6>
+              </td>
             </tr>
             </tr>
             <tr className="border-bottom">
             <tr className="border-bottom">
-              <th className="py-2"><code>-keyword</code></th>
-              <td><h6 className="m-0 text-muted">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
+              <th className="py-2">
+                <code>-keyword</code>
+              </th>
+              <td>
+                <h6 className="m-0 text-muted">
+                  {t('search_help.exclude.desc', { word: 'keyword' })}
+                </h6>
+              </td>
             </tr>
             </tr>
             <tr className="border-bottom">
             <tr className="border-bottom">
-              <th className="py-2"><code>prefix:/user/</code></th>
-              <td><h6 className="m-0 text-muted">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
+              <th className="py-2">
+                <code>prefix:/user/</code>
+              </th>
+              <td>
+                <h6 className="m-0 text-muted">
+                  {t('search_help.prefix.desc', { path: '/user/' })}
+                </h6>
+              </td>
             </tr>
             </tr>
             <tr className="border-bottom">
             <tr className="border-bottom">
-              <th className="py-2"><code>-prefix:/user/</code></th>
-              <td><h6 className="m-0 text-muted">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
+              <th className="py-2">
+                <code>-prefix:/user/</code>
+              </th>
+              <td>
+                <h6 className="m-0 text-muted">
+                  {t('search_help.exclude_prefix.desc', { path: '/user/' })}
+                </h6>
+              </td>
             </tr>
             </tr>
             <tr className="border-bottom">
             <tr className="border-bottom">
-              <th className="py-2"><code>tag:wiki</code></th>
-              <td><h6 className="m-0 text-muted">{ t('search_help.tag.desc', { tag: 'wiki' }) }</h6></td>
+              <th className="py-2">
+                <code>tag:wiki</code>
+              </th>
+              <td>
+                <h6 className="m-0 text-muted">
+                  {t('search_help.tag.desc', { tag: 'wiki' })}
+                </h6>
+              </td>
             </tr>
             </tr>
             <tr>
             <tr>
-              <th className="py-2"><code>-tag:wiki</code></th>
-              <td><h6 className="m-0 text-muted">{ t('search_help.exclude_tag.desc', { tag: 'wiki' }) }</h6></td>
+              <th className="py-2">
+                <code>-tag:wiki</code>
+              </th>
+              <td>
+                <h6 className="m-0 text-muted">
+                  {t('search_help.exclude_tag.desc', { tag: 'wiki' })}
+                </h6>
+              </td>
             </tr>
             </tr>
           </tbody>
           </tbody>
         </table>
         </table>

+ 15 - 20
apps/app/src/features/search/client/components/SearchMenuItem.tsx

@@ -1,35 +1,30 @@
-import React, { type JSX } from 'react';
+import type React from 'react';
+import type { JSX } from 'react';
 
 
 import type { GetItemProps } from '../interfaces/downshift';
 import type { GetItemProps } from '../interfaces/downshift';
 
 
 import styles from './SearchMenuItem.module.scss';
 import styles from './SearchMenuItem.module.scss';
 
 
 type Props = {
 type Props = {
-  url: string
-  index: number
-  isActive: boolean
-  getItemProps: GetItemProps
-  children: React.ReactNode
-}
+  url: string;
+  index: number;
+  isActive: boolean;
+  getItemProps: GetItemProps;
+  children: React.ReactNode;
+};
 
 
 export const SearchMenuItem = (props: Props): JSX.Element => {
 export const SearchMenuItem = (props: Props): JSX.Element => {
-  const {
-    url, index, isActive, getItemProps, children,
-  } = props;
+  const { url, index, isActive, getItemProps, children } = props;
 
 
-  const itemMenuOptions = (
-    getItemProps({
-      index,
-      item: { url },
-      className: `d-flex align-items-center px-2 py-1 text-muted ${isActive ? 'active' : ''}`,
-    })
-  );
+  const itemMenuOptions = getItemProps({
+    index,
+    item: { url },
+    className: `d-flex align-items-center px-2 py-1 text-muted ${isActive ? 'active' : ''}`,
+  });
 
 
   return (
   return (
     <div className={`search-menu-item ${styles['search-menu-item']}`}>
     <div className={`search-menu-item ${styles['search-menu-item']}`}>
-      <li {...itemMenuOptions}>
-        { children }
-      </li>
+      <li {...itemMenuOptions}>{children}</li>
     </div>
     </div>
   );
   );
 };
 };

+ 29 - 21
apps/app/src/features/search/client/components/SearchMethodMenuItem.tsx

@@ -1,7 +1,6 @@
-import React, { type JSX } from 'react';
-
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import React, { type JSX } from 'react';
 
 
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 
 
@@ -10,30 +9,28 @@ import type { GetItemProps } from '../interfaces/downshift';
 import { SearchMenuItem } from './SearchMenuItem';
 import { SearchMenuItem } from './SearchMenuItem';
 
 
 type Props = {
 type Props = {
-  activeIndex: number | null
-  searchKeyword: string
-  getItemProps: GetItemProps
-}
+  activeIndex: number | null;
+  searchKeyword: string;
+  getItemProps: GetItemProps;
+};
 
 
 export const SearchMethodMenuItem = (props: Props): JSX.Element => {
 export const SearchMethodMenuItem = (props: Props): JSX.Element => {
-  const {
-    activeIndex, searchKeyword, getItemProps,
-  } = props;
+  const { activeIndex, searchKeyword, getItemProps } = props;
 
 
   const { t } = useTranslation('commons');
   const { t } = useTranslation('commons');
 
 
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
 
 
-  const dPagePath = (new DevidedPagePath(currentPagePath ?? '', true, true));
+  const dPagePath = new DevidedPagePath(currentPagePath ?? '', true, true);
   const currentPageName = `
   const currentPageName = `
-  ${(!(dPagePath.isRoot || dPagePath.isFormerRoot) ? '...' : '')}/${(dPagePath.isRoot ? '' : `${dPagePath.latter}/`)}
+  ${!(dPagePath.isRoot || dPagePath.isFormerRoot) ? '...' : ''}/${dPagePath.isRoot ? '' : `${dPagePath.latter}/`}
   `;
   `;
 
 
   const shouldShowMenuItem = searchKeyword.trim().length > 0;
   const shouldShowMenuItem = searchKeyword.trim().length > 0;
 
 
   return (
   return (
     <div>
     <div>
-      { shouldShowMenuItem && (
+      {shouldShowMenuItem && (
         <div data-testid="search-all-menu-item">
         <div data-testid="search-all-menu-item">
           <SearchMenuItem
           <SearchMenuItem
             index={0}
             index={0}
@@ -41,10 +38,14 @@ export const SearchMethodMenuItem = (props: Props): JSX.Element => {
             getItemProps={getItemProps}
             getItemProps={getItemProps}
             url={`/_search?q=${searchKeyword}`}
             url={`/_search?q=${searchKeyword}`}
           >
           >
-            <span className="material-symbols-outlined fs-4 me-3 p-0">search</span>
+            <span className="material-symbols-outlined fs-4 me-3 p-0">
+              search
+            </span>
             <div className="w-100 d-flex align-items-md-center flex-md-row align-items-start flex-column">
             <div className="w-100 d-flex align-items-md-center flex-md-row align-items-start flex-column">
               <span className="text-break me-auto">{searchKeyword}</span>
               <span className="text-break me-auto">{searchKeyword}</span>
-              <span className="small text-body-tertiary">{t('search_method_menu_item.search_in_all')}</span>
+              <span className="small text-body-tertiary">
+                {t('search_method_menu_item.search_in_all')}
+              </span>
             </div>
             </div>
           </SearchMenuItem>
           </SearchMenuItem>
         </div>
         </div>
@@ -56,30 +57,37 @@ export const SearchMethodMenuItem = (props: Props): JSX.Element => {
           getItemProps={getItemProps}
           getItemProps={getItemProps}
           url={`/_search?q=prefix:${currentPagePath} ${searchKeyword}`}
           url={`/_search?q=prefix:${currentPagePath} ${searchKeyword}`}
         >
         >
-          <span className="material-symbols-outlined fs-4 me-3 p-0">search</span>
+          <span className="material-symbols-outlined fs-4 me-3 p-0">
+            search
+          </span>
           <div className="w-100 d-flex align-items-md-center flex-md-row align-items-start flex-column">
           <div className="w-100 d-flex align-items-md-center flex-md-row align-items-start flex-column">
             <code className="text-break">{currentPageName}</code>
             <code className="text-break">{currentPageName}</code>
             <span className="ms-md-2 text-break me-auto">{searchKeyword}</span>
             <span className="ms-md-2 text-break me-auto">{searchKeyword}</span>
-            <span className="small text-body-tertiary">{t('search_method_menu_item.only_children_of_this_tree')}</span>
+            <span className="small text-body-tertiary">
+              {t('search_method_menu_item.only_children_of_this_tree')}
+            </span>
           </div>
           </div>
         </SearchMenuItem>
         </SearchMenuItem>
       </div>
       </div>
 
 
-      { shouldShowMenuItem && (
+      {shouldShowMenuItem && (
         <SearchMenuItem
         <SearchMenuItem
           index={2}
           index={2}
           isActive={activeIndex === 2}
           isActive={activeIndex === 2}
           getItemProps={getItemProps}
           getItemProps={getItemProps}
           url={`/_search?q="${searchKeyword}"`}
           url={`/_search?q="${searchKeyword}"`}
         >
         >
-          <span className="material-symbols-outlined fs-4 me-3 p-0">search</span>
+          <span className="material-symbols-outlined fs-4 me-3 p-0">
+            search
+          </span>
           <div className="w-100 d-flex align-items-md-center flex-md-row align-items-start flex-column">
           <div className="w-100 d-flex align-items-md-center flex-md-row align-items-start flex-column">
             <span className="text-break me-auto">{`"${searchKeyword}"`}</span>
             <span className="text-break me-auto">{`"${searchKeyword}"`}</span>
-            <span className="small text-body-tertiary">{t('search_method_menu_item.exact_mutch')}</span>
+            <span className="small text-body-tertiary">
+              {t('search_method_menu_item.exact_mutch')}
+            </span>
           </div>
           </div>
         </SearchMenuItem>
         </SearchMenuItem>
-      ) }
+      )}
     </div>
     </div>
-
   );
   );
 };
 };

+ 29 - 17
apps/app/src/features/search/client/components/SearchModal.tsx

@@ -1,10 +1,9 @@
-
-import React, {
-  useState, useCallback, useEffect, type JSX,
-} from 'react';
-
-import Downshift, { type DownshiftState, type StateChangeOptions } from 'downshift';
+import Downshift, {
+  type DownshiftState,
+  type StateChangeOptions,
+} from 'downshift';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { Modal, ModalBody } from 'reactstrap';
 import { Modal, ModalBody } from 'reactstrap';
 
 
 import { isIncludeAiMenthion, removeAiMenthion } from '../../utils/ai';
 import { isIncludeAiMenthion, removeAiMenthion } from '../../utils/ai';
@@ -17,7 +16,6 @@ import { SearchMethodMenuItem } from './SearchMethodMenuItem';
 import { SearchResultMenuItem } from './SearchResultMenuItem';
 import { SearchResultMenuItem } from './SearchResultMenuItem';
 
 
 const SearchModal = (): JSX.Element => {
 const SearchModal = (): JSX.Element => {
-
   const [searchKeyword, setSearchKeyword] = useState('');
   const [searchKeyword, setSearchKeyword] = useState('');
   const [isMenthionedToAi, setMenthionedToAi] = useState(false);
   const [isMenthionedToAi, setMenthionedToAi] = useState(false);
 
 
@@ -29,10 +27,13 @@ const SearchModal = (): JSX.Element => {
     setSearchKeyword(searchText);
     setSearchKeyword(searchText);
   }, []);
   }, []);
 
 
-  const selectSearchMenuItemHandler = useCallback((selectedItem: DownshiftItem) => {
-    router.push(selectedItem.url);
-    closeSearchModal();
-  }, [closeSearchModal, router]);
+  const selectSearchMenuItemHandler = useCallback(
+    (selectedItem: DownshiftItem) => {
+      router.push(selectedItem.url);
+      closeSearchModal();
+    },
+    [closeSearchModal, router],
+  );
 
 
   const submitHandler = useCallback(() => {
   const submitHandler = useCallback(() => {
     const url = new URL('_search', 'http://example.com');
     const url = new URL('_search', 'http://example.com');
@@ -41,7 +42,10 @@ const SearchModal = (): JSX.Element => {
     closeSearchModal();
     closeSearchModal();
   }, [closeSearchModal, router, searchKeyword]);
   }, [closeSearchModal, router, searchKeyword]);
 
 
-  const stateReducer = (state: DownshiftState<DownshiftItem>, changes: StateChangeOptions<DownshiftItem>) => {
+  const stateReducer = (
+    state: DownshiftState<DownshiftItem>,
+    changes: StateChangeOptions<DownshiftItem>,
+  ) => {
     // Do not update highlightedIndex on mouse hover
     // Do not update highlightedIndex on mouse hover
     if (changes.type === Downshift.stateChangeTypes.itemMouseEnter) {
     if (changes.type === Downshift.stateChangeTypes.itemMouseEnter) {
       return {
       return {
@@ -59,8 +63,7 @@ const SearchModal = (): JSX.Element => {
     }
     }
     if (searchModalData?.searchKeyword == null) {
     if (searchModalData?.searchKeyword == null) {
       setSearchKeyword('');
       setSearchKeyword('');
-    }
-    else {
+    } else {
       setSearchKeyword(searchModalData.searchKeyword);
       setSearchKeyword(searchModalData.searchKeyword);
     }
     }
   }, [searchModalData?.isOpened, searchModalData?.searchKeyword]);
   }, [searchModalData?.isOpened, searchModalData?.searchKeyword]);
@@ -72,7 +75,12 @@ const SearchModal = (): JSX.Element => {
   const searchKeywordWithoutAi = removeAiMenthion(searchKeyword);
   const searchKeywordWithoutAi = removeAiMenthion(searchKeyword);
 
 
   return (
   return (
-    <Modal size="lg" isOpen={searchModalData?.isOpened ?? false} toggle={closeSearchModal} data-testid="search-modal">
+    <Modal
+      size="lg"
+      isOpen={searchModalData?.isOpened ?? false}
+      toggle={closeSearchModal}
+      data-testid="search-modal"
+    >
       <ModalBody className="pb-2">
       <ModalBody className="pb-2">
         <Downshift
         <Downshift
           onSelect={selectSearchMenuItemHandler}
           onSelect={selectSearchMenuItemHandler}
@@ -88,7 +96,9 @@ const SearchModal = (): JSX.Element => {
           }) => (
           }) => (
             <div {...getRootProps({}, { suppressRefError: true })}>
             <div {...getRootProps({}, { suppressRefError: true })}>
               <div className="text-muted d-flex justify-content-center align-items-center p-1">
               <div className="text-muted d-flex justify-content-center align-items-center p-1">
-                <span className={`material-symbols-outlined fs-4 me-3 ${isMenthionedToAi ? 'text-primary' : ''}`}>
+                <span
+                  className={`material-symbols-outlined fs-4 me-3 ${isMenthionedToAi ? 'text-primary' : ''}`}
+                >
                   {isMenthionedToAi ? 'psychology' : 'search'}
                   {isMenthionedToAi ? 'psychology' : 'search'}
                 </span>
                 </span>
                 <SearchForm
                 <SearchForm
@@ -102,7 +112,9 @@ const SearchModal = (): JSX.Element => {
                   className="btn border-0 d-flex justify-content-center p-0"
                   className="btn border-0 d-flex justify-content-center p-0"
                   onClick={closeSearchModal}
                   onClick={closeSearchModal}
                 >
                 >
-                  <span className="material-symbols-outlined fs-4 ms-3 py-0">close</span>
+                  <span className="material-symbols-outlined fs-4 ms-3 py-0">
+                    close
+                  </span>
                 </button>
                 </button>
               </div>
               </div>
 
 

+ 40 - 30
apps/app/src/features/search/client/components/SearchResultMenuItem.tsx

@@ -1,6 +1,5 @@
-import React, { useCallback, type JSX } from 'react';
-
 import { PagePathLabel, UserPicture } from '@growi/ui/dist/components';
 import { PagePathLabel, UserPicture } from '@growi/ui/dist/components';
+import React, { type JSX, useCallback } from 'react';
 import { useDebounce } from 'usehooks-ts';
 import { useDebounce } from 'usehooks-ts';
 
 
 import { useSWRxSearch } from '~/stores/search';
 import { useSWRxSearch } from '~/stores/search';
@@ -10,10 +9,10 @@ import type { GetItemProps } from '../interfaces/downshift';
 import { SearchMenuItem } from './SearchMenuItem';
 import { SearchMenuItem } from './SearchMenuItem';
 
 
 type Props = {
 type Props = {
-  activeIndex: number | null,
-  searchKeyword: string,
-  getItemProps: GetItemProps,
-}
+  activeIndex: number | null;
+  searchKeyword: string;
+  getItemProps: GetItemProps;
+};
 export const SearchResultMenuItem = (props: Props): JSX.Element => {
 export const SearchResultMenuItem = (props: Props): JSX.Element => {
   const { activeIndex, searchKeyword, getItemProps } = props;
   const { activeIndex, searchKeyword, getItemProps } = props;
 
 
@@ -21,16 +20,23 @@ export const SearchResultMenuItem = (props: Props): JSX.Element => {
 
 
   const isEmptyKeyword = searchKeyword.trim().length === 0;
   const isEmptyKeyword = searchKeyword.trim().length === 0;
 
 
-  const { data: searchResult, isLoading } = useSWRxSearch(isEmptyKeyword ? null : debouncedKeyword, null, { limit: 10 });
+  const { data: searchResult, isLoading } = useSWRxSearch(
+    isEmptyKeyword ? null : debouncedKeyword,
+    null,
+    { limit: 10 },
+  );
 
 
   /**
   /**
    *  SearchMenu is a combination of a list of SearchMethodMenuItem and SearchResultMenuItem (this component).
    *  SearchMenu is a combination of a list of SearchMethodMenuItem and SearchResultMenuItem (this component).
    *  If no keywords are entered into SearchForm, SearchMethodMenuItem returns a single item. Conversely, when keywords are entered, three items are returned.
    *  If no keywords are entered into SearchForm, SearchMethodMenuItem returns a single item. Conversely, when keywords are entered, three items are returned.
    *  For these reasons, the starting index of SearchResultMemuItem changes depending on the presence or absence of the searchKeyword.
    *  For these reasons, the starting index of SearchResultMemuItem changes depending on the presence or absence of the searchKeyword.
    */
    */
-  const getFiexdIndex = useCallback((index: number) => {
-    return (isEmptyKeyword ? 1 : 3) + index;
-  }, [isEmptyKeyword]);
+  const getFiexdIndex = useCallback(
+    (index: number) => {
+      return (isEmptyKeyword ? 1 : 3) + index;
+    },
+    [isEmptyKeyword],
+  );
 
 
   if (isLoading) {
   if (isLoading) {
     return (
     return (
@@ -41,35 +47,39 @@ export const SearchResultMenuItem = (props: Props): JSX.Element => {
     );
     );
   }
   }
 
 
-  if (isEmptyKeyword || searchResult == null || searchResult.data.length === 0) {
+  if (
+    isEmptyKeyword ||
+    searchResult == null ||
+    searchResult.data.length === 0
+  ) {
     return <></>;
     return <></>;
   }
   }
 
 
   return (
   return (
     <div>
     <div>
       <div className="border-top mt-2 mb-2" />
       <div className="border-top mt-2 mb-2" />
-      {searchResult?.data
-        .map((item, index) => (
-          <SearchMenuItem
-            key={item.data._id}
-            index={getFiexdIndex(index)}
-            isActive={getFiexdIndex(index) === activeIndex}
-            getItemProps={getItemProps}
-            url={item.data._id}
-          >
-            <UserPicture user={item.data.creator} />
+      {searchResult?.data.map((item, index) => (
+        <SearchMenuItem
+          key={item.data._id}
+          index={getFiexdIndex(index)}
+          isActive={getFiexdIndex(index) === activeIndex}
+          getItemProps={getItemProps}
+          url={item.data._id}
+        >
+          <UserPicture user={item.data.creator} />
 
 
-            <span className="ms-3 text-break text-wrap">
-              <PagePathLabel path={item.data.path} />
-            </span>
+          <span className="ms-3 text-break text-wrap">
+            <PagePathLabel path={item.data.path} />
+          </span>
 
 
-            <span className="text-body-tertiary ms-2 d-flex justify-content-center align-items-center">
-              <span className="material-symbols-outlined fs-6 p-0">footprint</span>
-              <span className="fs-6">{item.data.seenUsers.length}</span>
+          <span className="text-body-tertiary ms-2 d-flex justify-content-center align-items-center">
+            <span className="material-symbols-outlined fs-6 p-0">
+              footprint
             </span>
             </span>
-          </SearchMenuItem>
-        ))
-      }
+            <span className="fs-6">{item.data.seenUsers.length}</span>
+          </span>
+        </SearchMenuItem>
+      ))}
     </div>
     </div>
   );
   );
 };
 };

+ 4 - 2
apps/app/src/features/search/client/interfaces/downshift.ts

@@ -2,5 +2,7 @@ import type { ControllerStateAndHelpers } from 'downshift';
 
 
 export type DownshiftItem = { url: string };
 export type DownshiftItem = { url: string };
 
 
-export type GetItemProps = ControllerStateAndHelpers<DownshiftItem>['getItemProps']
-export type GetInputProps = ControllerStateAndHelpers<DownshiftItem>['getInputProps']
+export type GetItemProps =
+  ControllerStateAndHelpers<DownshiftItem>['getItemProps'];
+export type GetInputProps =
+  ControllerStateAndHelpers<DownshiftItem>['getInputProps'];

+ 24 - 12
apps/app/src/features/search/client/stores/search.ts

@@ -5,23 +5,35 @@ import type { SWRResponse } from 'swr';
 import { useStaticSWR } from '~/stores/use-static-swr';
 import { useStaticSWR } from '~/stores/use-static-swr';
 
 
 type SearchModalStatus = {
 type SearchModalStatus = {
-  isOpened: boolean,
-  searchKeyword?: string,
-}
+  isOpened: boolean;
+  searchKeyword?: string;
+};
 
 
 type SearchModalUtils = {
 type SearchModalUtils = {
-  open(keywordOnInit?: string): void
-  close(): void
-}
-export const useSearchModal = (status?: SearchModalStatus): SWRResponse<SearchModalStatus, Error> & SearchModalUtils => {
+  open(keywordOnInit?: string): void;
+  close(): void;
+};
+export const useSearchModal = (
+  status?: SearchModalStatus,
+): SWRResponse<SearchModalStatus, Error> & SearchModalUtils => {
   const initialStatus = { isOpened: false };
   const initialStatus = { isOpened: false };
-  const swrResponse = useStaticSWR<SearchModalStatus, Error>('SearchModal', status, { fallbackData: initialStatus });
+  const swrResponse = useStaticSWR<SearchModalStatus, Error>(
+    'SearchModal',
+    status,
+    { fallbackData: initialStatus },
+  );
 
 
   return {
   return {
     ...swrResponse,
     ...swrResponse,
-    open: useCallback((keywordOnInit?: string) => {
-      swrResponse.mutate({ isOpened: true, searchKeyword: keywordOnInit });
-    }, [swrResponse]),
-    close: useCallback(() => swrResponse.mutate({ isOpened: false }), [swrResponse]),
+    open: useCallback(
+      (keywordOnInit?: string) => {
+        swrResponse.mutate({ isOpened: true, searchKeyword: keywordOnInit });
+      },
+      [swrResponse],
+    ),
+    close: useCallback(
+      () => swrResponse.mutate({ isOpened: false }),
+      [swrResponse],
+    ),
   };
   };
 };
 };

+ 0 - 3
biome.json

@@ -28,13 +28,10 @@
       "!apps/app/src/client/**",
       "!apps/app/src/client/**",
       "!apps/app/src/components/**",
       "!apps/app/src/components/**",
       "!apps/app/src/features/growi-plugin/**",
       "!apps/app/src/features/growi-plugin/**",
-      "!apps/app/src/features/mermaid/**",
       "!apps/app/src/features/openai/**",
       "!apps/app/src/features/openai/**",
       "!apps/app/src/features/opentelemetry/**",
       "!apps/app/src/features/opentelemetry/**",
       "!apps/app/src/features/page-bulk-export/**",
       "!apps/app/src/features/page-bulk-export/**",
-      "!apps/app/src/features/plantuml/**",
       "!apps/app/src/features/rate-limiter/**",
       "!apps/app/src/features/rate-limiter/**",
-      "!apps/app/src/features/search/**",
       "!apps/app/src/interfaces/**",
       "!apps/app/src/interfaces/**",
       "!apps/app/src/models/**",
       "!apps/app/src/models/**",
       "!apps/app/src/pages/**",
       "!apps/app/src/pages/**",