mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-25 04:24:43 -04:00
feat(sidebar): add media browser
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
8693edbf6a
commit
6bb2452705
11 changed files with 265 additions and 61 deletions
|
@ -395,6 +395,14 @@
|
||||||
"contributors": "Count of contributors",
|
"contributors": "Count of contributors",
|
||||||
"wordCount": "Count of words"
|
"wordCount": "Count of words"
|
||||||
},
|
},
|
||||||
|
"mediaBrowser": {
|
||||||
|
"title": "Media",
|
||||||
|
"deleteMedia": "Delete uploaded file",
|
||||||
|
"confirmDeletion": "Do you really want to delete this file?",
|
||||||
|
"errorDeleting": "The uploaded file could not be deleted.",
|
||||||
|
"mediaDeleted": "The uploaded file has been deleted.",
|
||||||
|
"noMediaUploads": "There are no media files uploaded to this note yet"
|
||||||
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"snippetImport": {
|
"snippetImport": {
|
||||||
"title": "Import from Snippet",
|
"title": "Import from Snippet",
|
||||||
|
@ -553,6 +561,7 @@
|
||||||
"loading": "Loading ...",
|
"loading": "Loading ...",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
|
"success": "Success",
|
||||||
"errorWhileLoading": "An unexpected error occurred while loading '{{name}}'.\nCheck the browser console for more information.\nReport this error only if it comes up again.",
|
"errorWhileLoading": "An unexpected error occurred while loading '{{name}}'.\nCheck the browser console for more information.\nReport this error only if it comes up again.",
|
||||||
"errorOccurred": "An error occurred",
|
"errorOccurred": "An error occurred",
|
||||||
"readForMoreInfo": "Read here for more information",
|
"readForMoreInfo": "Read here for more information",
|
||||||
|
|
|
@ -1,47 +1,5 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`DeletionModal disables deletion when user is not owner 1`] = `
|
|
||||||
<div
|
|
||||||
class="modal-dialog"
|
|
||||||
data-testid="commonModal"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="modal-content"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="modal-header"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="modal-title h4"
|
|
||||||
>
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
aria-label="Close"
|
|
||||||
class="btn-close"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="modal-body"
|
|
||||||
>
|
|
||||||
testText
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="modal-footer"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-danger"
|
|
||||||
disabled=""
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
testDeletionButton
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`DeletionModal renders correctly with deletionButtonI18nKey 1`] = `
|
exports[`DeletionModal renders correctly with deletionButtonI18nKey 1`] = `
|
||||||
<div
|
<div
|
||||||
class="modal-dialog"
|
class="modal-dialog"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -29,16 +29,4 @@ describe('DeletionModal', () => {
|
||||||
const modal = await screen.findByTestId('commonModal')
|
const modal = await screen.findByTestId('commonModal')
|
||||||
expect(modal).toMatchSnapshot()
|
expect(modal).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('disables deletion when user is not owner', async () => {
|
|
||||||
mockNotePermissions('test2', 'test')
|
|
||||||
const onConfirm = jest.fn()
|
|
||||||
render(
|
|
||||||
<DeletionModal onConfirm={onConfirm} deletionButtonI18nKey={'testDeletionButton'} show={true}>
|
|
||||||
testText
|
|
||||||
</DeletionModal>
|
|
||||||
)
|
|
||||||
const modal = await screen.findByTestId('commonModal')
|
|
||||||
expect(modal).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useIsOwner } from '../../../hooks/common/use-is-owner'
|
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
import type { CommonModalProps } from './common-modal'
|
import type { CommonModalProps } from './common-modal'
|
||||||
import { CommonModal } from './common-modal'
|
import { CommonModal } from './common-modal'
|
||||||
|
@ -15,6 +14,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
export interface DeletionModalProps extends CommonModalProps {
|
export interface DeletionModalProps extends CommonModalProps {
|
||||||
onConfirm: () => void
|
onConfirm: () => void
|
||||||
deletionButtonI18nKey: string
|
deletionButtonI18nKey: string
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,11 +38,10 @@ export const DeletionModal: React.FC<PropsWithChildren<DeletionModalProps>> = ({
|
||||||
deletionButtonI18nKey,
|
deletionButtonI18nKey,
|
||||||
titleIcon,
|
titleIcon,
|
||||||
children,
|
children,
|
||||||
|
disabled = false,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const isOwner = useIsOwner()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal
|
<CommonModal
|
||||||
show={show}
|
show={show}
|
||||||
|
@ -53,7 +52,7 @@ export const DeletionModal: React.FC<PropsWithChildren<DeletionModalProps>> = ({
|
||||||
{...props}>
|
{...props}>
|
||||||
<Modal.Body>{children}</Modal.Body>
|
<Modal.Body>{children}</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button {...cypressId('deletionModal.confirmButton')} variant='danger' onClick={onConfirm} disabled={!isOwner}>
|
<Button {...cypressId('deletionModal.confirmButton')} variant='danger' onClick={onConfirm} disabled={disabled}>
|
||||||
<Trans i18nKey={deletionButtonI18nKey} />
|
<Trans i18nKey={deletionButtonI18nKey} />
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { AliasesSidebarEntry } from './specific-sidebar-entries/aliases-sidebar-
|
||||||
import { DeleteNoteSidebarEntry } from './specific-sidebar-entries/delete-note-sidebar-entry/delete-note-sidebar-entry'
|
import { DeleteNoteSidebarEntry } from './specific-sidebar-entries/delete-note-sidebar-entry/delete-note-sidebar-entry'
|
||||||
import { ExportSidebarMenu } from './specific-sidebar-entries/export-sidebar-menu/export-sidebar-menu'
|
import { ExportSidebarMenu } from './specific-sidebar-entries/export-sidebar-menu/export-sidebar-menu'
|
||||||
import { ImportMenuSidebarMenu } from './specific-sidebar-entries/import-menu-sidebar-menu'
|
import { ImportMenuSidebarMenu } from './specific-sidebar-entries/import-menu-sidebar-menu'
|
||||||
|
import { MediaBrowserSidebarMenu } from './specific-sidebar-entries/media-browser-sidebar-menu/media-browser-sidebar-menu'
|
||||||
import { NoteInfoSidebarMenu } from './specific-sidebar-entries/note-info-sidebar-menu/note-info-sidebar-menu'
|
import { NoteInfoSidebarMenu } from './specific-sidebar-entries/note-info-sidebar-menu/note-info-sidebar-menu'
|
||||||
import { PermissionsSidebarEntry } from './specific-sidebar-entries/permissions-sidebar-entry/permissions-sidebar-entry'
|
import { PermissionsSidebarEntry } from './specific-sidebar-entries/permissions-sidebar-entry/permissions-sidebar-entry'
|
||||||
import { PinNoteSidebarEntry } from './specific-sidebar-entries/pin-note-sidebar-entry/pin-note-sidebar-entry'
|
import { PinNoteSidebarEntry } from './specific-sidebar-entries/pin-note-sidebar-entry/pin-note-sidebar-entry'
|
||||||
|
@ -57,6 +58,11 @@ export const Sidebar: React.FC = () => {
|
||||||
<RevisionSidebarEntry hide={selectionIsNotNone} />
|
<RevisionSidebarEntry hide={selectionIsNotNone} />
|
||||||
<PermissionsSidebarEntry hide={selectionIsNotNone} />
|
<PermissionsSidebarEntry hide={selectionIsNotNone} />
|
||||||
<AliasesSidebarEntry hide={selectionIsNotNone} />
|
<AliasesSidebarEntry hide={selectionIsNotNone} />
|
||||||
|
<MediaBrowserSidebarMenu
|
||||||
|
onClick={toggleValue}
|
||||||
|
selectedMenuId={selectedMenu}
|
||||||
|
menuId={DocumentSidebarMenuSelection.MEDIA_BROWSER}
|
||||||
|
/>
|
||||||
<ImportMenuSidebarMenu
|
<ImportMenuSidebarMenu
|
||||||
menuId={DocumentSidebarMenuSelection.IMPORT}
|
menuId={DocumentSidebarMenuSelection.IMPORT}
|
||||||
selectedMenuId={selectedMenu}
|
selectedMenuId={selectedMenu}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -9,6 +9,7 @@ import type { ModalVisibilityProps } from '../../../../common/modals/common-moda
|
||||||
import { DeletionModal } from '../../../../common/modals/deletion-modal'
|
import { DeletionModal } from '../../../../common/modals/deletion-modal'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Trans } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
import { useIsOwner } from '../../../../../hooks/common/use-is-owner'
|
||||||
|
|
||||||
export interface DeleteHistoryNoteModalProps {
|
export interface DeleteHistoryNoteModalProps {
|
||||||
modalTitleI18nKey?: string
|
modalTitleI18nKey?: string
|
||||||
|
@ -45,6 +46,7 @@ export const DeleteNoteModal: React.FC<DeleteNoteModalProps & DeleteHistoryNoteM
|
||||||
modalButtonI18nKey
|
modalButtonI18nKey
|
||||||
}) => {
|
}) => {
|
||||||
const noteTitle = useNoteTitle()
|
const noteTitle = useNoteTitle()
|
||||||
|
const isOwner = useIsOwner()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DeletionModal
|
<DeletionModal
|
||||||
|
@ -53,6 +55,7 @@ export const DeleteNoteModal: React.FC<DeleteNoteModalProps & DeleteHistoryNoteM
|
||||||
deletionButtonI18nKey={modalButtonI18nKey ?? 'editor.modal.deleteNote.button'}
|
deletionButtonI18nKey={modalButtonI18nKey ?? 'editor.modal.deleteNote.button'}
|
||||||
show={show}
|
show={show}
|
||||||
onHide={onHide}
|
onHide={onHide}
|
||||||
|
disabled={!isOwner}
|
||||||
titleI18nKey={modalTitleI18nKey ?? 'editor.modal.deleteNote.title'}>
|
titleI18nKey={modalTitleI18nKey ?? 'editor.modal.deleteNote.title'}>
|
||||||
<h5>
|
<h5>
|
||||||
<Trans i18nKey={modalQuestionI18nKey ?? 'editor.modal.deleteNote.question'} />
|
<Trans i18nKey={modalQuestionI18nKey ?? 'editor.modal.deleteNote.question'} />
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import React from 'react'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the info message for the media browser empty state.
|
||||||
|
*/
|
||||||
|
export const MediaBrowserEmpty: React.FC = () => {
|
||||||
|
useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='text-center p-2'>
|
||||||
|
<p className='text-muted'>
|
||||||
|
<Trans i18nKey={'editor.mediaBrowser.noMediaUploads'} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { SidebarButton } from '../../sidebar-button/sidebar-button'
|
||||||
|
import { SidebarMenu } from '../../sidebar-menu/sidebar-menu'
|
||||||
|
import type { SpecificSidebarMenuProps } from '../../types'
|
||||||
|
import { DocumentSidebarMenuSelection } from '../../types'
|
||||||
|
import React, { Fragment, useCallback, useMemo, useState } from 'react'
|
||||||
|
import { ArrowLeft as IconArrowLeft, Images as IconImages } from 'react-bootstrap-icons'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { useAsync } from 'react-use'
|
||||||
|
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||||
|
import { getMediaForNote } from '../../../../../api/notes'
|
||||||
|
import { AsyncLoadingBoundary } from '../../../../common/async-loading-boundary/async-loading-boundary'
|
||||||
|
import { MediaEntry } from './media-entry'
|
||||||
|
import type { MediaUpload } from '../../../../../api/media/types'
|
||||||
|
import { MediaEntryDeletionModal } from './media-entry-deletion-modal'
|
||||||
|
import { MediaBrowserEmpty } from './media-browser-empty'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the media browser "menu" for the sidebar.
|
||||||
|
*
|
||||||
|
* @param className Additional class names given to the menu button
|
||||||
|
* @param menuId The id of the menu
|
||||||
|
* @param onClick The callback, that should be called when the menu button is pressed
|
||||||
|
* @param selectedMenuId The currently selected menu id
|
||||||
|
*/
|
||||||
|
export const MediaBrowserSidebarMenu: React.FC<SpecificSidebarMenuProps> = ({
|
||||||
|
className,
|
||||||
|
menuId,
|
||||||
|
onClick,
|
||||||
|
selectedMenuId
|
||||||
|
}) => {
|
||||||
|
useTranslation()
|
||||||
|
const noteId = useApplicationState((state) => state.noteDetails?.id ?? '')
|
||||||
|
const [mediaEntryForDeletion, setMediaEntryForDeletion] = useState<MediaUpload | null>(null)
|
||||||
|
|
||||||
|
const hide = selectedMenuId !== DocumentSidebarMenuSelection.NONE && selectedMenuId !== menuId
|
||||||
|
const expand = selectedMenuId === menuId
|
||||||
|
const onClickHandler = useCallback(() => {
|
||||||
|
onClick(menuId)
|
||||||
|
}, [menuId, onClick])
|
||||||
|
|
||||||
|
const { value, loading, error } = useAsync(() => getMediaForNote(noteId), [expand, noteId])
|
||||||
|
|
||||||
|
const mediaEntries = useMemo(() => {
|
||||||
|
if (loading || error || !value) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return value.map((entry) => <MediaEntry entry={entry} key={entry.id} onDelete={setMediaEntryForDeletion} />)
|
||||||
|
}, [value, loading, error, setMediaEntryForDeletion])
|
||||||
|
|
||||||
|
const cancelDeletion = useCallback(() => {
|
||||||
|
setMediaEntryForDeletion(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<SidebarButton
|
||||||
|
hide={hide}
|
||||||
|
icon={expand ? IconArrowLeft : IconImages}
|
||||||
|
className={className}
|
||||||
|
onClick={onClickHandler}>
|
||||||
|
<Trans i18nKey={'editor.mediaBrowser.title'} />
|
||||||
|
</SidebarButton>
|
||||||
|
<SidebarMenu expand={expand}>
|
||||||
|
<AsyncLoadingBoundary loading={loading} componentName={'MediaBrowserSidebarMenu'} error={error}>
|
||||||
|
{mediaEntries}
|
||||||
|
{mediaEntries.length === 0 && <MediaBrowserEmpty />}
|
||||||
|
</AsyncLoadingBoundary>
|
||||||
|
</SidebarMenu>
|
||||||
|
{mediaEntryForDeletion && (
|
||||||
|
<MediaEntryDeletionModal entry={mediaEntryForDeletion} show={true} onHide={cancelDeletion} />
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import type { MediaEntryProps } from './media-entry'
|
||||||
|
import type { ModalVisibilityProps } from '../../../../common/modals/common-modal'
|
||||||
|
import { DeletionModal } from '../../../../common/modals/deletion-modal'
|
||||||
|
import { deleteUploadedMedia } from '../../../../../api/media'
|
||||||
|
import { useUiNotifications } from '../../../../notifications/ui-notification-boundary'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
type MediaEntryDeletionModalProps = Pick<MediaEntryProps, 'entry'> & ModalVisibilityProps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a modal for confirming the deletion of a media entry.
|
||||||
|
*
|
||||||
|
* @param entry The media entry to delete
|
||||||
|
* @param show Whether the modal should be shown
|
||||||
|
* @param onHide The callback when the modal should be hidden
|
||||||
|
*/
|
||||||
|
export const MediaEntryDeletionModal: React.FC<MediaEntryDeletionModalProps> = ({ entry, show, onHide }) => {
|
||||||
|
useTranslation()
|
||||||
|
const { showErrorNotification, dispatchUiNotification } = useUiNotifications()
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
deleteUploadedMedia(entry.id)
|
||||||
|
.then(() => {
|
||||||
|
dispatchUiNotification('common.success', 'editor.mediaBrowser.mediaDeleted', {})
|
||||||
|
})
|
||||||
|
.catch(showErrorNotification('editor.mediaBrowser.errorDeleting'))
|
||||||
|
.finally(onHide)
|
||||||
|
}, [showErrorNotification, dispatchUiNotification, entry, onHide])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeletionModal
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
deletionButtonI18nKey={'common.delete'}
|
||||||
|
show={show}
|
||||||
|
onHide={onHide}
|
||||||
|
titleI18nKey={'editor.mediaBrowser.deleteMedia'}>
|
||||||
|
<Trans i18nKey={'editor.mediaBrowser.confirmDeletion'} />
|
||||||
|
</DeletionModal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import React, { useCallback, useMemo } from 'react'
|
||||||
|
import type { MediaUpload } from '../../../../../api/media/types'
|
||||||
|
import { useBaseUrl } from '../../../../../hooks/common/use-base-url'
|
||||||
|
import { Button, ButtonGroup } from 'react-bootstrap'
|
||||||
|
import {
|
||||||
|
Trash as IconTrash,
|
||||||
|
FileRichtextFill as IconFileRichtextFill,
|
||||||
|
Person as IconPerson,
|
||||||
|
Clock as IconClock
|
||||||
|
} from 'react-bootstrap-icons'
|
||||||
|
import { useIsOwner } from '../../../../../hooks/common/use-is-owner'
|
||||||
|
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||||
|
import { UserAvatarForUsername } from '../../../../common/user-avatar/user-avatar-for-username'
|
||||||
|
import { useChangeEditorContentCallback } from '../../../change-content-context/use-change-editor-content-callback'
|
||||||
|
import { replaceSelection } from '../../../editor-pane/tool-bar/formatters/replace-selection'
|
||||||
|
|
||||||
|
export interface MediaEntryProps {
|
||||||
|
entry: MediaUpload
|
||||||
|
onDelete: (entry: MediaUpload) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a single media entry in the media browser.
|
||||||
|
*
|
||||||
|
* @param entry The media entry to render
|
||||||
|
* @param onDelete The callback to call when the entry should be deleted
|
||||||
|
*/
|
||||||
|
export const MediaEntry: React.FC<MediaEntryProps> = ({ entry, onDelete }) => {
|
||||||
|
const changeEditorContent = useChangeEditorContentCallback()
|
||||||
|
const user = useApplicationState((state) => state.user?.username)
|
||||||
|
const baseUrl = useBaseUrl()
|
||||||
|
const isOwner = useIsOwner()
|
||||||
|
|
||||||
|
const imageUrl = useMemo(() => {
|
||||||
|
return `${baseUrl}api/private/media/${entry.id}`
|
||||||
|
}, [entry, baseUrl])
|
||||||
|
const textCreatedTime = useMemo(() => {
|
||||||
|
return new Date(entry.createdAt).toLocaleString()
|
||||||
|
}, [entry])
|
||||||
|
|
||||||
|
const handleInsertIntoNote = useCallback(() => {
|
||||||
|
changeEditorContent?.(({ currentSelection }) => {
|
||||||
|
return replaceSelection(
|
||||||
|
{ from: currentSelection.to ?? currentSelection.from },
|
||||||
|
``,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, [changeEditorContent, entry, imageUrl])
|
||||||
|
|
||||||
|
const deleteEntry = useCallback(() => {
|
||||||
|
onDelete(entry)
|
||||||
|
}, [entry, onDelete])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'p-2 border-bottom border-opacity-50'}>
|
||||||
|
<a href={imageUrl} target={'_blank'} rel={'noreferrer'} className={'text-center d-block mb-2'}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img src={imageUrl} alt={`Upload ${entry.id}`} height={100} className={'mw-100'} />
|
||||||
|
</a>
|
||||||
|
<div className={'w-100 d-flex flex-row align-items-center justify-content-between'}>
|
||||||
|
<div>
|
||||||
|
<small className={'d-inline-flex flex-row align-items-center'}>
|
||||||
|
<IconPerson className={'me-1'} />
|
||||||
|
<UserAvatarForUsername username={entry.username} size={'sm'} />
|
||||||
|
</small>
|
||||||
|
<br />
|
||||||
|
<small>
|
||||||
|
<IconClock className={'me-1'} />
|
||||||
|
{textCreatedTime}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<ButtonGroup className={'my-2'}>
|
||||||
|
<Button size={'sm'} variant={'primary'} onClick={handleInsertIntoNote}>
|
||||||
|
<IconFileRichtextFill />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size={'sm'}
|
||||||
|
variant={'danger'}
|
||||||
|
disabled={!isOwner && (!user || entry.username !== user)}
|
||||||
|
onClick={deleteEntry}>
|
||||||
|
<IconTrash />
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ export enum DocumentSidebarMenuSelection {
|
||||||
NONE,
|
NONE,
|
||||||
USERS_ONLINE,
|
USERS_ONLINE,
|
||||||
NOTE_INFO,
|
NOTE_INFO,
|
||||||
|
MEDIA_BROWSER,
|
||||||
IMPORT,
|
IMPORT,
|
||||||
EXPORT
|
EXPORT
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue