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

Merge branch 'master' into dev/6.2.x

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

+ 2 - 1
apps/app/package.json

@@ -263,6 +263,7 @@
     "socket.io-client": "^4.2.0",
     "source-map-loader": "^4.0.1",
     "swagger2openapi": "^5.3.1",
-    "tsc-alias": "^1.2.9"
+    "tsc-alias": "^1.2.9",
+    "fslightbox-react": "^1.7.6"
   }
 }

+ 4 - 0
apps/app/src/client/services/renderer/renderer.tsx

@@ -16,6 +16,7 @@ import type { Pluggable } from 'unified';
 
 import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents/DrawioViewerWithEditButton';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
+import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
 import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import * as mermaid from '~/features/mermaid';
@@ -113,6 +114,7 @@ export const generateViewOptions = (
     components.table = TableWithEditButton;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.img = LightBox;
   }
 
   if (config.isEnabledXssPrevention) {
@@ -218,6 +220,7 @@ export const generateSimpleViewOptions = (
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.img = LightBox;
   }
 
   if (config.isEnabledXssPrevention) {
@@ -295,6 +298,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.img = LightBox;
   }
 
   if (config.isEnabledXssPrevention) {

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss

@@ -64,7 +64,7 @@ $grw-bookmark-item-padding-left: 35px;
 
     .grw-bookmark-item-list{
       min-width: 30px;
-      height: 35px;
+      height: 50px;
 
       .picture {
         width: 16px;

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

@@ -162,7 +162,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
               validationTarget={ValidationTarget.PAGE}
             />
           )
-          : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle} />}
+          : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle} isNarrowView />}
 
         <div className='grw-foldertree-control'>
           <PageItemControl

+ 6 - 8
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -27,6 +27,7 @@ import {
 import {
   useSWRMUTxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound, useTemplateTagData, useSWRxPageInfo,
 } from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
   useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors,
@@ -282,12 +283,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     return;
   }, [mutatePageTagsForEditors]);
 
-  const reload = useCallback(() => {
-    if (currentPathname != null) {
-      router.push(currentPathname);
-    }
-  }, [currentPathname, router]);
-
   const duplicateItemClickedHandler = useCallback(async(page: IPageForPageDuplicateModal) => {
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
       router.push(toPath);
@@ -297,10 +292,12 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
   const renameItemClickedHandler = useCallback(async(page: IPageToRenameWithMeta<IPageInfoForEntity>) => {
     const renamedHandler: OnRenamedFunction = () => {
-      reload();
+      mutateCurrentPage();
+      mutatePageInfo();
+      mutatePageTree();
     };
     openRenameModal(page, { onRenamed: renamedHandler });
-  }, [openRenameModal, reload]);
+  }, [mutateCurrentPage, mutatePageInfo, openRenameModal]);
 
   const deleteItemClickedHandler = useCallback((pageWithMeta: IPageWithMeta) => {
     const deletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
@@ -320,6 +317,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
       mutateCurrentPage();
       mutatePageInfo();
+      mutatePageTree();
     };
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
   }, [currentPathname, mutateCurrentPage, openDeleteModal, router, mutatePageInfo]);

+ 0 - 1
apps/app/src/components/Page/RevisionRenderer.tsx

@@ -8,7 +8,6 @@ import loggerFactory from '~/utils/logger';
 
 import 'katex/dist/katex.min.css';
 
-
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
 type Props = {

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

@@ -190,6 +190,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
                       <NotAvailableForGuest>
                         <NotAvailableForReadOnlyUser>
                           <Button
+                            data-testid="comment-reply-button"
                             outline
                             color="secondary"
                             size="sm"

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

@@ -16,7 +16,12 @@ export const CommentControl = (props: CommentControlProps): JSX.Element => {
       <button type="button" className="btn btn-link p-2" onClick={onClickEditBtn}>
         <i className="ti ti-pencil"></i>
       </button>
-      <button type="button" className="btn btn-link p-2 mr-2" onClick={onClickDeleteBtn}>
+      <button
+        data-testid="comment-delete-button"
+        type="button"
+        className="btn btn-link p-2 mr-2"
+        onClick={onClickDeleteBtn}
+      >
         <i className="ti ti-close"></i>
       </button>
     </div>

+ 1 - 0
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -279,6 +279,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     );
     const submitButton = (
       <Button
+        data-testid="comment-submit-button"
         outline
         color="primary"
         className="btn btn-outline-primary rounded-pill"

+ 4 - 0
apps/app/src/components/PageList/PageListItemS.module.scss

@@ -0,0 +1,4 @@
+.page-title {
+  flex: 1;
+  line-height: 1.2;
+}

+ 18 - 4
apps/app/src/components/PageList/PageListItemS.tsx

@@ -4,18 +4,24 @@ import type { IPageHasId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
 import { PageListMeta, PagePathLabel } from '@growi/ui/dist/components/PagePath';
 import Link from 'next/link';
+import Clamp from 'react-multiline-clamp';
 
+import styles from './PageListItemS.module.scss';
 
 type PageListItemSProps = {
   page: IPageHasId,
   noLink?: boolean,
-  pageTitle?: string,
+  pageTitle?: string
+  isNarrowView?: boolean,
 }
 
 export const PageListItemS = (props: PageListItemSProps): JSX.Element => {
 
   const {
-    page, noLink = false, pageTitle,
+    page,
+    noLink = false,
+    pageTitle,
+    isNarrowView = false,
   } = props;
 
   const path = pageTitle != null ? pageTitle : page.path;
@@ -28,9 +34,17 @@ export const PageListItemS = (props: PageListItemSProps): JSX.Element => {
   return (
     <>
       <UserPicture user={page.lastUpdateUser} noLink={noLink} />
-      {pagePathElement}
+      {isNarrowView ? (
+        <Clamp lines={2}>
+          <div className={`mx-2 ${styles['page-title']} ${noLink ? 'text-break' : ''}`}>
+            {pagePathElement}
+          </div>
+        </Clamp>
+      ) : (
+        pagePathElement
+      )}
       <span className="ml-2">
-        <PageListMeta page={page} />
+        <PageListMeta page={page} shouldSpaceOutIcon />
       </span>
     </>
   );

+ 16 - 0
apps/app/src/components/ReactMarkdownComponents/LightBox.tsx

@@ -0,0 +1,16 @@
+import React, { useState } from 'react';
+
+import FsLightbox from 'fslightbox-react';
+
+export const LightBox = (props) => {
+  const [toggler, setToggler] = useState(false);
+  return (
+    <>
+      <img src={props.src} alt={props.alt} onClick={() => setToggler(!toggler)}/>
+      <FsLightbox
+        toggler={toggler}
+        sources={[props.src]}
+      />
+    </>
+  );
+};

+ 4 - 2
apps/app/src/components/SearchPage/SearchPageBase.tsx

@@ -17,6 +17,10 @@ import { mutatePageTree } from '~/stores/page-listing';
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
+// Do not import with next/dynamic
+// see: https://github.com/weseek/growi/pull/7923
+import { SearchResultList } from './SearchResultList';
+
 import styles from './SearchPageBase.module.scss';
 
 // https://regex101.com/r/brrkBu/1
@@ -41,8 +45,6 @@ type Props = {
   searchPager: React.ReactNode,
 }
 
-
-const SearchResultList = dynamic(() => import('./SearchResultList').then(mod => mod.SearchResultList), { ssr: false });
 const SearchResultContent = dynamic(() => import('./SearchResultContent').then(mod => mod.SearchResultContent), {
   ssr: false,
   loading: () => <></>,

+ 2 - 2
apps/app/src/components/Sidebar/SidebarNav.tsx

@@ -88,8 +88,8 @@ export const SidebarNav: FC<Props> = (props: Props) => {
   const { onItemSelected } = props;
 
   return (
-    <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`} data-vrt-blackout-sidebar-nav>
-      <div className="grw-sidebar-nav-primary-container">
+    <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`}>
+      <div className="grw-sidebar-nav-primary-container" data-vrt-blackout-sidebar-nav>
         {/* eslint-disable max-len */}
         <PrimaryItem contents={SidebarContentsType.TREE} label="Page Tree" iconName="format_list_bulleted" onItemSelected={onItemSelected} />
         <PrimaryItem contents={SidebarContentsType.CUSTOM} label="Custom Sidebar" iconName="code" onItemSelected={onItemSelected} />

+ 3 - 2
apps/app/src/features/questionnaire/server/service/questionnaire.ts

@@ -4,6 +4,7 @@ import * as os from 'node:os';
 import type { IUserHasId } from '@growi/core';
 
 import { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
+import { aclService } from '~/server/service/acl';
 
 import {
   GrowiWikiType, GrowiExternalAuthProviderType, IGrowiInfo, GrowiServiceType, GrowiAttachmentType, GrowiDeploymentType,
@@ -34,8 +35,8 @@ class QuestionnaireService {
     const currentUsersCount = await User.countDocuments();
     const currentActiveUsersCount = await User.countActiveUsers();
 
-    const wikiMode = this.crowi.configManager.getConfig('crowi', 'security:wikiMode');
-    const wikiType = wikiMode === 'private' ? GrowiWikiType.closed : GrowiWikiType.open;
+    const isGuestAllowedToRead = aclService.isGuestAllowedToRead();
+    const wikiType = isGuestAllowedToRead ? GrowiWikiType.open : GrowiWikiType.closed;
 
     const activeExternalAccountTypes: GrowiExternalAuthProviderType[] = Object.values(GrowiExternalAuthProviderType).filter((type) => {
       return this.crowi.configManager.getConfig('crowi', `security:passport-${type}:isEnabled`);

+ 9 - 7
apps/app/src/stores/context.tsx

@@ -1,5 +1,5 @@
 import type { ColorScheme, IUserHasId } from '@growi/core';
-import { SWRResponse } from 'swr';
+import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { SupportedActionType } from '~/interfaces/activity';
@@ -232,14 +232,16 @@ export const useIsReadOnlyUser = (): SWRResponse<boolean, Error> => {
 export const useIsAdmin = (): SWRResponse<boolean, Error> => {
   const { data: currentUser, isLoading } = useCurrentUser();
 
-  const isAdminUser = currentUser != null ? currentUser.admin : false;
-
-  return useSWRImmutable(
-    isLoading ? null : ['isAdminUser', currentUser?._id],
-    () => isAdminUser,
+  return useSWR(
+    isLoading ? null : ['isAdminUser', currentUser?._id, currentUser?.admin],
+    ([, , isAdmin]) => isAdmin ?? false,
     {
-      fallbackData: isAdminUser,
+      fallbackData: currentUser?.admin ?? false,
       keepPreviousData: true,
+      // disable all revalidation but revalidateIfStale
+      revalidateOnMount: false,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
   );
 };

+ 145 - 0
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--comments.cy.ts

@@ -0,0 +1,145 @@
+context('Comment', () => {
+  const ssPrefix = 'comments-';
+  let commentCount = 0;
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+
+    // visit page
+    cy.visit('/comment');
+    cy.collapseSidebar(true, true);
+  })
+
+  it('Create comment page', () => {
+    // save page
+    cy.get('#grw-page-editor-mode-manager').as('pageEditorModeManager').should('be.visible');
+    cy.waitUntil(() => {
+      // do
+      cy.get('@pageEditorModeManager').within(() => {
+        cy.get('button:nth-child(2)').click();
+      });
+      // until
+      return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
+    });
+    cy.get('.CodeMirror').should('be.visible');
+
+    cy.getByTestid('page-editor').should('be.visible');
+    cy.getByTestid('save-page-btn').click();
+  })
+
+  it('Successfully add comments', () => {
+    const commetText = 'add comment';
+
+    cy.getByTestid('page-comment-button').click();
+
+    // Open comment editor
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('open-comment-editor-button').click();
+      // wait until
+      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
+    });
+
+    cy.get('.CodeMirror').type(commetText);
+    cy.getByTestid("comment-submit-button").eq(0).click();
+
+    // Check update comment count
+    commentCount += 1
+    cy.getByTestid('page-comment-button').contains(commentCount);
+    cy.screenshot(`${ssPrefix}1-add-comments`);
+  });
+
+  it('Successfully reply comments', () => {
+    const commetText = 'reply comment';
+
+    cy.getByTestid('page-comment-button').click();
+
+    // Open reply comment editor
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('comment-reply-button').eq(0).click();
+      // wait until
+      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
+    });
+
+    cy.get('.CodeMirror').type(commetText);
+    cy.getByTestid("comment-submit-button").eq(0).click();
+
+    // Check update comment count
+    commentCount += 1
+    cy.getByTestid('page-comment-button').contains(commentCount);
+    cy.screenshot(`${ssPrefix}2-reply-comments`);
+  });
+
+  it('Successfully delete comments', () => {
+
+    cy.getByTestid('page-comment-button').click();
+
+    cy.get('.page-comments').should('be.visible');
+    cy.getByTestid('comment-delete-button').eq(0).click({force: true});
+    cy.get('.modal-content').then($elem => $elem.is(':visible'));
+    cy.get('.modal-footer > button:nth-child(3)').click();
+
+    // Check update comment count
+    commentCount -= 2
+    cy.getByTestid('page-comment-button').contains(commentCount);
+    cy.screenshot(`${ssPrefix}3-delete-comments`);
+  });
+
+  // Mention username in comment
+  it('Successfully mention username in comment', () => {
+    const username = '@adm';
+
+    cy.getByTestid('page-comment-button').click();
+
+    // Open comment editor
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('open-comment-editor-button').click();
+      // wait until
+      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
+    });
+
+    cy.waitUntil(() => {
+      // do
+      cy.get('.CodeMirror').type(username);
+      // wait until
+      return cy.get('.CodeMirror-hints').then($elem => $elem.is(':visible'));
+    });
+
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}4-mention-username-found`) });
+    // Click on mentioned username
+    cy.get('.CodeMirror-hints > li').first().click();
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}5-mention-username-mentioned`) });
+  });
+
+  it('Username not found when mention username in comment', () => {
+    const username = '@user';
+
+    cy.getByTestid('page-comment-button').click();
+
+    // Open comment editor
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('open-comment-editor-button').click();
+      // wait until
+      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
+    });
+
+    cy.waitUntil(() => {
+      // do
+      cy.get('.CodeMirror').type(username);
+      // wait until
+      return cy.get('.CodeMirror-hints').then($elem => $elem.is(':visible'));
+    });
+
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}6-mention-username-not-found`) });
+    // Click on username not found hint
+    cy.get('.CodeMirror-hints > li').first().click();
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}7-mention-no-username-mentioned`) });
+  });
+
+})

+ 0 - 61
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--username-mention.cy.ts

@@ -1,61 +0,0 @@
-context('Mention username in comment', () => {
-  const ssPrefix = 'mention-username-';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-
-    // Visit /Sandbox
-    cy.visit('/Sandbox');
-    cy.waitUntilSkeletonDisappear();
-
-    cy.collapseSidebar(true, true);
-
-    // Go to comment page
-    cy.getByTestid('page-comment-button').click();
-
-    // Open comment editor
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('open-comment-editor-button').click();
-      // wait until
-      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
-    });
-
-  });
-
-  it('Successfully mention username in comment', () => {
-    const username = '@adm';
-
-    cy.waitUntil(() => {
-      // do
-      cy.get('.CodeMirror').type(username);
-      // wait until
-      return cy.get('.CodeMirror-hints').then($elem => $elem.is(':visible'));
-    });
-
-    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}1-username-found`) });
-    // Click on mentioned username
-    cy.get('.CodeMirror-hints > li').first().click();
-    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}2-username-mentioned`) });
-  });
-
-  it('Username not found when mention username in comment', () => {
-    const username = '@user';
-
-    cy.waitUntil(() => {
-      // do
-      cy.get('.CodeMirror').type(username);
-      // wait until
-      return cy.get('.CodeMirror-hints').then($elem => $elem.is(':visible'));
-    });
-
-    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}3-username-not-found`) });
-    // Click on username not found hint
-    cy.get('.CodeMirror-hints > li').first().click();
-    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}4-no-username-mentioned`) });
-  });
-
-});

+ 1 - 1
apps/app/test/integration/service/questionnaire.test.ts

@@ -53,7 +53,7 @@ describe('QuestionnaireService', () => {
         deploymentType: 'growi-docker-compose',
         type: 'on-premise',
         version: crowi.version,
-        wikiType: 'open',
+        wikiType: 'closed',
       });
     });
 

+ 8 - 8
packages/ui/src/components/PagePath/PageListMeta.tsx

@@ -46,7 +46,7 @@ const SeenUsersCount = (props: SeenUsersCountProps): JSX.Element => {
   const strengthClass = `strength-${strengthLevel}`; // strength-{0, 1, 2, 3, 4}
 
   return (
-    <span className={`seen-users-count ${shouldSpaceOutIcon ? 'mr-3' : ''} ${strengthClass}`}>
+    <span className={`seen-users-count ${shouldSpaceOutIcon ? 'mr-2' : ''} ${strengthClass}`}>
       <i className="footstamp-icon"><FootstampIcon /></i>
       {count}
     </span>
@@ -70,40 +70,40 @@ export const PageListMeta: FC<PageListMetaProps> = (props: PageListMetaProps) =>
   // top check
   let topLabel;
   if (isTopPage(page.path)) {
-    topLabel = <span className={`badge badge-info ${shouldSpaceOutIcon ? 'mr-3' : ''} top-label`}>TOP</span>;
+    topLabel = <span className={`badge badge-info ${shouldSpaceOutIcon ? 'mr-2' : ''} top-label`}>TOP</span>;
   }
 
   // template check
   let templateLabel;
   if (checkTemplatePath(page.path)) {
-    templateLabel = <span className={`badge badge-info ${shouldSpaceOutIcon ? 'mr-3' : ''}`}>TMPL</span>;
+    templateLabel = <span className={`badge badge-info ${shouldSpaceOutIcon ? 'mr-2' : ''}`}>TMPL</span>;
   }
 
   let commentCount;
   if (page.commentCount > 0) {
-    commentCount = <span className={`${shouldSpaceOutIcon ? 'mr-3' : ''}`}><i className="icon-bubble" />{page.commentCount}</span>;
+    commentCount = <span className={`${shouldSpaceOutIcon ? 'mr-2' : ''}`}><i className="icon-bubble" />{page.commentCount}</span>;
   }
 
   let likerCount;
   if (props.likerCount != null && props.likerCount > 0) {
-    likerCount = <span className={`${shouldSpaceOutIcon ? 'mr-3' : ''}`}><i className="fa fa-heart-o" />{props.likerCount}</span>;
+    likerCount = <span className={`${shouldSpaceOutIcon ? 'mr-2' : ''}`}><i className="fa fa-heart-o" />{props.likerCount}</span>;
   }
 
   let locked;
   if (page.grant !== 1) {
-    locked = <span className={`${shouldSpaceOutIcon ? 'mr-3' : ''}`}><i className="icon-lock" /></span>;
+    locked = <span className={`${shouldSpaceOutIcon ? 'mr-2' : ''}`}><i className="icon-lock" /></span>;
   }
 
   let bookmarkCount;
   if (props.bookmarkCount != null && props.bookmarkCount > 0) {
-    bookmarkCount = <span className={`${shouldSpaceOutIcon ? 'mr-3' : ''}`}><i className="fa fa-bookmark-o" />{props.bookmarkCount}</span>;
+    bookmarkCount = <span className={`${shouldSpaceOutIcon ? 'mr-2' : ''}`}><i className="fa fa-bookmark-o" />{props.bookmarkCount}</span>;
   }
 
   return (
     <span className="page-list-meta">
       {topLabel}
       {templateLabel}
-      <SeenUsersCount count={page.seenUsers.length} basisViewersCount={basisViewersCount} />
+      <SeenUsersCount count={page.seenUsers.length} basisViewersCount={basisViewersCount} shouldSpaceOutIcon={shouldSpaceOutIcon} />
       {commentCount}
       {likerCount}
       {locked}

+ 5 - 0
yarn.lock

@@ -8483,6 +8483,11 @@ fsevents@^2.3.2, fsevents@~2.3.2:
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
   integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
 
+fslightbox-react@^1.7.6:
+  version "1.7.6"
+  resolved "https://registry.yarnpkg.com/fslightbox-react/-/fslightbox-react-1.7.6.tgz#eb9565e1f836b647cdbdf4734705222ca542dbd3"
+  integrity sha512-7LN2GZRLHo2vZGKdH+BZDJUoUDkCRCLlZ5hOwtLtZplmGZQ9nqzpG54cTax7XNjbYGTWLT6BHdMiL5zOEhiRlA==
+
 fstream@^1.0.12:
   version "1.0.12"
   resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"