Przeglądaj źródła

Merge pull request #15150 from overleaf/mf-file-view-tpr-module

Move mendeley/zotero file-view UI to `tpr-webmodule` folder

GitOrigin-RevId: af3cfe614fcf415d5842cf98dc2a42a3898ccd8b
M Fahru 2 lat temu
rodzic
commit
4556675ad2

+ 4 - 2
services/web/config/settings.defaults.js

@@ -827,8 +827,10 @@ module.exports = {
     createFileModes: [],
     gitBridge: [],
     publishModal: [],
-    tprLinkedFileInfo: [],
-    tprLinkedFileRefreshError: [],
+    tprFileViewInfo: [],
+    tprFileViewRefreshError: [],
+    tprFileViewRefreshButton: [],
+    tprFileViewNotOriginalImporter: [],
     contactUsModal: [],
     editorToolbarButtons: [],
     sourceEditorExtensions: [],

+ 17 - 6
services/web/frontend/js/features/file-view/components/file-view-header.tsx

@@ -12,11 +12,17 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac
 import { LinkedFileIcon } from './file-view-icons'
 import { BinaryFile, hasProvider, LinkedFile } from '../types/binary-file'
 import FileViewRefreshButton from './file-view-refresh-button'
-import FileViewNotOriginalImporter from './file-view-not-original-importer'
 import FileViewRefreshError from './file-view-refresh-error'
 
-const tprLinkedFileInfo = importOverleafModules('tprLinkedFileInfo') as {
-  import: { LinkedFileInfo: ElementType }
+const tprFileViewInfo = importOverleafModules('tprFileViewInfo') as {
+  import: { TPRFileViewInfo: ElementType }
+  path: string
+}[]
+
+const tprFileViewNotOriginalImporter = importOverleafModules(
+  'tprFileViewNotOriginalImporter'
+) as {
+  import: { TPRFileViewNotOriginalImporter: ElementType }
   path: string
 }[]
 
@@ -79,8 +85,8 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
     <div>
       {file.linkedFileData && fileInfo}
       {file.linkedFileData &&
-        tprLinkedFileInfo.map(({ import: { LinkedFileInfo }, path }) => (
-          <LinkedFileInfo key={path} file={file} />
+        tprFileViewInfo.map(({ import: { TPRFileViewInfo }, path }) => (
+          <TPRFileViewInfo key={path} file={file} />
         ))}
       {file.linkedFileData && permissionsLevel !== 'readOnly' && (
         <FileViewRefreshButton file={file} setRefreshError={setRefreshError} />
@@ -95,7 +101,12 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
         &nbsp;
         <span>{t('download')}</span>
       </a>
-      {file.linkedFileData && <FileViewNotOriginalImporter file={file} />}
+      {file.linkedFileData &&
+        tprFileViewNotOriginalImporter.map(
+          ({ import: { TPRFileViewNotOriginalImporter }, path }) => (
+            <TPRFileViewNotOriginalImporter key={path} file={file} />
+          )
+        )[0]}
       {refreshError && (
         <FileViewRefreshError file={file} refreshError={refreshError} />
       )}

+ 0 - 38
services/web/frontend/js/features/file-view/components/file-view-not-original-importer.tsx

@@ -1,38 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { capitalize } from 'lodash'
-import { useUserContext } from '@/shared/context/user-context'
-import { BinaryFile, hasProvider } from '../types/binary-file'
-
-type FileViewNotOriginalImporterProps = {
-  file: BinaryFile
-}
-
-export default function FileViewNotOriginalImporter({
-  file,
-}: FileViewNotOriginalImporterProps) {
-  const { t } = useTranslation()
-  const { id: userId } = useUserContext()
-
-  const isMendeleyOrZotero =
-    hasProvider(file, 'mendeley') || hasProvider(file, 'zotero')
-
-  if (!isMendeleyOrZotero) {
-    return null
-  }
-
-  const isImporter = file.linkedFileData.importer_id === userId
-
-  if (isImporter) {
-    return null
-  }
-
-  return (
-    <div className="row">
-      <div className="alert">
-        {t('only_importer_can_refresh', {
-          provider: capitalize(file.linkedFileData.provider),
-        })}
-      </div>
-    </div>
-  )
-}

+ 64 - 38
services/web/frontend/js/features/file-view/components/file-view-refresh-button.tsx

@@ -1,73 +1,99 @@
 import {
   type Dispatch,
   type SetStateAction,
+  type ElementType,
   useCallback,
   useState,
 } from 'react'
 import { useTranslation } from 'react-i18next'
-import classNames from 'classnames'
 import Icon from '@/shared/components/icon'
 import { postJSON } from '@/infrastructure/fetch-json'
 import { useProjectContext } from '@/shared/context/project-context'
 import useAbortController from '@/shared/hooks/use-abort-controller'
-import { useUserContext } from '@/shared/context/user-context'
-import { hasProvider, type BinaryFile } from '../types/binary-file'
+import type { BinaryFile } from '../types/binary-file'
 import { Nullable } from '../../../../../types/utils'
+import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
 
 type FileViewRefreshButtonProps = {
   setRefreshError: Dispatch<SetStateAction<Nullable<string>>>
   file: BinaryFile
 }
 
+const tprFileViewRefreshButton = importOverleafModules(
+  'tprFileViewRefreshButton'
+) as {
+  import: { TPRFileViewRefreshButton: ElementType }
+  path: string
+}[]
+
 export default function FileViewRefreshButton({
   setRefreshError,
   file,
 }: FileViewRefreshButtonProps) {
+  const { _id: projectId } = useProjectContext()
   const { signal } = useAbortController()
-  const { t } = useTranslation()
   const [refreshing, setRefreshing] = useState(false)
-  const { _id: projectId } = useProjectContext()
-  const { id: userId } = useUserContext()
-
-  const isMendeleyOrZotero =
-    hasProvider(file, 'mendeley') || hasProvider(file, 'zotero')
 
-  let isImporter
+  const refreshFile = useCallback(
+    (isTPR: Nullable<boolean>) => {
+      setRefreshing(true)
+      // Replacement of the file handled by the file tree
+      window.expectingLinkedFileRefreshedSocketFor = file.name
+      const body = {
+        shouldReindexReferences: isTPR || /\.bib$/.test(file.name),
+      }
+      postJSON(`/project/${projectId}/linked_file/${file.id}/refresh`, {
+        signal,
+        body,
+      })
+        .then(() => {
+          setRefreshing(false)
+        })
+        .catch(err => {
+          setRefreshing(false)
+          setRefreshError(err.data?.message || err.message)
+        })
+    },
+    [file, projectId, signal, setRefreshError]
+  )
 
-  if (isMendeleyOrZotero) {
-    isImporter = file.linkedFileData.importer_id === userId
+  if (tprFileViewRefreshButton.length > 0) {
+    return tprFileViewRefreshButton.map(
+      ({ import: { TPRFileViewRefreshButton }, path }) => (
+        <TPRFileViewRefreshButton
+          key={path}
+          file={file}
+          refreshFile={refreshFile}
+          refreshing={refreshing}
+        />
+      )
+    )[0]
+  } else {
+    return (
+      <FileViewRefreshButtonDefault
+        refreshFile={refreshFile}
+        refreshing={refreshing}
+      />
+    )
   }
+}
 
-  const buttonClickable = isMendeleyOrZotero ? isImporter : true
+type FileViewRefreshButtonDefaultProps = {
+  refreshFile: (isTPR: Nullable<boolean>) => void
+  refreshing: boolean
+}
 
-  const refreshFile = useCallback(() => {
-    setRefreshing(true)
-    // Replacement of the file handled by the file tree
-    window.expectingLinkedFileRefreshedSocketFor = file.name
-    const body = {
-      shouldReindexReferences: isMendeleyOrZotero || /\.bib$/.test(file.name),
-    }
-    postJSON(`/project/${projectId}/linked_file/${file.id}/refresh`, {
-      signal,
-      body,
-    })
-      .then(() => {
-        setRefreshing(false)
-      })
-      .catch(err => {
-        setRefreshing(false)
-        setRefreshError(err.data?.message || err.message)
-      })
-  }, [file, projectId, signal, setRefreshError, isMendeleyOrZotero])
+function FileViewRefreshButtonDefault({
+  refreshFile,
+  refreshing,
+}: FileViewRefreshButtonDefaultProps) {
+  const { t } = useTranslation()
 
   return (
     <button
-      className={classNames('btn', {
-        'btn-primary': buttonClickable,
-        'btn-secondary': !buttonClickable,
-      })}
-      onClick={refreshFile}
-      disabled={refreshing || !buttonClickable}
+      className="btn btn-primary"
+      onClick={() => refreshFile(null)}
+      disabled={refreshing}
     >
       <Icon type="refresh" spin={refreshing} fw />
       <span>{refreshing ? `${t('refreshing')}…` : t('refresh')}</span>

+ 28 - 71
services/web/frontend/js/features/file-view/components/file-view-refresh-error.tsx

@@ -1,87 +1,44 @@
 import type { ElementType } from 'react'
 import { useTranslation } from 'react-i18next'
 import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
-import { BinaryFile, hasProvider } from '../types/binary-file'
-
-const tprLinkedFileRefreshError = importOverleafModules(
-  'tprLinkedFileRefreshError'
-) as {
-  import: { LinkedFileRefreshError: ElementType }
-  path: string
-}[]
+import { BinaryFile } from '../types/binary-file'
 
 type FileViewRefreshErrorProps = {
   file: BinaryFile
   refreshError: string
 }
 
+const tprFileViewRefreshError = importOverleafModules(
+  'tprFileViewRefreshError'
+) as {
+  import: { TPRFileViewRefreshError: ElementType }
+  path: string
+}[]
+
 export default function FileViewRefreshError({
   file,
   refreshError,
 }: FileViewRefreshErrorProps) {
-  const isMendeleyOrZotero =
-    hasProvider(file, 'mendeley') || hasProvider(file, 'zotero')
-
-  return (
-    <div className="row">
-      <br />
-
-      {isMendeleyOrZotero ? (
-        <FileViewMendeleyOrZoteroRefreshError file={file} />
-      ) : (
-        <FileViewDefaultRefreshError refreshError={refreshError} />
-      )}
-    </div>
-  )
-}
-
-type FileViewMendeleyOrZoteroRefreshErrorProps = {
-  file: BinaryFile
-}
-
-function FileViewMendeleyOrZoteroRefreshError({
-  file,
-}: FileViewMendeleyOrZoteroRefreshErrorProps) {
   const { t } = useTranslation()
-  return (
-    <div
-      className="alert alert-danger col-md-10 col-md-offset-1"
-      style={{ display: 'flex', alignItems: 'center', gap: '10px' }}
-    >
-      <div>
-        {t('something_not_right')}!&nbsp;
-        {tprLinkedFileRefreshError.map(
-          ({ import: { LinkedFileRefreshError }, path }) => (
-            <LinkedFileRefreshError key={path} file={file} />
-          )
-        )}
-      </div>
-      <div className="text-center">
-        <button className="btn btn-danger">
-          <a
-            href="/user/settings"
-            target="_blank"
-            style={{ fontWeight: 'bold', textDecoration: 'none' }}
-          >
-            {t('go_to_settings')}
-          </a>
-        </button>
-      </div>
-    </div>
-  )
-}
 
-type FileViewDefaultRefreshErrorProps = {
-  refreshError: string
-}
-
-function FileViewDefaultRefreshError({
-  refreshError,
-}: FileViewDefaultRefreshErrorProps) {
-  const { t } = useTranslation()
-  return (
-    <div className="alert alert-danger col-md-6 col-md-offset-3">
-      {t('access_denied')}: {refreshError}
-    </div>
-  )
+  if (tprFileViewRefreshError.length > 0) {
+    return tprFileViewRefreshError.map(
+      ({ import: { TPRFileViewRefreshError }, path }) => (
+        <TPRFileViewRefreshError
+          key={path}
+          file={file}
+          refreshError={refreshError}
+        />
+      )
+    )[0]
+  } else {
+    return (
+      <div className="row">
+        <br />
+        <div className="alert alert-danger col-md-6 col-md-offset-3">
+          {t('access_denied')}: {refreshError}
+        </div>
+      </div>
+    )
+  }
 }

+ 1 - 9
services/web/frontend/js/features/file-view/types/binary-file.ts

@@ -1,16 +1,8 @@
-type LinkedFileData = {
+export type LinkedFileData = {
   url: {
     provider: 'url'
     url: string
   }
-  zotero: {
-    provider: 'zotero'
-    importer_id: string
-  }
-  mendeley: {
-    provider: 'mendeley'
-    importer_id: string
-  }
   project_file: {
     provider: 'project_file'
     v1_source_doc_id?: string

+ 3 - 47
services/web/frontend/stories/file-view.stories.jsx → services/web/frontend/stories/file-view/file-view.stories.jsx

@@ -1,6 +1,6 @@
-import FileView from '../js/features/file-view/components/file-view'
-import useFetchMock from './hooks/use-fetch-mock'
-import { ScopeDecorator } from './decorators/scope'
+import FileView from '../../js/features/file-view/components/file-view'
+import useFetchMock from '../hooks/use-fetch-mock'
+import { ScopeDecorator } from '../decorators/scope'
 
 const bodies = {
   latex: `\\documentclass{article}
@@ -161,50 +161,6 @@ ImageFile.args = {
   },
 }
 
-export const ThirdPartyReferenceFile = args => {
-  useFetchMock(fetchMock =>
-    setupFetchMock(fetchMock).get(
-      'express:/project/:project_id/file/:file_id',
-      { body: bodies.bibtex }
-    )
-  )
-
-  return <FileView {...args} />
-}
-
-ThirdPartyReferenceFile.args = {
-  file: {
-    ...fileData,
-    name: 'references.bib',
-    linkedFileData: {
-      provider: 'zotero',
-    },
-  },
-}
-
-export const ThirdPartyReferenceFileWithError = args => {
-  useFetchMock(fetchMock =>
-    setupFetchMock(fetchMock).head(
-      'express:/project/:project_id/file/:file_id',
-      { status: 500 },
-      { overwriteRoutes: true }
-    )
-  )
-  return <FileView {...args} />
-}
-ThirdPartyReferenceFileWithError.storyName =
-  'Third Party Reference File (Error)'
-ThirdPartyReferenceFileWithError.args = {
-  file: {
-    ...fileData,
-    id: '500500500500500500500500',
-    name: 'references.bib',
-    linkedFileData: {
-      provider: 'zotero',
-    },
-  },
-}
-
 export const TextFile = args => {
   useFetchMock(fetchMock =>
     setupFetchMock(fetchMock).get(

+ 0 - 25
services/web/frontend/stories/linked-file.stories.jsx

@@ -1,25 +0,0 @@
-import { LinkedFileInfo } from '../../modules/tpr-webmodule/frontend/js/components/linked-file-info'
-
-export const MendeleyLinkedFile = args => {
-  return <LinkedFileInfo {...args} />
-}
-
-MendeleyLinkedFile.args = {
-  file: {
-    linkedFileData: {
-      provider: 'mendeley',
-    },
-  },
-}
-
-export default {
-  title: 'Editor / LinkedFileInfo',
-  component: LinkedFileInfo,
-  args: {
-    file: {
-      id: 'file-id',
-      name: 'file.tex',
-      created: new Date(),
-    },
-  },
-}

+ 0 - 108
services/web/test/frontend/features/file-view/components/file-view-not-original-importer.test.tsx

@@ -1,108 +0,0 @@
-import { screen } from '@testing-library/react'
-import { expect } from 'chai'
-import FileViewNotOriginalImporter from '@/features/file-view/components/file-view-not-original-importer'
-import { BinaryFile } from '@/features/file-view/types/binary-file'
-import { renderWithEditorContext } from '../../../helpers/render-with-context'
-import { USER_ID } from '../../../helpers/editor-providers'
-
-describe('<FileViewNotOriginalImporter />', function () {
-  describe('provider is not mendeley and zotero', function () {
-    it('does not show error if provider is not mendeley and zotero', function () {
-      const urlFile: BinaryFile<'url'> = {
-        id: '123abc',
-        _id: '123abc',
-        linkedFileData: {
-          provider: 'url',
-          url: '/url/file.png',
-        },
-        created: new Date(2023, 1, 17, 3, 24),
-        name: 'file.png',
-        type: 'file',
-        selected: true,
-      }
-
-      renderWithEditorContext(<FileViewNotOriginalImporter file={urlFile} />)
-
-      const text = screen.queryByText(
-        /Only the person who originally imported this/
-      )
-
-      expect(text).to.not.exist
-    })
-  })
-
-  describe('provider is mendeley or zotero', function () {
-    it('does not show error if current user is the original importer of the file', function () {
-      const mendeleyFile: BinaryFile<'mendeley'> = {
-        id: '123abc',
-        _id: '123abc',
-        linkedFileData: {
-          provider: 'mendeley',
-          importer_id: USER_ID,
-        },
-        created: new Date(2023, 1, 17, 3, 24),
-        name: 'references.bib',
-        type: 'file',
-        selected: true,
-      }
-
-      renderWithEditorContext(
-        <FileViewNotOriginalImporter file={mendeleyFile} />
-      )
-
-      const text = screen.queryByText(
-        'Only the person who originally imported this Mendeley file can refresh it.'
-      )
-
-      expect(text).to.not.exist
-    })
-
-    it('shows error if provider is mendeley and current user is not the original importer of the file', function () {
-      const mendeleyFile: BinaryFile<'mendeley'> = {
-        id: '123abc',
-        _id: '123abc',
-        linkedFileData: {
-          provider: 'mendeley',
-          importer_id: 'user123',
-        },
-        created: new Date(2023, 1, 17, 3, 24),
-        name: 'references.bib',
-        type: 'file',
-        selected: true,
-      }
-
-      renderWithEditorContext(
-        <FileViewNotOriginalImporter file={mendeleyFile} />
-      )
-
-      const text = screen.getByText(
-        'Only the person who originally imported this Mendeley file can refresh it.'
-      )
-
-      expect(text).to.exist
-    })
-
-    it('shows error if provider is zotero and current user is not the original importer of the file', function () {
-      const zoteroFile: BinaryFile<'zotero'> = {
-        id: '123abc',
-        _id: '123abc',
-        linkedFileData: {
-          provider: 'zotero',
-          importer_id: 'user123',
-        },
-        created: new Date(2023, 1, 17, 3, 24),
-        name: 'references.bib',
-        type: 'file',
-        selected: true,
-      }
-
-      renderWithEditorContext(<FileViewNotOriginalImporter file={zoteroFile} />)
-
-      const text = screen.getByText(
-        'Only the person who originally imported this Zotero file can refresh it.'
-      )
-
-      expect(text).to.exist
-    })
-  })
-})

+ 0 - 58
services/web/test/frontend/features/file-view/components/file-view-refresh-button.test.jsx

@@ -3,7 +3,6 @@ import {
   fireEvent,
   waitForElementToBeRemoved,
 } from '@testing-library/react'
-import { expect } from 'chai'
 import fetchMock from 'fetch-mock'
 import FileViewRefreshButton from '@/features/file-view/components/file-view-refresh-button'
 import { renderWithEditorContext } from '../../../helpers/render-with-context'
@@ -22,27 +21,6 @@ describe('<FileViewRefreshButton />', function () {
     created: new Date(2021, 1, 17, 3, 24).toISOString(),
   }
 
-  const thirdPartyReferenceFile = {
-    name: 'example.tex',
-    linkedFileData: {
-      provider: 'zotero',
-      importer_id: USER_ID,
-    },
-    created: new Date(2021, 1, 17, 3, 24).toISOString(),
-  }
-
-  const thirdPartyNotOriginalImporterReferenceFile = {
-    name: 'references.bib',
-    linkedFileData: {
-      v1_source_doc_id: 'v1-source-id',
-      source_project_id: 'source-project-id',
-      source_entity_path: '/source-entity-path.ext',
-      provider: 'mendeley',
-      importer_id: '123abc',
-    },
-    created: new Date(2021, 1, 17, 3, 24).toISOString(),
-  }
-
   beforeEach(function () {
     fetchMock.reset()
   })
@@ -65,40 +43,4 @@ describe('<FileViewRefreshButton />', function () {
 
     await screen.findByText('Refresh')
   })
-
-  it('Reindexes references after refreshing a file from a third-party provider', async function () {
-    fetchMock.post(
-      'express:/project/:project_id/linked_file/:file_id/refresh',
-      {
-        new_file_id: '5ff7418157b4e144321df5c4',
-      }
-    )
-
-    renderWithEditorContext(
-      <FileViewRefreshButton file={thirdPartyReferenceFile} />
-    )
-
-    fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
-
-    await waitForElementToBeRemoved(() =>
-      screen.getByText('Refreshing', { exact: false })
-    )
-
-    expect(fetchMock.done()).to.be.true
-
-    const lastCallBody = JSON.parse(fetchMock.lastCall()[1].body)
-    expect(lastCallBody.shouldReindexReferences).to.be.true
-  })
-
-  it('is disabled when user is not original importer', function () {
-    renderWithEditorContext(
-      <FileViewRefreshButton
-        file={thirdPartyNotOriginalImporterReferenceFile}
-      />
-    )
-
-    const button = screen.getByRole('button', { name: 'Refresh' })
-
-    expect(button.disabled).to.equal(true)
-  })
 })

+ 23 - 89
services/web/test/frontend/features/file-view/components/file-view-refresh-error.test.tsx

@@ -1,96 +1,30 @@
 import { render, screen } from '@testing-library/react'
 import FileViewRefreshError from '@/features/file-view/components/file-view-refresh-error'
 import type { BinaryFile } from '@/features/file-view/types/binary-file'
-import { expect } from 'chai'
 
 describe('<FileViewRefreshError />', function () {
-  describe('<FileViewMendeleyOrZoteroRefreshError />', function () {
-    it('shows correct error message for mendeley', async function () {
-      const mendeleyFile: BinaryFile<'mendeley'> = {
-        id: '123abc',
-        _id: '123abc',
-        linkedFileData: {
-          provider: 'mendeley',
-          importer_id: 'user123456',
-        },
-        created: new Date(2023, 1, 17, 3, 24),
-        name: 'references.bib',
-        type: 'file',
-        selected: true,
-      }
-
-      render(
-        <FileViewRefreshError
-          file={mendeleyFile}
-          refreshError="error_message"
-        />
-      )
-
-      screen.getByText(/Something’s not right!/)
-      screen.getByText(
-        /It looks like you need to re-link your Mendeley account./
-      )
-
-      const goToSettingsLink = screen.getByRole('link', {
-        name: 'Go to settings',
-      })
-
-      expect(goToSettingsLink.getAttribute('href')).to.equal('/user/settings')
-    })
-
-    it('shows correct error message for zotero', async function () {
-      const zoteroFile: BinaryFile<'zotero'> = {
-        id: '123abc',
-        _id: '123abc',
-        linkedFileData: {
-          provider: 'zotero',
-          importer_id: 'user123456',
-        },
-        created: new Date(2023, 1, 17, 3, 24),
-        name: 'references.bib',
-        type: 'file',
-        selected: true,
-      }
-
-      render(
-        <FileViewRefreshError file={zoteroFile} refreshError="error_message" />
-      )
-
-      screen.getByText(/Something’s not right!/)
-      screen.getByText(/It looks like you need to re-link your Zotero account./)
-
-      const goToSettingsLink = screen.getByRole('link', {
-        name: 'Go to settings',
-      })
-
-      expect(goToSettingsLink.getAttribute('href')).to.equal('/user/settings')
-    })
-  })
-
-  describe('<FileViewDefaultRefreshError />', function () {
-    it('shows correct error message', function () {
-      const anotherProjectFile: BinaryFile<'project_file'> = {
-        id: '123abc',
-        _id: '123abc',
-        linkedFileData: {
-          provider: 'project_file',
-          source_project_id: 'some-id',
-          source_entity_path: '/path/',
-        },
-        created: new Date(2023, 1, 17, 3, 24),
-        name: 'frog.jpg',
-        type: 'file',
-        selected: true,
-      }
-
-      render(
-        <FileViewRefreshError
-          file={anotherProjectFile}
-          refreshError="An error message"
-        />
-      )
-
-      screen.getByText('Access Denied: An error message')
-    })
+  it('shows correct error message', function () {
+    const anotherProjectFile: BinaryFile<'project_file'> = {
+      id: '123abc',
+      _id: '123abc',
+      linkedFileData: {
+        provider: 'project_file',
+        source_project_id: 'some-id',
+        source_entity_path: '/path/',
+      },
+      created: new Date(2023, 1, 17, 3, 24),
+      name: 'frog.jpg',
+      type: 'file',
+      selected: true,
+    }
+
+    render(
+      <FileViewRefreshError
+        file={anotherProjectFile}
+        refreshError="An error message"
+      />
+    )
+
+    screen.getByText('Access Denied: An error message')
   })
 })