Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added upload new version context menu option #783

Merged
merged 14 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/build/api/docs.jsonopenapi.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@ export type InputFormModalProps = Omit<ModalFuncProps, 'content'> & {
initialValue?: string
}

interface UploadFormProps extends Omit<InputFormModalProps, 'initialValues'> {
accept?: string
}

export interface UseFormModalHookResponse {
input: (props: InputFormModalProps) => { destroy: () => void, update: (configUpdate: ConfigUpdate) => void }
confirm: (props: ModalFuncProps) => { destroy: () => void, update: (configUpdate: ConfigUpdate) => void }
upload: (props: UploadFormProps) => { destroy: () => void, update: (configUpdate: ConfigUpdate) => void }
}

export function useFormModal (): UseFormModalHookResponse {
Expand All @@ -52,7 +57,8 @@ export function useFormModal (): UseFormModalHookResponse {
modalResult.then(() => {}, () => {})
return modalResult
},
confirm: (props) => modal.confirm(withConfirm(props))
confirm: (props) => modal.confirm(withConfirm(props)),
upload: (props) => modal.confirm(withUpload(props))
}),
[]
)
Expand Down Expand Up @@ -139,3 +145,69 @@ export function withConfirm (props: ModalFuncProps): ModalFuncProps {
cancelText: props.cancelText ?? i18n.t('no')
}
}

export function withUpload (props: UploadFormProps): ModalFuncProps {
const inputRef = React.createRef<InputRef>()
const uuid = pimcoreUUid()
const fieldName = `upload-${uuid}`
const {
label,
rule,
accept,
...modalProps
} = props

let formattedRule: Rule[] = []
if (rule !== undefined) {
formattedRule = [rule]
}

const UploadForm = forwardRef(function InputForm (props: InputFormProps, ref: RefObject<InputRef>): React.JSX.Element {
return (
<Form
form={ props.form }
initialValues={ props.initialValues }
layout={ 'vertical' }
>
<Form.Item
label={ label }
name={ props.fieldName }
rules={ formattedRule }
>
<Input
accept={ accept }
ref={ ref }
type="file"
/>
</Form.Item>
</Form>
)
})

return {
...modalProps,
type: props.type ?? 'confirm',
icon: props.icon ?? null,
onOk: async () => {
return await new Promise((resolve, reject) => {
form!.validateFields()
.then(() => {
const files = inputRef.current!.input!.files

props.onOk?.(files)
resolve(files)
})
.catch(() => {
reject(new Error('Invalid form'))
})
})
},
content: <UploadForm
fieldName={ fieldName }
form={ form! }
initialValues={ {} }
key={ 'upload-form' }
ref={ inputRef }
/>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Pimcore
*
* This source file is available under two different licenses:
* - Pimcore Open Core License (POCL)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license https://github.com/pimcore/studio-ui-bundle/blob/1.x/LICENSE.md POCL and PCL
*/

import { type Asset, type AssetReplaceApiResponse, useAssetReplaceMutation } from '@Pimcore/modules/asset/asset-api-slice.gen'
import { type TreeNodeProps } from '@Pimcore/components/element-tree/node/tree-node'
import { type ItemType } from '@Pimcore/components/dropdown/dropdown'
import { useTranslation } from 'react-i18next'
import { Icon } from '@Pimcore/components/icon/icon'
import React from 'react'
import { useFormModal } from '@Pimcore/components/modal/form-modal/hooks/use-form-modal'
import { useMessage } from '@Pimcore/components/message/useMessage'
import { useCacheUpdate } from '@Pimcore/modules/element/hooks/use-cache-update'

export interface UseUploadNewVersionReturn {
uploadNewVersion: (id: number, accept?: string) => void
uploadNewVersionContextMenuItem: (node: Asset, onFinish?: () => void) => ItemType
uploadNewVersionTreeContextMenuItem: (node: TreeNodeProps) => ItemType
}

export const useUploadNewVersion = (): UseUploadNewVersionReturn => {
const { t } = useTranslation()
const modal = useFormModal()
const messageApi = useMessage()
const { updateFieldValue } = useCacheUpdate('asset', ['ASSET_TREE'])
const [replaceAsset] = useAssetReplaceMutation()

const uploadNewVersion = (id: number, accept?: string, onFinish?: () => void): void => {
modal.upload({
title: t('asset.upload'),
label: t('asset.upload.label'),
accept,
rule: {
required: true,
message: t('element.rename.validation')
},
onOk: async (value: FileList) => {
const file = value[0]

await uploadNewVersionMutation(id, file)
onFinish?.()
}
})
}

const uploadNewVersionMutation = async (id: number, file: File): Promise<void> => {
const formData = new FormData()
formData.append('file', file)

const replaceAssetTask = replaceAsset({
id,
body: formData as any
})

try {
const response = (await replaceAssetTask) as any

if (response.error !== undefined) {
throw new Error(response.error.data.error as string)
}

const data = response.data as AssetReplaceApiResponse
updateFieldValue(
id,
'filename',
data.data
)
} catch (e: any) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
messageApi.error({
content: e.message
})
}
}

const uploadNewVersionContextMenuItem = (node: Asset, onFinish?: () => void): ItemType => {
return {
label: t('asset.tree.context-menu.upload-new-version'),
key: 'upload-new-version',
icon: <Icon value={ 'upload-cloud' } />,
hidden: node.type === 'folder',
onClick: () => {
uploadNewVersion(
node.id,
node.mimeType as string | undefined,
onFinish
)
}
}
}

const uploadNewVersionTreeContextMenuItem = (node: TreeNodeProps): ItemType => {
return {
label: t('asset.tree.context-menu.upload-new-version'),
key: 'upload-new-version',
icon: <Icon value={ 'upload-cloud' } />,
hidden: node.type === 'folder',
onClick: () => {
uploadNewVersion(
parseInt(node.id),
node.metaData.asset.mimeType as string | undefined
)
}
}
}

return {
uploadNewVersion,
uploadNewVersionTreeContextMenuItem,
uploadNewVersionContextMenuItem
}
}
50 changes: 49 additions & 1 deletion assets/js/src/core/modules/asset/asset-api-slice.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,24 @@ const injectedRtkApi = api
}),
providesTags: ["Assets"],
}),
assetImageStreamCustom: build.query<AssetImageStreamCustomApiResponse, AssetImageStreamCustomApiArg>({
query: (queryArg) => ({
url: `/pimcore-studio/api/assets/${queryArg.id}/image/stream/custom`,
params: {
mimeType: queryArg.mimeType,
resizeMode: queryArg.resizeMode,
width: queryArg.width,
height: queryArg.height,
quality: queryArg.quality,
dpi: queryArg.dpi,
contain: queryArg.contain,
frame: queryArg.frame,
cover: queryArg.cover,
forceResize: queryArg.forceResize,
},
}),
providesTags: ["Assets"],
}),
assetImageDownloadByFormat: build.query<
AssetImageDownloadByFormatApiResponse,
AssetImageDownloadByFormatApiArg
Expand Down Expand Up @@ -600,6 +618,32 @@ export type AssetImageDownloadCustomApiArg = {
/** Dpi of downloaded image */
dpi?: number;
};
export type AssetImageStreamCustomApiResponse =
/** status 200 Image asset stream based on custom thumbnail configuration */ Blob;
export type AssetImageStreamCustomApiArg = {
/** Id of the image */
id: number;
/** Mime type of downloaded image. */
mimeType: "JPEG" | "PNG";
/** Resize mode of downloaded image. */
resizeMode: "resize" | "scaleByWidth" | "scaleByHeight";
/** Width of downloaded image */
width?: number;
/** Height of downloaded image */
height?: number;
/** Quality of downloaded image */
quality?: number;
/** Dpi of downloaded image */
dpi?: number;
/** Contain */
contain?: boolean;
/** Frame */
frame?: boolean;
/** Cover */
cover?: boolean;
/** ForceResize */
forceResize?: boolean;
};
export type AssetImageDownloadByFormatApiResponse = /** status 200 Image asset binary file based on format */ Blob;
export type AssetImageDownloadByFormatApiArg = {
/** Id of the image */
Expand Down Expand Up @@ -705,7 +749,10 @@ export type AssetUploadInfoApiArg = {
/** Name of the file to upload */
fileName: string;
};
export type AssetReplaceApiResponse = /** status 200 Successfully replaced asset binary */ void;
export type AssetReplaceApiResponse = /** status 200 File name of the successfully replaced asset */ {
/** new file name of the asset */
data: string;
};
export type AssetReplaceApiArg = {
/** Id of the asset */
id: number;
Expand Down Expand Up @@ -1110,6 +1157,7 @@ export const {
useAssetUpdateGridConfigurationMutation,
useAssetGetGridMutation,
useAssetImageDownloadCustomQuery,
useAssetImageStreamCustomQuery,
useAssetImageDownloadByFormatQuery,
useAssetImageStreamPreviewQuery,
useAssetImageDownloadByThumbnailQuery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { Icon } from '@Pimcore/components/icon/icon'
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'
import { useUploadNewVersion } from '@Pimcore/modules/asset/actions/upload-new-version/upload-new-version'
import { useOpen } from '@Pimcore/modules/element/actions/open/open'

interface FlexContainerProps {
Expand All @@ -37,6 +38,7 @@ const FlexContainer = (props: FlexContainerProps): React.JSX.Element => {
const { renameContextMenuItem } = useRename('asset')
const { deleteContextMenuItem } = useDelete('asset')
const { downloadContextMenuItem } = useDownload()
const { uploadNewVersionContextMenuItem } = useUploadNewVersion()
const { openContextMenuItem } = useOpen('asset')

const cards: ReactNode[] = []
Expand Down Expand Up @@ -64,6 +66,7 @@ const FlexContainer = (props: FlexContainerProps): React.JSX.Element => {
hidden: true
},
renameContextMenuItem(asset),
uploadNewVersionContextMenuItem(asset),
downloadContextMenuItem(asset),
deleteContextMenuItem(asset)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useLock } from '@Pimcore/modules/element/actions/lock/use-lock'
import { useZipDownload } from '@Pimcore/modules/asset/actions/zip-download/use-zip-download'
import { checkElementPermission } from '@Pimcore/modules/element/permissions/permission-helper'
import { useDownload } from '@Pimcore/modules/asset/actions/download/use-download'
import { useUploadNewVersion } from '@Pimcore/modules/asset/actions/upload-new-version/upload-new-version'

export const AssetTreeContextMenu = (props: TreeContextMenuProps): React.JSX.Element => {
const { t } = useTranslation()
Expand All @@ -46,6 +47,7 @@ export const AssetTreeContextMenu = (props: TreeContextMenuProps): React.JSX.Ele
const { downloadTreeContextMenuItem } = useDownload()
const { copyTreeContextMenuItem, cutTreeContextMenuItem, pasteTreeContextMenuItem, pasteCutContextMenuItem } = useCopyPaste('asset')
const { lockTreeContextMenuItem, lockAndPropagateTreeContextMenuItem, unlockTreeContextMenuItem, unlockAndPropagateTreeContextMenuItem } = useLock('asset')
const { uploadNewVersionTreeContextMenuItem } = useUploadNewVersion()
const node = props.node

useEffect(() => {
Expand Down Expand Up @@ -91,6 +93,7 @@ export const AssetTreeContextMenu = (props: TreeContextMenuProps): React.JSX.Ele
pasteCutContextMenuItem(parseInt(props.node.id)),
deleteTreeContextMenuItem(props.node),
createZipDownloadContextMenuItem(props.node),
uploadNewVersionTreeContextMenuItem(props.node),
downloadTreeContextMenuItem(props.node),
{
label: t('element.tree.context-menu.advanced'),
Expand Down
Loading
Loading