Skip to content

Commit

Permalink
Added contextMenu to the grid (#847)
Browse files Browse the repository at this point in the history
* added basic context menu to list

* WIP: added context menu to grid

* updated context menu logic

* added `delete` option to context menu

* added `download` option

* added `download` and `delete` options to grid, cleanup

* added `refresh-grid` hook, added `refresh-grid` hook to `rename` and `delete`

* centralized `getElementById` function

* fixed typo

* added updated api

* cleanup

* removed unused type

* Apply eslint-fixer changes

* Automatic frontend build

* added wrapper to context menu to render it just if there are items

* Automatic frontend build

* fixed rename grid reload

* Automatic frontend build

* fixed element context issue

* Automatic frontend build

* removed row `id`, `isLocked` and `permissions` from the loop

* Automatic frontend build

* remove console log

* Automatic frontend build

* optimized `useElementContext` hook

* Automatic frontend build

---------

Co-authored-by: Corepex <[email protected]>
  • Loading branch information
Corepex and Corepex authored Jan 15, 2025
1 parent 2625c28 commit 1abf1d5
Show file tree
Hide file tree
Showing 35 changed files with 1,957 additions and 53 deletions.
80 changes: 48 additions & 32 deletions assets/js/src/core/components/grid/grid-cell/grid-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,50 +16,66 @@ import React, { useMemo } from 'react'
import { GridCell } from './grid-cell'
import { type GridContextProviderProps } from '../grid-context'
import { type GridProps } from '@Pimcore/types/components/types'
import { Dropdown, type DropdownMenuProps } from '@Pimcore/components/dropdown/dropdown'

export interface GridRowProps {
row: Row<any>
modifiedCells: string
isSelected?: boolean
tableElement: GridContextProviderProps['table']
columns: GridProps['columns']
contextMenuItems?: DropdownMenuProps['items']
}

const GridRow = ({ row, isSelected, modifiedCells, ...props }: GridRowProps): React.JSX.Element => {
const GridRow = ({ row, isSelected, modifiedCells, contextMenuItems = [], ...props }: GridRowProps): React.JSX.Element => {
const memoModifiedCells = useMemo(() => { return JSON.parse(modifiedCells) }, [modifiedCells])

return useMemo(() => {
return (
<tr
className={ ['ant-table-row', row.getIsSelected() ? 'ant-table-row-selected' : ''].join(' ') }
key={ row.id }
>
{row.getVisibleCells().map(cell => (
<td
className='ant-table-cell'
const renderWithContextMenu = (children: React.ReactNode): React.JSX.Element => {
if (contextMenuItems.length > 0) {
return (
<Dropdown
key={ row.id }
menu={ { items: contextMenuItems } }
trigger={ ['contextMenu'] }
>
{children}
</Dropdown>
)
}

return <>{children}</>
}

return useMemo(() => renderWithContextMenu(
<tr
className={ ['ant-table-row', row.getIsSelected() ? 'ant-table-row-selected' : ''].join(' ') }
key={ row.id }
>
{row.getVisibleCells().map(cell => (
<td
className='ant-table-cell'
key={ cell.id }
style={ cell.column.columnDef.meta?.autoWidth === true
? {
width: 'auto',
minWidth: cell.column.getSize()
}
: {
width: cell.column.getSize(),
maxWidth: cell.column.getSize()
}
}
>
<GridCell
cell={ cell }
isModified={ isModifiedCell(cell.column.id) }
key={ cell.id }
style={ cell.column.columnDef.meta?.autoWidth === true
? {
width: 'auto',
minWidth: cell.column.getSize()
}
: {
width: cell.column.getSize(),
maxWidth: cell.column.getSize()
}
}
>
<GridCell
cell={ cell }
isModified={ isModifiedCell(cell.column.id) }
key={ cell.id }
tableElement={ props.tableElement }
/>
</td>
))}
</tr>
)
}, [JSON.stringify(row), memoModifiedCells, isSelected, props.columns])
tableElement={ props.tableElement }
/>
</td>
))}
</tr>
), [JSON.stringify(row), memoModifiedCells, isSelected, props.columns])

function isModifiedCell (cellId: string): boolean {
return memoModifiedCells.find((item) => item.columnId === cellId) !== undefined
Expand Down
40 changes: 35 additions & 5 deletions assets/js/src/core/components/grid/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import {
type CellContext,
type Column,
type ColumnDef,
type ColumnResizeMode, type ColumnSizingInfoState,
flexRender, functionalUpdate,
getCoreRowModel, getSortedRowModel,
type ColumnResizeMode,
type ColumnSizingInfoState,
flexRender,
functionalUpdate,
getCoreRowModel,
getSortedRowModel,
type RowData,
type RowSelectionState,
type SortingState,
Expand All @@ -36,9 +39,13 @@ import { useTranslation } from 'react-i18next'
import { Checkbox, Skeleton } from 'antd'
import { GridRow } from './grid-cell/grid-row'
import { SortButton, type SortDirection, SortDirections } from '../sort-button/sort-button'
import { DynamicTypeRegistryProvider } from '@Pimcore/modules/element/dynamic-types/registry/provider/dynamic-type-registry-provider'
import {
DynamicTypeRegistryProvider
} from '@Pimcore/modules/element/dynamic-types/registry/provider/dynamic-type-registry-provider'
import { type GridProps } from '@Pimcore/types/components/types'
import trackError, { GeneralError } from '@Pimcore/modules/app/error-handler'
import { type DropdownMenuProps } from '@Pimcore/components/dropdown/dropdown'
import type { AssetGetGridApiResponse } from '@Pimcore/modules/asset/asset-api-slice.gen'

declare module '@tanstack/react-table' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -59,7 +66,21 @@ export interface ExtendedCellContext extends CellContext<any, any> {
modified?: boolean
}

export const Grid = ({ enableMultipleRowSelection = false, modifiedCells = [], sorting, manualSorting = false, enableSorting = false, enableRowSelection = false, selectedRows = {}, ...props }: GridProps): React.JSX.Element => {
export interface GridContextMenuProps extends Pick<AssetGetGridApiResponse['items'][number], 'isLocked' | 'permissions'> {
id: number
}

export const Grid = ({
enableMultipleRowSelection = false,
modifiedCells = [],
sorting,
manualSorting = false,
enableSorting = false,
enableRowSelection = false,
selectedRows = {},
contextMenuItems = [],
...props
}: GridProps): React.JSX.Element => {
const { t } = useTranslation()
const hashId = useCssComponentHash('table')
const { styles } = useStyles()
Expand Down Expand Up @@ -197,6 +218,14 @@ export const Grid = ({ enableMultipleRowSelection = false, modifiedCells = [], s
</div>
)

const getContextMenuItems = (row: any): DropdownMenuProps['items'] => {
const possibleContextMenuItems = contextMenuItems.map((item) => {
return item(row)
})

return possibleContextMenuItems.filter((item) => item !== undefined)
}

return useMemo(() => (
<DynamicTypeRegistryProvider serviceIds={ ['DynamicTypes/GridCellRegistry'] }>
<div className={ ['ant-table-wrapper', hashId, styles.grid].join(' ') }>
Expand Down Expand Up @@ -264,6 +293,7 @@ export const Grid = ({ enableMultipleRowSelection = false, modifiedCells = [], s
{table.getRowModel().rows.map(row => (
<GridRow
columns={ columns }
contextMenuItems={ getContextMenuItems(row) }
isSelected={ row.getIsSelected() }
key={ row.id }
modifiedCells={ JSON.stringify(getModifiedRow(row.id)) }
Expand Down
21 changes: 19 additions & 2 deletions assets/js/src/core/modules/asset/actions/download/use-download.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { getPrefix } from '@Pimcore/app/api/pimcore/route'
import { saveFileLocal } from '@Pimcore/utils/files'
import type { GridContextMenuProps } from '@Pimcore/components/grid/grid'

export interface UseDownloadReturn {
download: (id: string, label?: string) => void
downloadContextMenuItem: (node: Asset, onFinish?: () => void) => ItemType
downloadTreeContextMenuItem: (node: TreeNodeProps) => ItemType
downloadGridContextMenuItem: (row: any) => ItemType | undefined
}

export const useDownload = (): UseDownloadReturn => {
Expand All @@ -34,7 +36,7 @@ export const useDownload = (): UseDownloadReturn => {
saveFileLocal(downloadUrl, label)
}

const handleDownload = (node: Asset | TreeNodeProps, onFinish?: () => void): void => {
const handleDownload = (node: Asset | TreeNodeProps | GridContextMenuProps, onFinish?: () => void): void => {
const id = typeof node.id === 'string' ? node.id : node.id.toString()
download(id)

Expand All @@ -61,9 +63,24 @@ export const useDownload = (): UseDownloadReturn => {
}
}

const downloadGridContextMenuItem = (row: any): ItemType | undefined => {
const data: GridContextMenuProps = row.original ?? {}
if (data.id === undefined || data.isLocked === undefined || data.permissions === undefined) {
return
}

return {
label: t('asset.tree.context-menu.download'),
key: 'download',
icon: <Icon value={ 'download' } />,
onClick: () => { handleDownload(data) }
}
}

return {
download,
downloadContextMenuItem,
downloadTreeContextMenuItem
downloadTreeContextMenuItem,
downloadGridContextMenuItem
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import {
import { useListColumns, useListSelectedRows, useListSorting } from './hooks/use-list'
import { uuid } from '@Pimcore/utils/uuid'
import { useDynamicTypeResolver } from '@Pimcore/modules/element/dynamic-types/resolver/hooks/use-dynamic-type-resolver'
import { useOpen } from '@Pimcore/modules/element/actions/open/open'
import { useRename } from '@Pimcore/modules/element/actions/rename/use-rename'
import { useDelete } from '@Pimcore/modules/element/actions/delete/use-delete'
import { useDownload } from '@Pimcore/modules/asset/actions/download/use-download'

interface GridContainerProps {
assets: AssetGetGridApiResponse | undefined
Expand Down Expand Up @@ -62,6 +66,10 @@ const GridContainer = (props: GridContainerProps): React.JSX.Element => {
const { selectedRows, setSelectedRows } = useListSelectedRows()
const { sorting, setSorting } = useListSorting()
const { hasType } = useDynamicTypeResolver()
const open = useOpen('asset')
const rename = useRename('asset')
const remove = useDelete('asset')
const download = useDownload()

const onSelectedRowsChange = useCallback((rows: RowSelectionState): void => {
setSelectedRows(rows)
Expand Down Expand Up @@ -129,6 +137,16 @@ const GridContainer = (props: GridContainerProps): React.JSX.Element => {
columnIdentifiers.forEach((columnIdentifier) => {
const columnIdentifierString = decodeColumnIdentifier(columnIdentifier)

row.id = item.id
row.isLocked = item.isLocked
row.permissions = item.permissions

item.columns?.forEach((column) => {
if (column.key === columnIdentifier.key && column.locale === columnIdentifier.locale) {
row[columnIdentifierString] = column.value
}
})

handleProcessColumns({ assetItem: item, assetRow: row, columnIdentifier, columnIdentifierString })
})

Expand All @@ -153,6 +171,12 @@ const GridContainer = (props: GridContainerProps): React.JSX.Element => {
return (
<Grid
columns={ columns }
contextMenuItems={ [
open.openGridContextMenuItem,
rename.renameGridContextMenuItem,
remove.deleteGridContextMenuItem,
download.downloadGridContextMenuItem
] }
data={ data }
enableMultipleRowSelection
enableSorting
Expand Down
41 changes: 38 additions & 3 deletions assets/js/src/core/modules/element/actions/delete/use-delete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,20 @@ import type { TreeNodeProps } from '@Pimcore/components/element-tree/node/tree-n
import { useRefreshTree } from '@Pimcore/modules/element/actions/refresh-tree/use-refresh-tree'
import { createJob as createDeleteJob } from '@Pimcore/modules/execution-engine/jobs/delete/factory'
import { defaultTopics, topics } from '@Pimcore/modules/execution-engine/topics'
import type { AssetDeleteZipApiArg } from '@Pimcore/modules/asset/asset-api-slice.gen'
import { type AssetDeleteZipApiArg } from '@Pimcore/modules/asset/asset-api-slice.gen'
import { useJobs } from '@Pimcore/modules/execution-engine/hooks/useJobs'
import { useElementDeleteMutation } from '@Pimcore/modules/element/element-api-slice.gen'
import { checkElementPermission } from '@Pimcore/modules/element/permissions/permission-helper'
import { type Element } from '@Pimcore/modules/element/element-helper'
import { getElementKey } from '@Pimcore/modules/element/element-helper'
import { type Element, getElementKey } from '@Pimcore/modules/element/element-helper'
import type { GridContextMenuProps } from '@Pimcore/components/grid/grid'
import { useElementApi } from '@Pimcore/modules/element/hooks/use-element-api'
import { useRefreshGrid } from '@Pimcore/modules/element/actions/refresh-grid/use-refresh-grid'

export interface UseDeleteHookReturn {
deleteElement: (id: number, label: string, parentId?: number) => void
deleteTreeContextMenuItem: (node: TreeNodeProps) => ItemType
deleteContextMenuItem: (node: Element, onFinish?: () => void) => ItemType
deleteGridContextMenuItem: (row: any) => ItemType | undefined
deleteMutation: (id: number, parentId?: number) => Promise<void>
}

Expand All @@ -40,6 +43,8 @@ export const useDelete = (elementType: ElementType): UseDeleteHookReturn => {
const modal = useFormModal()
const { addJob } = useJobs()
const { refreshTree } = useRefreshTree(elementType)
const { refreshGrid } = useRefreshGrid(elementType)
const { getElementById } = useElementApi(elementType)
const [elementDelete] = useElementDeleteMutation()

const deleteElement = (id: number, label: string, parentId?: number, onFinish?: () => void): void => {
Expand Down Expand Up @@ -83,6 +88,35 @@ export const useDelete = (elementType: ElementType): UseDeleteHookReturn => {
}
}

const deleteGridContextMenuItem = (row: any): ItemType | undefined => {
const data: GridContextMenuProps = row.original ?? {}
if (data.id === undefined || data.isLocked === undefined || data.permissions === undefined) {
return
}

return {
label: t('element.delete'),
key: 'delete',
icon: <Icon value={ 'trash' } />,
hidden: !checkElementPermission(data.permissions, 'delete') || data.isLocked,
onClick: async () => {
await stagedLoading(data.id)
}
}
}

const stagedLoading = async (id: GridContextMenuProps['id']): Promise<void> => {
const node = await getElementById(id)

const parentId = node!.parentId ?? undefined
deleteElement(
node!.id,
getElementKey(node!, elementType),
parentId,
() => { refreshGrid() }
)
}

const deleteMutation = async (id: number, parentId?: number, onFinish?: () => void): Promise<void> => {
const promise = elementDelete({
id,
Expand Down Expand Up @@ -121,6 +155,7 @@ export const useDelete = (elementType: ElementType): UseDeleteHookReturn => {
deleteElement,
deleteTreeContextMenuItem,
deleteContextMenuItem,
deleteGridContextMenuItem,
deleteMutation
}
}
25 changes: 24 additions & 1 deletion assets/js/src/core/modules/element/actions/open/open.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import { useTranslation } from 'react-i18next'
import { type Element } from '@Pimcore/modules/element/element-helper'
import { useElementHelper } from '@Pimcore/modules/element/hooks/use-element-helper'
import { type ElementType } from 'types/element-type.d'
import type { GridContextMenuProps } from '@Pimcore/components/grid/grid'

export interface UseOpenHookReturn {
openContextMenuItem: (node: Element, onFinish?: () => void) => ItemType
openGridContextMenuItem: (row: any) => ItemType | undefined
}

export const useOpen = (elementType: ElementType): UseOpenHookReturn => {
Expand All @@ -43,7 +45,28 @@ export const useOpen = (elementType: ElementType): UseOpenHookReturn => {
}
}

const openGridContextMenuItem = (row: any): ItemType | undefined => {
const data: GridContextMenuProps = row.original ?? {}
if (data.id === undefined || data.isLocked === undefined || data.permissions === undefined) {
return
}

return {
label: t('element.open'),
key: 'open',
icon: <Icon value={ 'open-folder' } />,
hidden: !checkElementPermission(data.permissions, 'view'),
onClick: async () => {
await openElement({
id: data.id,
type: elementType
})
}
}
}

return {
openContextMenuItem
openContextMenuItem,
openGridContextMenuItem
}
}
Loading

0 comments on commit 1abf1d5

Please sign in to comment.