- {/* Image if exists */}
- {announcement.imageUrl && (
-

- )}
+ {/* Images if exist */}
+ {announcement.imageUrl && (() => {
+ const images = announcement.imageUrl.split('|').filter(Boolean)
+ return images.length > 0 ? (
+
1 ? 'grid grid-cols-2 gap-2' : ''}`}>
+ {images.map((img, idx) => (
+
)
1 ? idx + 1 : ''}`.trim()}
+ className="w-full rounded-lg object-cover"
+ />
+ ))}
+
+ ) : null
+ })()}
{/* Full Content */}
diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx
index c5e5134..e667f36 100644
--- a/ui/src/views/list/Grid.tsx
+++ b/ui/src/views/list/Grid.tsx
@@ -61,6 +61,7 @@ import {
setGridPanelColor,
} from './Utils'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
+import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { useFilters } from './useFilters'
import { useToolbar } from './useToolbar'
@@ -955,7 +956,11 @@ const Grid = (props: GridProps) => {
name: i.dataField,
editorType2: i.editorType2,
editorType:
- i.editorType2 == PlatformEditorTypes.dxGridBox ? 'dxDropDownBox' : i.editorType2,
+ i.editorType2 == PlatformEditorTypes.dxGridBox
+ ? 'dxDropDownBox'
+ : i.editorType2 == PlatformEditorTypes.dxImageUpload
+ ? undefined
+ : i.editorType2,
colSpan: i.colSpan,
isRequired: i.isRequired,
editorOptions,
@@ -1357,6 +1362,7 @@ const Grid = (props: GridProps) => {
>
+
{
+ const col = cellElement.column
+ if (!col) {
+ return <>>
+ }
+
+ // options: önce column.extras.imageUploadOptions, yoksa editorOptions JSON'undan parse et
+ let options: any = col.extras?.imageUploadOptions
+ if (!options || Object.keys(options).length === 0) {
+ try {
+ const raw = col.extras?.editorOptions ?? col.editorOptions
+ if (raw) {
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
+ options = parsed
+ }
+ } catch {
+ // ignore
+ }
+ }
+ options = options ?? {}
+ // JSON'dan string olarak gelebilir: "true" / "false"
+ const isMultiple: boolean = options.multiple === true || options.multiple === 'true'
+
+ const [uploading, setUploading] = useState(false)
+ const inputRef = useRef(null)
+
+ const thumbW: number = options.width ?? 40
+ const thumbH: number = options.height ?? 40
+
+ const currentValue = cellElement.value
+ const initialUrls: string[] = currentValue
+ ? Array.isArray(currentValue)
+ ? currentValue.filter(Boolean)
+ : [currentValue].filter(Boolean)
+ : []
+
+ const [urls, setUrls] = useState(initialUrls)
+
+ const removeImage = (index: number) => {
+ const newUrls = urls.filter((_, i) => i !== index)
+ setUrls(newUrls)
+ if (isMultiple) {
+ cellElement.setValue(newUrls.length > 0 ? newUrls : null)
+ } else {
+ cellElement.setValue(newUrls[0] ?? null)
+ }
+ }
+
+ const toBase64 = (file: File): Promise =>
+ new Promise((resolve, reject) => {
+ const reader = new FileReader()
+ reader.onload = () => resolve(reader.result as string)
+ reader.onerror = reject
+ reader.readAsDataURL(file)
+ })
+
+ const handleFiles = async (files: FileList | null) => {
+ if (!files?.length) return
+
+ setUploading(true)
+ try {
+ // isMultiple=true ise mevcut url'leri koru, üstüne ekle; false ise sıfırla
+ const uploadedUrls: string[] = isMultiple ? [...urls] : []
+ for (const file of Array.from(files)) {
+ if (options.uploadUrl) {
+ // HTTP upload
+ const formData = new FormData()
+ const fileFieldName: string = options.fileFieldName ?? 'file'
+ formData.append(fileFieldName, file)
+ const res = await fetch(options.uploadUrl, { method: 'POST', body: formData })
+ if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
+ const data = await res.json()
+ const url: string = data?.url ?? data?.fileUrl ?? data?.path ?? data
+ uploadedUrls.push(url)
+ } else {
+ // uploadUrl yapılandırılmamışsa base64 olarak sakla
+ const base64 = await toBase64(file)
+ uploadedUrls.push(base64)
+ }
+ }
+
+ const newUrls = uploadedUrls.filter(Boolean)
+ setUrls(newUrls)
+ const newValue = isMultiple ? newUrls : (newUrls[newUrls.length - 1] ?? null)
+ cellElement.setValue(newValue)
+ } catch (e) {
+ console.error('ImageUploadEditorComponent upload error:', e)
+ } finally {
+ setUploading(false)
+ if (inputRef.current) inputRef.current.value = ''
+ }
+ }
+
+ return (
+
+ {urls.map((url, i) => (
+
+

+
+
+ ))}
+
+
handleFiles(e.target.files)}
+ />
+
+
+ )
+}
+
+export { ImageUploadEditorComponent }
diff --git a/ui/src/views/list/useListFormColumns.ts b/ui/src/views/list/useListFormColumns.ts
index 6d9f9ba..1b25cbe 100644
--- a/ui/src/views/list/useListFormColumns.ts
+++ b/ui/src/views/list/useListFormColumns.ts
@@ -65,6 +65,33 @@ const cellTemplateMultiValue = (
}
}
+const cellTemplateImage = (
+ cellElement: HTMLElement,
+ cellInfo: DataGridTypes.ColumnCellTemplateData,
+) => {
+ if (cellInfo?.value) {
+ const urls: string[] = Array.isArray(cellInfo.value)
+ ? cellInfo.value.filter(Boolean)
+ : [cellInfo.value].filter(Boolean)
+
+ const col = cellInfo.column as any
+ const imgOptions = col?.extras?.imageUploadOptions ?? {}
+ const w: number = imgOptions.width ?? 40
+ const h: number = imgOptions.height ?? 40
+
+ const imgs = urls
+ .map(
+ (url) =>
+ `
`,
+ )
+ .join('')
+
+ cellElement.style.cssText += 'display:flex;flex-wrap:wrap;align-items:center;gap:4px;'
+ cellElement.innerHTML = imgs
+ cellElement.title = urls.join(', ')
+ }
+}
+
function calculateFilterExpressionMultiValue(
this: DataGridTypes.Column,
filterValue: any,
@@ -641,6 +668,44 @@ const useListFormColumns = ({
}
}
// #endregion
+
+ // #region image upload editor
+ if (!colData.lookupDto?.dataSourceType) {
+ const allFormItems = gridDto.gridOptions.editingFormDto.flatMap((group) => group.items)
+ const imageFormItem = allFormItems.find((a) => a?.dataField === colData.fieldName)
+ if (imageFormItem?.editorType2 === PlatformEditorTypes.dxImageUpload) {
+ // imageUploadOptions'ı önce imageFormItem.imageUploadOptions'dan al,
+ // yoksa editorOptions JSON içinden parse et (admin panelinde editorOptions ile yapılandırılır)
+ let imageUploadOptions = imageFormItem.imageUploadOptions
+ if (!imageUploadOptions && imageFormItem.editorOptions) {
+ try {
+ imageUploadOptions = JSON.parse(imageFormItem.editorOptions)
+ } catch {
+ imageUploadOptions = undefined
+ }
+ }
+ // Kolon editorOptions'ından da dene (column-level config)
+ if (!imageUploadOptions && colData.editorOptions) {
+ try {
+ imageUploadOptions =
+ typeof colData.editorOptions === 'string'
+ ? JSON.parse(colData.editorOptions)
+ : (colData.editorOptions as any)
+ } catch {
+ imageUploadOptions = undefined
+ }
+ }
+ column.extras = {
+ multiValue: imageUploadOptions?.multiple ?? false,
+ editorOptions: imageFormItem.editorOptions,
+ imageUploadOptions: imageUploadOptions,
+ }
+ column.editCellTemplate = 'cellEditImageUpload'
+ column.cellTemplate = cellTemplateImage as any
+ }
+ }
+ // #endregion image upload editor
+
if (colData.validationRuleDto) {
// for server side validation : https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/DataValidation/jQuery/Light/
column.validationRules = colData.validationRuleDto as ValidationRule[]