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

Merge branch 'master' into dev/7.0.x

Yuki Takei 2 лет назад
Родитель
Сommit
18e9033254

+ 1 - 1
.github/ISSUE_TEMPLATE/config.yml

@@ -4,5 +4,5 @@ contact_links:
     url: https://github.com/weseek/growi/discussions
     about: If you have feature requests or suggestions, you can create a new discussion and consider it with the community.
   - name: Questions
-    url: https://growi-slackin.weseek.co.jp/
+    url: https://communityinviter.com/apps/wsgrowi/invite/
     about: If you have questions, you can join our Slack team and talk about anything, anytime.

+ 19 - 0
CHANGELOG.md

@@ -4,6 +4,25 @@
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.1.15](https://github.com/weseek/growi/compare/v6.1.14...v6.1.15) - 2023-09-11
+
+### 🚀 Improvement
+
+- imprv: Add CSP style-src for Safari and Content-Disposition of attachment (for v6.1.x) (#8057) @yuki-takei
+
+## [v6.1.14](https://github.com/weseek/growi/compare/v6.1.13...v6.1.14) - 2023-08-22
+
+### 🐛 Bug Fixes
+
+- fix: Add option to lightbox (6.1.x) (#8003) @yuki-takei
+
+## [v6.1.13](https://github.com/weseek/growi/compare/v6.1.12...v6.1.13) - 2023-08-17
+
+### 🐛 Bug Fixes
+
+- fix: Do not work img tag if use style property (#7988) @jam411
+- fix: "Searching..." label appearing unnecessarily (#7990) @yuki-takei
+
 ## [v6.1.12](https://github.com/weseek/growi/compare/v6.1.11...v6.1.12) - 2023-08-14
 
 ### 🐛 Bug Fixes

+ 2 - 2
README.md

@@ -7,7 +7,7 @@
 </p>
 <p align="center">
   <a href="https://github.com/weseek/growi/releases/latest"><img src="https://img.shields.io/github/release/weseek/growi.svg"></a>
-  <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+  <a href="https://communityinviter.com/apps/wsgrowi/invite/">join our Slack team</a>
 </p>
 
 <p align="center">
@@ -132,7 +132,7 @@ You can write issues and PRs in English or Japanese.
 
 ## Discussion
 
-If you have questions or suggestions, you can [join our Slack team](https://growi-slackin.weseek.co.jp/) and talk about anything, anytime.
+If you have questions or suggestions, you can [join our Slack team](https://communityinviter.com/apps/wsgrowi/invite/) and talk about anything, anytime.
 
 # License
 

+ 2 - 2
README_JP.md

@@ -6,7 +6,7 @@
   </p>
   <p align="center">
     <a href="https://github.com/weseek/growi/releases/latest"><img src="https://img.shields.io/github/release/weseek/growi.svg"></a>
-    <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+    <a href="https://communityinviter.com/apps/wsgrowi/invite/">join our Slack team</a>
   </p>
 
 <p align="center">
@@ -129,7 +129,7 @@ Issue と Pull requests の作成は英語・日本語どちらでも受け付
 
 ## GROWI について話し合いましょう!
 
-質問や提案があれば、私たちの [Slack team](https://growi-slackin.weseek.co.jp/) にぜひご参加ください。
+質問や提案があれば、私たちの [Slack team](https://communityinviter.com/apps/wsgrowi/invite/) にぜひご参加ください。
 いつでも、どこでも GROWI について議論しましょう!
 
 # ライセンス

+ 1 - 1
SECURITY.md

@@ -13,7 +13,7 @@
 
 If you believe you have found a security vulnerability in any GROWI related repository, please report it to us using one of the methods described below.
 
-  * [Join our Slack team](https://growi-slackin.weseek.co.jp/) and send DM to `@yuki` who is the lead developer
+  * [Join our Slack team](https://communityinviter.com/apps/wsgrowi/invite/) and send DM to `@yuki` who is the lead developer
   * Report to JPCERT/CC[^jpcertcc]
     * [[PDF] JPCERT/CC Vulnerability Coordination and Disclosure Policy](https://www.jpcert.or.jp/english/vh/vul-coordination-disclosure-policy_2019.pdf)
 

+ 1 - 1
apps/app/resource/locales/en_US/welcome.md

@@ -58,7 +58,7 @@ We can display the content list using a table and `$lsx`.
 
 # Slack
 
-<a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+<a href="https://communityinviter.com/apps/wsgrowi/invite/">join our Slack team</a>
 
 We welcome newcomers joining our slack channel to help improve GROWI.
 In addition to discussing development, we are also happy to answer your questions when you join.

+ 1 - 1
apps/app/resource/locales/ja_JP/welcome.md

@@ -54,7 +54,7 @@ GROWI は個人・法人向けの Wiki | ナレッジベースツールです。
 
 # Slack
 
-<a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+<a href="https://communityinviter.com/apps/wsgrowi/invite/">join our Slack team</a>
 
 GROWI をより良いものにするために、是非 Slack に参加してください。  
 開発に関する議論を行っている他、導入時の質問等も受け付けています。

+ 1 - 1
apps/app/resource/locales/zh_CN/welcome.md

@@ -58,7 +58,7 @@ GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
 
 # Slack
 
-<a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+<a href="https://communityinviter.com/apps/wsgrowi/invite/">join our Slack team</a>
 
 我们欢迎新人加入我们的slack频道,帮助改善Growi。
 除了讨论发展问题,我们也很乐意在你加入时回答你的问题。

+ 58 - 0
apps/app/src/client/services/search-operation.ts

@@ -0,0 +1,58 @@
+import { useCallback, useEffect, useRef } from 'react';
+
+import { type SWRResponseWithUtils, withUtils } from '@growi/core/dist/swr';
+import { useRouter } from 'next/router';
+import useSWRImmutable from 'swr/immutable';
+
+
+type UseKeywordManagerUtils = {
+  pushState: (newKeyword: string) => void,
+}
+
+export const useKeywordManager = (): SWRResponseWithUtils<UseKeywordManagerUtils, string> => {
+  // routerRef solve the problem of infinite redrawing that occurs with routers
+  const router = useRouter();
+  const routerRef = useRef(router);
+
+  // parse URL Query
+  const queries = router.query.q;
+  const initialKeyword = (Array.isArray(queries) ? queries.join(' ') : queries) ?? '';
+
+  const swrResponse = useSWRImmutable<string>('searchKeyword', null, {
+    fallbackData: initialKeyword,
+  });
+
+  const { mutate } = swrResponse;
+  const pushState = useCallback((newKeyword: string) => {
+    mutate((prevKeyword) => {
+      if (prevKeyword !== newKeyword) {
+        const newUrl = new URL('/_search', 'http://example.com');
+        newUrl.searchParams.append('q', newKeyword);
+        routerRef.current.push(`${newUrl.pathname}${newUrl.search}`, '');
+      }
+
+      return newKeyword;
+    });
+  }, [mutate]);
+
+  // detect search keyword from the query of URL
+  useEffect(() => {
+    mutate(initialKeyword);
+  }, [mutate, initialKeyword]);
+
+  // browser back and forward
+  useEffect(() => {
+    routerRef.current.beforePopState(({ url }) => {
+      const newUrl = new URL(url, 'https://exmple.com');
+      const newKeyword = newUrl.searchParams.get('q');
+      if (newKeyword != null) {
+        mutate(newKeyword);
+      }
+      return true;
+    });
+  }, [mutate]);
+
+  return withUtils(swrResponse, {
+    pushState,
+  });
+};

+ 4 - 5
apps/app/src/components/Navbar/GlobalSearch.tsx

@@ -9,6 +9,7 @@ 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,
@@ -46,6 +47,8 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
   const [isScopeChildren, setScopeChildren] = useState<boolean|undefined>(isSearchScopeChildrenAsDefault ?? false);
   const [isFocused, setFocused] = useState<boolean>(false);
 
+  const { pushState } = useKeywordManager();
+
   useEffect(() => {
     setScopeChildren(isSearchScopeChildrenAsDefault);
   }, [isSearchScopeChildrenAsDefault]);
@@ -63,17 +66,13 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
   }, [returnPathForURL, router]);
 
   const search = useCallback(() => {
-    const url = new URL(window.location.href);
-    url.pathname = '/_search';
-
     // construct search query
     let q = text;
     if (isScopeChildren) {
       q += ` prefix:${currentPagePath ?? window.location.pathname}`;
     }
-    url.searchParams.append('q', q);
 
-    router.push(url.href);
+    pushState(q);
   }, [currentPagePath, isScopeChildren, router, text]);
 
   const scopeLabel = isScopeChildren

+ 10 - 1
apps/app/src/components/Page/RenderTagLabels.tsx

@@ -2,6 +2,8 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 
+import { useKeywordManager } from '~/client/services/search-operation';
+
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 
@@ -15,6 +17,8 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
   const { tags, isTagLabelsDisabled, openEditorModal } = props;
   const { t } = useTranslation();
 
+  const { pushState } = useKeywordManager();
+
   function openEditorHandler() {
     if (openEditorModal == null) {
       return;
@@ -28,7 +32,12 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
     <>
       {tags.map((tag) => {
         return (
-          <a key={tag} href={`/_search?q=tag:${tag}`} className="grw-tag-label badge bg-primary me-2">
+          <a
+            key={tag}
+            type="button"
+            className="grw-tag-label badge bg-primary me-2"
+            onClick={() => pushState(`tag:${tag}`)}
+          >
             {tag}
           </a>
         );

+ 15 - 40
apps/app/src/components/SearchPage.tsx

@@ -1,23 +1,21 @@
 import React, {
-  useCallback, useEffect, useMemo, useRef, useState,
+  useCallback, useMemo, useRef, useState,
 } from 'react';
 
-
 import { useTranslation } from 'next-i18next';
-import { useRouter } from 'next/router';
-
 
-import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
-import { IFormattedSearchResult } from '~/interfaces/search';
+import type { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
+import { useKeywordManager } from '~/client/services/search-operation';
+import type { IFormattedSearchResult } from '~/interfaces/search';
 import { useIsSearchServiceReachable, useShowPageLimitationL } from '~/stores/context';
-import { ISearchConditions, ISearchConfigurations, useSWRxSearch } from '~/stores/search';
+import { type ISearchConditions, type ISearchConfigurations, useSWRxSearch } from '~/stores/search';
 
 import { NotAvailableForGuest } from './NotAvailableForGuest';
 import { NotAvailableForReadOnlyUser } from './NotAvailableForReadOnlyUser';
 import PaginationWrapper from './PaginationWrapper';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import SearchControl from './SearchPage/SearchControl';
-import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
+import { type IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
 
 
 // TODO: replace with "customize:showPageLimitationS"
@@ -93,15 +91,8 @@ export const SearchPage = (): JSX.Element => {
   const { t } = useTranslation();
   const { data: showPageLimitationL } = useShowPageLimitationL();
 
-  // routerRef solve the problem of infinite redrawing that occurs with routers
-  const router = useRouter();
-  const routerRef = useRef(router);
+  const { data: keyword, pushState } = useKeywordManager();
 
-  // parse URL Query
-  const queries = router.query.q;
-  const initQ = (Array.isArray(queries) ? queries.join(' ') : queries) ?? '';
-
-  const [keyword, setKeyword] = useState<string>(initQ);
   const [offset, setOffset] = useState<number>(0);
   const [limit, setLimit] = useState<number>(showPageLimitationL ?? INITIAL_PAGIONG_SIZE);
   const [configurationsByControl, setConfigurationsByControl] = useState<Partial<ISearchConfigurations>>({});
@@ -110,17 +101,20 @@ export const SearchPage = (): JSX.Element => {
 
   const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
 
-  const { data, conditions, mutate } = useSWRxSearch(keyword, null, {
+  const { data, conditions, mutate } = useSWRxSearch(keyword ?? '', null, {
     ...configurationsByControl,
     offset,
     limit,
   });
 
-  const searchInvokedHandler = useCallback((_keyword: string, newConfigurations: Partial<ISearchConfigurations>) => {
-    setKeyword(_keyword);
+  const searchInvokedHandler = useCallback((newKeyword: string, newConfigurations: Partial<ISearchConfigurations>) => {
     setOffset(0);
     setConfigurationsByControl(newConfigurations);
-  }, []);
+
+    pushState(newKeyword);
+
+    mutate();
+  }, [keyword, mutate, pushState]);
 
   const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => {
     const instance = searchPageBaseRef.current;
@@ -176,25 +170,6 @@ export const SearchPage = (): JSX.Element => {
   // for bulk deletion
   const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate());
 
-  // push state
-  useEffect(() => {
-    const newUrl = new URL('/_search', 'http://example.com');
-    newUrl.searchParams.append('q', keyword);
-    routerRef.current.push(`${newUrl.pathname}${newUrl.search}`, '', { shallow: true });
-  }, [keyword, routerRef]);
-
-  // browser back and forward
-  useEffect(() => {
-    routerRef.current.beforePopState(({ url }) => {
-      const newUrl = new URL(url, 'https://exmple.com');
-      const newKeyword = newUrl.searchParams.get('q');
-      if (newKeyword != null) {
-        setKeyword(newKeyword);
-      }
-      return true;
-    });
-  }, [setKeyword, routerRef]);
-
   const hitsCount = data?.meta.hitsCount;
 
   const allControl = useMemo(() => {
@@ -246,7 +221,7 @@ export const SearchPage = (): JSX.Element => {
     return (
       <SearchResultListHead
         searchResult={data}
-        searchingKeyword={keyword}
+        searchingKeyword={keyword ?? ''}
         offset={offset}
         pagingSize={limit}
         onPagingSizeChanged={pagingSizeChangedHandler}

+ 32 - 20
apps/app/src/components/SearchPage/SearchControl.tsx

@@ -18,7 +18,7 @@ type Props = {
   isEnableFilter: boolean,
   initialSearchConditions: Partial<ISearchConditions>,
 
-  onSearchInvoked: (keyword: string, configurations: Partial<ISearchConfigurations>) => void,
+  onSearchInvoked?: (keyword: string, configurations: Partial<ISearchConfigurations>) => void,
 
   allControl: React.ReactNode,
 }
@@ -34,7 +34,9 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
     allControl,
   } = props;
 
-  const [keyword, setKeyword] = useState(initialSearchConditions.keyword ?? '');
+  const keywordOnInit = initialSearchConditions.keyword ?? '';
+
+  const [keyword, setKeyword] = useState(keywordOnInit);
   const [sort, setSort] = useState<SORT_AXIS>(initialSearchConditions.sort ?? SORT_AXIS.RELATION_SCORE);
   const [order, setOrder] = useState<SORT_ORDER>(initialSearchConditions.order ?? SORT_ORDER.DESC);
   const [includeUserPages, setIncludeUserPages] = useState(initialSearchConditions.includeUserPages ?? false);
@@ -43,32 +45,42 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
 
   const { t } = useTranslation('');
 
-  const invokeSearch = useCallback(() => {
-    if (onSearchInvoked == null) {
-      return;
-    }
+  const searchFormSubmittedHandler = useCallback((input: string) => {
+    setKeyword(input);
 
-    onSearchInvoked(keyword, {
+    onSearchInvoked?.(input, {
       sort, order, includeUserPages, includeTrashPages,
     });
-  }, [keyword, sort, order, includeTrashPages, includeUserPages, onSearchInvoked]);
-
-  const searchFormSubmittedHandler = useCallback((input: string) => {
-    setKeyword(input);
-  }, []);
+  }, [includeTrashPages, includeUserPages, onSearchInvoked, order, sort]);
 
   const changeSortHandler = useCallback((nextSort: SORT_AXIS, nextOrder: SORT_ORDER) => {
     setSort(nextSort);
     setOrder(nextOrder);
-  }, []);
 
-  useEffect(() => {
-    invokeSearch();
-  }, [invokeSearch]);
+    onSearchInvoked?.(keyword, {
+      sort: nextSort, order: nextOrder, includeUserPages, includeTrashPages,
+    });
+  }, [includeTrashPages, includeUserPages, keyword, onSearchInvoked]);
+
+  const changeIncludeUserPagesHandler = useCallback((include: boolean) => {
+    setIncludeUserPages(include);
+
+    onSearchInvoked?.(keyword, {
+      sort, order, includeUserPages: include, includeTrashPages,
+    });
+  }, [includeTrashPages, keyword, onSearchInvoked, order, sort]);
+
+  const changeIncludeTrashPagesHandler = useCallback((include: boolean) => {
+    setIncludeTrashPages(include);
+
+    onSearchInvoked?.(keyword, {
+      sort, order, includeUserPages, includeTrashPages: include,
+    });
+  }, [includeUserPages, keyword, onSearchInvoked, order, sort]);
 
   useEffect(() => {
-    setKeyword(initialSearchConditions.keyword ?? '');
-  }, [initialSearchConditions.keyword]);
+    setKeyword(keywordOnInit);
+  }, [keywordOnInit]);
 
   return (
     <div className="shadow-sm">
@@ -128,7 +140,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
                     type="checkbox"
                     id="flexCheckDefault"
                     defaultChecked={includeUserPages}
-                    onChange={e => setIncludeUserPages(e.target.checked)}
+                    onChange={e => changeIncludeUserPagesHandler(e.target.checked)}
                   />
                   <label
                     className="form-label form-check-label mb-0 d-flex align-items-center text-secondary with-no-font-weight"
@@ -145,7 +157,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
                     type="checkbox"
                     id="flexCheckChecked"
                     checked={includeTrashPages}
-                    onChange={e => setIncludeTrashPages(e.target.checked)}
+                    onChange={e => changeIncludeTrashPagesHandler(e.target.checked)}
                   />
                   <label
                     className="form-label form-check-label d-flex align-items-center text-secondary with-no-font-weight"

+ 11 - 8
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -5,8 +5,8 @@ import React, {
 import { isPopulated, type IPageHasId } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { UserPicture, FootstampIcon } from '@growi/ui/dist/components';
-import Link from 'next/link';
 
+import { useKeywordManager } from '~/client/services/search-operation';
 import FormattedDistanceDate from '~/components/FormattedDistanceDate';
 import InfiniteScroll from '~/components/InfiniteScroll';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
@@ -27,7 +27,8 @@ type PageItemLowerProps = {
 }
 
 type PageItemProps = PageItemLowerProps & {
-  isSmall: boolean
+  isSmall: boolean,
+  onClickTag?: (tagName: string) => void,
 }
 
 const PageItemLower = memo(({ page }: PageItemLowerProps): JSX.Element => {
@@ -47,7 +48,7 @@ const PageItemLower = memo(({ page }: PageItemLowerProps): JSX.Element => {
 });
 PageItemLower.displayName = 'PageItemLower';
 
-const PageItem = memo(({ page, isSmall }: PageItemProps): JSX.Element => {
+const PageItem = memo(({ page, isSmall, onClickTag }: PageItemProps): JSX.Element => {
   const dPagePath = new DevidedPagePath(page.path, false, true);
   const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
   const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
@@ -68,14 +69,14 @@ const PageItem = memo(({ page, isSmall }: PageItemProps): JSX.Element => {
       return <></>;
     }
     return (
-      <Link
+      <a
         key={tag.name}
-        href={`/_search?q=tag:${tag.name}`}
+        type="button"
         className="grw-tag-label badge bg-primary me-2 small"
-        prefetch={false}
+        onClick={() => onClickTag?.(tag.name)}
       >
         {tag.name}
-      </Link>
+      </a>
     );
   });
 
@@ -156,6 +157,8 @@ export const RecentChangesContent = ({ isSmall }: ContentProps): JSX.Element =>
   const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE, { suspense: true });
   const { data } = swrInifinitexRecentlyUpdated;
 
+  const { pushState } = useKeywordManager();
+
   const isEmpty = data?.[0]?.pages.length === 0;
   const isReachingEnd = isEmpty || (data != null && data[data.length - 1]?.pages.length < PER_PAGE);
 
@@ -168,7 +171,7 @@ export const RecentChangesContent = ({ isSmall }: ContentProps): JSX.Element =>
         >
           { data != null && data.map(apiResult => apiResult.pages).flat()
             .map(page => (
-              <PageItem key={page._id} page={page} isSmall={isSmall} />
+              <PageItem key={page._id} page={page} isSmall={isSmall} onClickTag={tagName => pushState(`tag:${tagName}`)} />
             ))
           }
         </InfiniteScroll>

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

@@ -1,7 +1,6 @@
 import React, { FC, memo } from 'react';
 
-import Link from 'next/link';
-
+import { useKeywordManager } from '~/client/services/search-operation';
 import { IDataTagCount } from '~/interfaces/tag';
 
 
@@ -23,21 +22,20 @@ const TagCloudBox: FC<Props> = memo((props:(Props & typeof defaultProps)) => {
   const { tags } = props;
   const maxTagTextLength: number = props.maxTagTextLength ?? MAX_TAG_TEXT_LENGTH;
 
+  const { pushState } = useKeywordManager();
+
   const tagElements = tags.map((tag:IDataTagCount) => {
     const tagNameFormat = (tag.name).length > maxTagTextLength ? `${(tag.name).slice(0, maxTagTextLength)}...` : tag.name;
 
-    const url = new URL('/_search', 'https://example.com');
-    url.searchParams.append('q', `tag:${tag.name}`);
-
     return (
-      <Link
+      <a
         key={tag.name}
-        href={`${url.pathname}${url.search}`}
+        type="button"
         className="grw-tag-label badge bg-primary me-2"
-        prefetch={false}
+        onClick={() => pushState(`tag:${tag.name}`)}
       >
         {tagNameFormat}
-      </Link>
+      </a>
     );
   });
 

+ 12 - 15
apps/app/src/components/TagList.tsx

@@ -3,8 +3,8 @@ import React, {
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
-import Link from 'next/link';
 
+import { useKeywordManager } from '~/client/services/search-operation';
 import { IDataTagCount } from '~/interfaces/tag';
 
 import PaginationWrapper from './PaginationWrapper';
@@ -29,26 +29,23 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
   const isTagExist: boolean = tagData.length > 0;
   const { t } = useTranslation('');
 
-  const generateTagList = useCallback((tagData) => {
-    return tagData.map((tag:IDataTagCount, index:number) => {
-      const tagListClasses: string = index === 0 ? 'list-group-item d-flex' : 'list-group-item d-flex border-top-0';
-
-      const url = new URL('/_search', 'https://example.com');
-      url.searchParams.append('q', `tag:${tag.name}`);
+  const { pushState } = useKeywordManager();
 
+  const generateTagList = useCallback((tagData) => {
+    return tagData.map((tag:IDataTagCount) => {
       return (
-        <Link
+        <button
           key={tag._id}
-          href={`${url.pathname}${url.search}`}
-          className={tagListClasses}
-          prefetch={false}
+          type="button"
+          className="list-group-item list-group-item-action d-flex"
+          onClick={() => pushState(`tag:${tag.name}`)}
         >
           <div className="text-truncate list-tag-name">{tag.name}</div>
           <div className="ms-4 my-auto py-1 px-2 list-tag-count badge bg-primary">{tag.count}</div>
-        </Link>
+        </button>
       );
     });
-  }, []);
+  }, [pushState]);
 
   if (!isTagExist) {
     return <h3>{ t('You have no tag, You can set tags on pages') }</h3>;
@@ -56,9 +53,9 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
 
   return (
     <>
-      <ul className="list-group text-start mb-5">
+      <div className="list-group text-start mb-5">
         {generateTagList(tagData)}
-      </ul>
+      </div>
       {isPaginationShown
       && (
         <PaginationWrapper

+ 13 - 4
apps/app/src/stores/tag.tsx

@@ -1,19 +1,28 @@
-import { SWRResponse } from 'swr';
-import useSWRImmutable from 'swr/immutable';
+import useSWR, { SWRResponse } from 'swr';
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { IResTagsListApiv1, IResTagsSearchApiv1 } from '~/interfaces/tag';
 
 export const useSWRxTagsList = (limit?: number, offset?: number): SWRResponse<IResTagsListApiv1, Error> => {
-  return useSWRImmutable(
+  return useSWR(
     ['/tags.list', limit, offset],
     ([endpoint, limit, offset]) => apiGet(endpoint, { limit, offset }).then((result: IResTagsListApiv1) => result),
+    {
+      keepPreviousData: true,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
+    },
   );
 };
 
 export const useSWRxTagsSearch = (query: string): SWRResponse<IResTagsSearchApiv1, Error> => {
-  return useSWRImmutable(
+  return useSWR(
     ['/tags.search', query],
     ([endpoint, query]) => apiGet(endpoint, { q: query }).then((result: IResTagsSearchApiv1) => result),
+    {
+      keepPreviousData: true,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
+    },
   );
 };

+ 2 - 1
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--sticky-features.cy.ts

@@ -65,7 +65,8 @@ context('Access to any page', () => {
     });
     cy.waitUntil(() => {
       cy.getByTestid('grw-subnav-switcher').within(() => {
-        cy.getByTestid('editor-button').should('be.visible').click();
+        cy.getByTestid('editor-button').as('editorButton').should('be.visible');
+        cy.get('@editorButton').click();
       });
       return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
     });

+ 1 - 1
apps/slackbot-proxy/src/views/top.ejs

@@ -38,7 +38,7 @@
           </ul>
         <div class="mt-3">
           GROWI is open-source software developed by WESEEK, Inc and we are looking for contributors who can work with us.<br>
-          Please <a href="https://growi-slackin.weseek.co.jp/">join</a> Slack and feel free to talk to WESEEK members!
+          Please <a href="https://communityinviter.com/apps/wsgrowi/invite/">join</a> Slack and feel free to talk to WESEEK members!
         </div>
       </div>
     </div>