Skip to content

Commit

Permalink
feat: Open note inside public repository
Browse files Browse the repository at this point in the history
The opening of the notes is subtle. The note is always opened in the instance of the note creator. This is especially true for shared notes or notes in shared folders. In this case, you need to contact the cozy-stack to find out where to open it. As this can take a little time, we use a redirection page rather than pre-calculating all the links when a folder is displayed
  • Loading branch information
cballevre committed Nov 18, 2024
1 parent d9f9398 commit c624f57
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 15 deletions.
8 changes: 8 additions & 0 deletions src/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ declare module 'cozy-client/dist/models/file' {
) => boolean
}

declare module 'cozy-client/dist/models/note' {
export const fetchURL: (
client: import('cozy-client/types/CozyClient').CozyClient,
file: { id: string },
options: { pathname: string }
) => Promise<string>
}

declare module '*.svg' {
import { FC, SVGProps } from 'react'
const content: FC<SVGProps<SVGElement>>
Expand Down
6 changes: 6 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -848,5 +848,11 @@
},
"LastUpdate": {
"titleFormat": "MMMM DD, YYYY, HH:MM"
},
"PublicNoteRedirect": {
"error": {
"title": "Unable to access document",
"subtitle": "The share link appears to be missing or invalid. Please ask the document owner to check access"
}
}
}
6 changes: 6 additions & 0 deletions src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -849,5 +849,11 @@
},
"LastUpdate": {
"titleFormat": "DD MMMM YYYY, HH:MM"
},
"PublicNoteRedirect": {
"error": {
"title": "Impossible d'accéder au document",
"subtitle": "Le lien de partage semble manquant ou invalide. Merci de demander au propriétaire du document de vérifier les accès"
}
}
}
3 changes: 3 additions & 0 deletions src/modules/navigation/AppRoute.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ROOT_DIR_ID, TRASH_DIR_ID } from 'constants/config'
import { SentryRoutes } from 'lib/sentry'
import { UploaderComponent } from 'modules//views/Upload/UploaderComponent'
import Layout from 'modules/layout/Layout'
import { PublicNoteRedirect } from 'modules/navigation/PublicNoteRedirect'
import FileOpenerExternal from 'modules/viewer/FileOpenerExternal'
import HarvestRoutes from 'modules/views/Drive/HarvestRoutes'
import { SharedDrivesFolderView } from 'modules/views/Drive/SharedDrivesFolderView'
Expand Down Expand Up @@ -50,6 +51,8 @@ const FilesRedirect = () => {
const AppRoute = () => (
<SentryRoutes>
<Route path="external/:fileId" element={<ExternalRedirect />} />
<Route path="note/:fileId" element={<PublicNoteRedirect />} />

<Route element={<Layout />}>
<Route path="upload" element={<UploaderComponent />} />
<Route path="/files/:folderId" element={<FilesRedirect />} />
Expand Down
68 changes: 68 additions & 0 deletions src/modules/navigation/PublicNoteRedirect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { FC, useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'

import { useClient } from 'cozy-client'
import { fetchURL } from 'cozy-client/dist/models/note'
import Empty from 'cozy-ui/transpiled/react/Empty'
import Icon from 'cozy-ui/transpiled/react/Icon'
import SadCozyIcon from 'cozy-ui/transpiled/react/Icons/SadCozy'
import Spinner from 'cozy-ui/transpiled/react/Spinner'
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'

import { joinPath } from 'lib/path'
import { DummyLayout } from 'modules/layout/DummyLayout'

const PublicNoteRedirect: FC = () => {
const { t } = useI18n()
const { fileId } = useParams()
const client = useClient()

const [noteUrl, setNoteUrl] = useState<string | null>(null)
const [fetchStatus, setFetchStatus] = useState<
'failed' | 'loading' | 'pending' | 'loaded'
>('pending')

useEffect(() => {
const fetchNoteUrl = async (fileId: string): Promise<void> => {
setFetchStatus('loading')
try {
const url = await fetchURL(
client,
{
id: fileId
},
{
pathname: joinPath(location.pathname, '')
}
)
setNoteUrl(url)
setFetchStatus('loaded')
} catch (error) {
setFetchStatus('failed')
}
}

if (fileId) {
void fetchNoteUrl(fileId)
}
}, [fileId, client])

if (noteUrl) {
window.location.href = noteUrl
}

return (
<DummyLayout>
{fetchStatus === 'failed' && (
<Empty
icon={<Icon icon={SadCozyIcon} color="var(--primaryColor)" />}
title={t('PublicNoteRedirect.error.title')}
text={t('PublicNoteRedirect.error.subtitle')}
/>
)}
{fetchStatus !== 'failed' && <Spinner size="xxlarge" middle noMargin />}
</DummyLayout>
)
}

export { PublicNoteRedirect }
80 changes: 78 additions & 2 deletions src/modules/navigation/hooks/helpers.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,76 @@ describe('computeFileType', () => {
expect(computeFileType(file)).toBe('nextcloud-file')
})

it('should return "note" for notes', () => {
it('should return "public-note-same-instance" for public notes on the same instance', () => {
const file = {
_type: 'io.cozy.files',
name: 'My journal.cozy-note',
type: 'file',
metadata: {
title: '',
version: '0'
},
cozyMetadata: {
createdOn: 'https://example.com/'
}
}
expect(
computeFileType(file, { isPublic: true, cozyUrl: 'https://example.com' })
).toBe('public-note-same-instance')
})

it('should return "note" for notes on the same instance', () => {
const file = {
_type: 'io.cozy.files',
name: 'My journal.cozy-note',
type: 'file',
metadata: {
title: '',
version: '0'
},
cozyMetadata: {
createdOn: 'https://example.com/'
}
}
expect(computeFileType(file)).toBe('note')
expect(computeFileType(file, { cozyUrl: 'https://example.com/' })).toBe(
'note'
)
})

it('should return "public-note" for notes on an another instance', () => {
const file = {
_type: 'io.cozy.files',
name: 'My journal.cozy-note',
type: 'file',
metadata: {
title: '',
version: '0'
},
cozyMetadata: {
createdOn: 'https://example.com/'
}
}
expect(computeFileType(file, { cozyUrl: 'https://another.com/' })).toBe(
'public-note'
)
})

it('should return "public-note" for public notes', () => {
const file = {
_type: 'io.cozy.files',
name: 'My journal.cozy-note',
type: 'file',
metadata: {
title: '',
version: '0'
},
cozyMetadata: {
createdOn: 'https://example.com/'
}
}
expect(
computeFileType(file, { isPublic: true, cozyUrl: 'https://another.com' })
).toBe('public-note')
})

it('should return "onlyoffice" for files opened by OnlyOffice when Office is enabled', () => {
Expand Down Expand Up @@ -158,6 +217,23 @@ describe('computePath', () => {
)
})

it('should return correct path for public-note', () => {
const file = { _id: 'note123' }
expect(
computePath(file, { type: 'public-note', pathname: '/public' })
).toBe('/note/note123')
})

it('should return correct path for public-note-same-instance', () => {
const file = { _id: 'note123' }
expect(
computePath(file, {
type: 'public-note-same-instance',
pathname: '/public'
})
).toBe('/?id=note123')
})

it('should return correct path for shortcut', () => {
const file = { _id: 'shortcut123' }
expect(computePath(file, { type: 'shortcut', pathname: '/any' })).toBe(
Expand Down
38 changes: 30 additions & 8 deletions src/modules/navigation/hooks/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import {

import type { File } from 'components/FolderPicker/types'
import { TRASH_DIR_ID } from 'constants/config'
import { joinPath } from 'lib/path'
import { isNextcloudShortcut } from 'modules/nextcloud/helpers'
import { makeOnlyOfficeFileRoute } from 'modules/views/OnlyOffice/helpers'

interface ComputeFileTypeOptions {
isOfficeEnabled?: boolean
isPublic?: boolean
cozyUrl?: string
}

interface ComputePathOptions {
Expand All @@ -22,7 +25,11 @@ interface ComputePathOptions {

export const computeFileType = (
file: File,
{ isOfficeEnabled = false }: ComputeFileTypeOptions = {}
{
isOfficeEnabled = false,
isPublic = false,
cozyUrl = ''
}: ComputeFileTypeOptions = {}
): string => {
if (file._id === TRASH_DIR_ID) {
return 'trash'
Expand All @@ -31,7 +38,16 @@ export const computeFileType = (
} else if (file._type === 'io.cozy.remote.nextcloud.files') {
return isDirectory(file) ? 'nextcloud-directory' : 'nextcloud-file'
} else if (isNote(file)) {
return 'note'
// createdOn url ends with a trailing slash whereas cozyUrl does not joinPath fixes this
const isSameInstance =
joinPath(cozyUrl, '') === file.cozyMetadata?.createdOn
if (isPublic && isSameInstance) {
return 'public-note-same-instance'
} else if (isSameInstance) {
return 'note'
} else {
return 'public-note'
}
} else if (shouldBeOpenedByOnlyOffice(file) && isOfficeEnabled) {
return 'onlyoffice'
} else if (isNextcloudShortcut(file)) {
Expand All @@ -46,13 +62,15 @@ export const computeFileType = (
}

export const computeApp = (type: string): string => {
if (type === 'nextcloud-file') {
return 'nextcloud'
}
if (type === 'note') {
return 'notes'
switch (type) {
case 'nextcloud-file':
return 'nextcloud'
case 'note':
case 'public-note-same-instance':
return 'notes'
default:
return 'drive'
}
return 'drive'
}

export const computePath = (
Expand All @@ -75,6 +93,10 @@ export const computePath = (
return file.links?.self ?? ''
case 'note':
return `/n/${file._id}`
case 'public-note-same-instance':
return `/?id=${file._id}`
case 'public-note':
return `/note/${file._id}`
case 'shortcut':
return `/external/${file._id}`
case 'directory':
Expand Down
17 changes: 13 additions & 4 deletions src/modules/navigation/hooks/useFileLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useClient, generateWebLink } from 'cozy-client'
import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'

import type { File } from 'components/FolderPicker/types'
import { joinPath } from 'lib/path'
import {
computeFileType,
computeApp,
Expand Down Expand Up @@ -48,8 +49,13 @@ const useFileLink = (file: File): UseFileLinkResult => {
const isOfficeEnabled = computeOfficeEnabled(isDesktop)
const { isPublic } = usePublicContext()

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
const cozyUrl = client?.getStackClient().uri as string

const type = computeFileType(file, {
isOfficeEnabled
isOfficeEnabled,
isPublic,
cozyUrl
})
const app = computeApp(type)
const path = computePath(file, {
Expand Down Expand Up @@ -81,11 +87,14 @@ const useFileLink = (file: File): UseFileLinkResult => {
? path
: generateWebLink({
slug: app,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
cozyUrl: client?.getStackClient().uri,
cozyUrl,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
subDomainType: client?.getInstanceOptions().subdomain,
pathname: currentURL.pathname,
// Inside notes, we need to add / at the end of /public/ or /preview/ to avoid 409 error
pathname:
type === 'public-note-same-instance'
? joinPath(currentURL.pathname, '')
: currentURL.pathname,
searchParams: searchParams as unknown as unknown[],
hash: to.pathname
})
Expand Down
Loading

0 comments on commit c624f57

Please sign in to comment.