Skip to content

Commit 3070a9e

Browse files
committed
feat: add image gallery functionality
- Implemented image gallery dialog to view images in a gallery format. - Added `useImageGallery` hook for managing gallery state and opening the dialog. - Integrated image gallery button in `ContentInfo` and context menu for image content. - Enhanced image loading and error handling in the `ImageView` component. - Created `ImageThumbnail` component for displaying image thumbnails. - Updated localization for image gallery features in both default and Hungarian languages. - Added utility functions for checking image content and fetching image URLs. - Adjusted styles for image gallery dialog and grid components.
1 parent c194b82 commit 3070a9e

24 files changed

Lines changed: 766 additions & 35 deletions

‎apps/sensenet/src/components/Icon.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ import travisCi from '@iconify-icons/logos/travis-ci'
6060
import vercelIcon from '@iconify-icons/logos/vercel-icon'
6161
import React, { CSSProperties, FunctionComponent } from 'react'
6262
import { EventLogEntry } from '../services/EventService'
63+
import { isImageContent } from '../services/image-content-service'
6364
import { IconFromPath } from './IconFromPath'
65+
import { ImageThumbnail } from './image-thumbnail'
6466
import { UserAvatar } from './UserAvatar'
6567

6668
export interface IconOptions {
@@ -186,6 +188,10 @@ const getIconByName = (name: string | undefined, options: IconOptions) => {
186188

187189
/* eslint-disable react/display-name */
188190
export const defaultContentResolvers: Array<IconResolver<GenericContent>> = [
191+
{
192+
get: (item, options) =>
193+
isImageContent(item) ? <ImageThumbnail content={item} repository={options.repo} style={options.style} /> : null,
194+
},
189195
{
190196
get: (item, options) => {
191197
let icon = item.Icon

‎apps/sensenet/src/components/content-list/content-list.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ import { TableCellProps } from 'react-virtualized'
2929
import { ResponsiveContext, ResponsivePersonalSettings } from '../../context'
3030
import { globals, useGlobalStyles } from '../../globalStyles'
3131
import { useLocalization, usePersonalSettings, useSelectionService } from '../../hooks'
32+
import { isImageContent } from '../../services'
3233
import { ContentBreadcrumbs } from '../ContentBreadcrumbs'
3334
import { ContentContextMenu } from '../context-menu/content-context-menu'
3435
import { useDialog } from '../dialogs'
3536
import { DropFileArea } from '../DropFileArea'
37+
import { useImageGallery } from '../image-gallery'
3638
import { SelectionControl } from '../SelectionControl'
3739
import { isFolderLikeTreeItem, SETTINGS_FOLDER_FILTER } from '../tree/tree-helpers'
3840
import { ContextMenuWrapper } from './context-menu-wrapper'
@@ -180,6 +182,7 @@ export const ContentList = <T extends GenericContent = GenericContent>(props: Co
180182
}, [currentChildren, currentDirection, currentOrder, userPersonalSettings.sortFoldersFirst])
181183
const [selected, setSelected] = useState<T[]>([])
182184
const [activeContent, setActiveContent] = useState<T>(children[0])
185+
const { openImageGallery } = useImageGallery()
183186

184187
const [columnSettings, setColumnSettings] = useState<Array<ColumnSetting<GenericContent>>>(
185188
personalSettings.content.fields,
@@ -331,9 +334,13 @@ export const ContentList = <T extends GenericContent = GenericContent>(props: Co
331334

332335
const handleActivateItem = useCallback(
333336
(item: T) => {
337+
if (isImageContent(item)) {
338+
openImageGallery(item, children)
339+
return
340+
}
334341
props.onParentChange(item)
335342
},
336-
[props],
343+
[children, openImageGallery, props],
337344
)
338345

339346
const handleItemClick = useCallback(

‎apps/sensenet/src/components/content/ContentInfo.tsx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getIcon } from '../context-menu/icons'
88
import { useContextMenuActions } from '../context-menu/use-context-menu-actions'
99
import { FavoriteButton } from '../favorites/FavoriteButton'
1010
import { Icon } from '../Icon'
11+
import { useImageGallery } from '../image-gallery'
1112

1213
export function ContentInfo() {
1314
const DISABLED_ACTIONS = ['Share', 'Preview', 'Delete']
@@ -21,6 +22,8 @@ export function ContentInfo() {
2122
})
2223
const { isWriteAvailable } = useWopi()
2324
const oDataActionsTitle = useLocalization().customActions.oDataActionsDialog.menuTitle
25+
const imageGalleryLocalization = useLocalization().imageGallery
26+
const { images, openImageGallery } = useImageGallery()
2427

2528
const setActionsWopi = useCallback(
2629
(contentFromCallback: GenericContent) => {
@@ -80,6 +83,17 @@ export function ContentInfo() {
8083
</div>
8184
<div className="buttonPanel">
8285
<FavoriteButton content={parentContent} />
86+
{images.length ? (
87+
<Button
88+
key="Gallery"
89+
title={imageGalleryLocalization.openGallery}
90+
disableRipple={true}
91+
data-test="content-gallery-action"
92+
onClick={() => openImageGallery(images[0], images)}>
93+
<ListItemIcon>{getIcon('gallery')}</ListItemIcon>
94+
<div style={{ flexGrow: 1 }}>{imageGalleryLocalization.openGallery}</div>
95+
</Button>
96+
) : null}
8397
<Button
8498
key="ODataActions"
8599
title={oDataActionsTitle}

‎apps/sensenet/src/components/context-menu/content-context-menu.tsx‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { useLogger, useWopi } from '@sensenet/hooks-react'
1010
import React, { useCallback, useContext, useEffect, useState } from 'react'
1111
import { ResponsiveContext } from '../../context'
1212
import { useLoadContent, useLocalization } from '../../hooks'
13+
import { isImageContent } from '../../services'
14+
import { useImageGallery } from '../image-gallery'
1315
import { contextMenuODataOptions } from './context-menu-odata-options'
1416
import { getIcon } from './icons'
1517
import { useContextMenuActions } from './use-context-menu-actions'
@@ -63,8 +65,11 @@ export const ContentContextMenu: React.FunctionComponent<ContentContextMenuProps
6365
)
6466

6567
const { runAction } = useContextMenuActions(props.content, setActionsWopi)
68+
const { openImageGallery } = useImageGallery()
6669
const device = useContext(ResponsiveContext)
6770
const oDataActionsTitle = useLocalization().customActions.oDataActionsDialog.menuTitle
71+
const imageGalleryLocalization = useLocalization().imageGallery
72+
const canViewImage = isImageContent(props.content)
6873
const runODataActions = () => {
6974
props.onClose?.()
7075
runAction('ODataActions')
@@ -85,6 +90,17 @@ export const ContentContextMenu: React.FunctionComponent<ContentContextMenuProps
8590
open={props.isOpened}
8691
PaperProps={{ style: { paddingBottom: '2em' } }}>
8792
<List>
93+
{canViewImage ? (
94+
<ListItem
95+
button
96+
onClick={() => {
97+
props.onClose?.()
98+
openImageGallery(props.content)
99+
}}>
100+
<ListItemIcon>{getIcon('viewimage')}</ListItemIcon>
101+
<ListItemText primary={imageGalleryLocalization.openImage} />
102+
</ListItem>
103+
) : null}
88104
<ListItem onClick={runODataActions}>
89105
<ListItemIcon>{getIcon('odataactions')}</ListItemIcon>
90106
<ListItemText primary={oDataActionsTitle} />
@@ -107,6 +123,19 @@ export const ContentContextMenu: React.FunctionComponent<ContentContextMenuProps
107123
</Drawer>
108124
) : (
109125
<Menu open={props.isOpened} {...props.menuProps} data-test="content-context-menu-root">
126+
{canViewImage ? (
127+
<MenuItem
128+
key="ViewImage"
129+
disableRipple={true}
130+
data-test="content-context-menu-view-image"
131+
onClick={() => {
132+
props.onClose?.()
133+
openImageGallery(props.content)
134+
}}>
135+
<ListItemIcon>{getIcon('viewimage')}</ListItemIcon>
136+
<div style={{ flexGrow: 1 }}>{imageGalleryLocalization.openImage}</div>
137+
</MenuItem>
138+
) : null}
110139
<MenuItem
111140
key="ODataActions"
112141
disableRipple={true}

‎apps/sensenet/src/components/context-menu/icons.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import HistoryIcon from '@material-ui/icons/History'
1010
import InfoIcon from '@material-ui/icons/Info'
1111
import Lock from '@material-ui/icons/Lock'
1212
import LockOpen from '@material-ui/icons/LockOpen'
13+
import PhotoLibraryOutlinedIcon from '@material-ui/icons/PhotoLibraryOutlined'
1314
import MuiPublishIcon from '@material-ui/icons/Publish'
1415
import ReportProblemIcon from '@material-ui/icons/ReportProblem'
1516
import RestoreIcon from '@material-ui/icons/RestoreFromTrash'
1617
import Search from '@material-ui/icons/Search'
1718
import ShareIcon from '@material-ui/icons/Share'
1819
import UndoIcon from '@material-ui/icons/Undo'
20+
import VisibilityOutlinedIcon from '@material-ui/icons/VisibilityOutlined'
1921
import React from 'react'
2022

2123
export const getIcon = (actionName: string) => {
@@ -34,6 +36,8 @@ export const getIcon = (actionName: string) => {
3436
return <Delete />
3537
case 'download':
3638
return <CloudDownload />
39+
case 'gallery':
40+
return <PhotoLibraryOutlinedIcon />
3741
case 'edit':
3842
return <Edit />
3943
case 'forceundocheckout':
@@ -42,6 +46,8 @@ export const getIcon = (actionName: string) => {
4246
return <FileCopyIcon />
4347
case 'preview':
4448
return <Search />
49+
case 'viewimage':
50+
return <VisibilityOutlinedIcon />
4551
case 'publish':
4652
return <MuiPublishIcon />
4753
case 'setpermissions':

‎apps/sensenet/src/components/dialogs/dialog-provider.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
DateRangePickerProps,
1515
DeleteContentDialogProps,
1616
ExecuteActionDialogProps,
17+
ImageGalleryDialogProps,
1718
ODataActionsDialogProps,
1819
OperationsDialogProps,
1920
PermissionEditorDialogProps,
@@ -34,6 +35,7 @@ export type DialogWithProps = (
3435
| { name: 'approve'; props: ApproveProps }
3536
| { name: 'restore'; props: RestoreProps }
3637
| { name: 'upload'; props: UploadDialogProps }
38+
| { name: 'image-gallery'; props: ImageGalleryDialogProps }
3739
| { name: 'execute-action'; props: ExecuteActionDialogProps }
3840
| { name: 'custom-action-result'; props: CustomActionResultDialogProps }
3941
| { name: 'logout' }

‎apps/sensenet/src/components/dialogs/dialogs.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const ExecuteAction = lazy(() => import('./execute-action'))
1414
const Logout = lazy(() => import('./logout'))
1515
const SaveQuery = lazy(() => import('./save-query'))
1616
const Upload = lazy(() => import('./upload/upload-dialog'))
17+
const ImageGallery = lazy(() => import('./image-gallery'))
1718
const ReferenceContentList = lazy(() => import('./reference-content-list'))
1819
const PermissionEditorDialog = lazy(() => import('./permission-editor-dialog'))
1920
const MemberSelect = lazy(() => import('./member-select-dialog'))
@@ -45,6 +46,8 @@ function dialogRenderer(dialog: DialogWithProps) {
4546
return <Restore {...dialog.props} />
4647
case 'upload':
4748
return <Upload {...dialog.props} />
49+
case 'image-gallery':
50+
return <ImageGallery {...dialog.props} />
4851
case 'execute-action':
4952
return <ExecuteAction {...dialog.props} />
5053
case 'custom-action-result':

0 commit comments

Comments
 (0)