From 6098758f3449ef5d2a6da1fca3702890281bc5d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?=
<76204082+iamsedatozturk@users.noreply.github.com>
Date: Wed, 10 Jun 2026 17:56:00 +0300
Subject: [PATCH] =?UTF-8?q?Grid,=20Tree=20ve=20Gantt=20i=C3=A7in=20ImageVi?=
=?UTF-8?q?ewer=20s=C3=BCtun=20yap=C4=B1s=C4=B1=20eklendi?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../GridOptionsDto/EditingFormDto.cs | 2 +-
.../Seeds/ListFormSeeder_Administration.cs | 10 +-
.../PlatformConsts.cs | 1 +
ui/src/proxy/form/models.ts | 1 +
.../JsonRowOpDialogEditForm.tsx | 4 +-
ui/src/views/admin/listForm/edit/options.ts | 1 +
ui/src/views/form/FormDevExpress.tsx | 21 ++
.../editors/ImageViewerEditorComponent.tsx | 154 ++++++++++++++
ui/src/views/form/types.ts | 6 +-
ui/src/views/form/useFormData.tsx | 4 +-
ui/src/views/list/GanttView.tsx | 2 +-
ui/src/views/list/Grid.tsx | 22 +-
ui/src/views/list/SchedulerView.tsx | 3 +-
ui/src/views/list/Tree.tsx | 22 +-
.../editors/ImageViewerEditorComponent.tsx | 198 ++++++++++++++++++
ui/src/views/list/useListFormColumns.ts | 144 +++++++++++--
16 files changed, 564 insertions(+), 31 deletions(-)
create mode 100644 ui/src/views/form/editors/ImageViewerEditorComponent.tsx
create mode 100644 ui/src/views/list/editors/ImageViewerEditorComponent.tsx
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/EditingFormDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/EditingFormDto.cs
index 9a53ace..9e62922 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/EditingFormDto.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/EditingFormDto.cs
@@ -30,7 +30,7 @@ public class EditingFormDto
public class EditingFormItemDto
{
///
- /// DataField Accepted Values: 'dxAutocomplete' | 'dxCalendar' | 'dxCheckBox' | 'dxColorBox' | 'dxDateBox' | 'dxDateRangeBox' | 'dxDropDownBox' | 'dxHtmlEditor' | 'dxLookup' | 'dxNumberBox' | 'dxRadioGroup' | 'dxRangeSlider' | 'dxSelectBox' | 'dxSlider' | 'dxSwitch' | 'dxTagBox' | 'dxTextArea' | 'dxTextBox';
+ /// DataField Accepted Values: 'dxAutocomplete' | 'dxCalendar' | 'dxCheckBox' | 'dxColorBox' | 'dxDateBox' | 'dxDateRangeBox' | 'dxDropDownBox' | 'dxHtmlEditor' | 'dxLookup' | 'dxNumberBox' | 'dxRadioGroup' | 'dxRangeSlider' | 'dxSelectBox' | 'dxSlider' | 'dxSwitch' | 'dxTagBox' | 'dxTextArea' | 'dxTextBox' | 'dxImageViewer' | 'dxImageUpload';
///
[JsonPropertyName("order")]
public int Order { get; set; }
diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs
index 7c4e1fb..82c864a 100644
--- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs
+++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs
@@ -3728,11 +3728,11 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
{
new() {
Order=1, ColCount=2, ColSpan=1, ItemType="group", Items = [
- new EditingFormItemDto { Order = 1, DataField = "Content", ColSpan=2, EditorType2 = EditorTypes.dxHtmlEditor, EditorOptions = EditorOptionValues.HtmlEditorOptions },
- new EditingFormItemDto { Order = 2, DataField = "UserId", ColSpan=1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
- new EditingFormItemDto { Order = 3, DataField = "LikeCount", ColSpan=1, EditorType2 = EditorTypes.dxNumberBox },
- new EditingFormItemDto { Order = 4, DataField = "IsLiked", ColSpan=1, EditorType2 = EditorTypes.dxCheckBox },
- new EditingFormItemDto { Order = 5, DataField = "IsOwnPost", ColSpan=1, EditorType2 = EditorTypes.dxCheckBox }
+ new EditingFormItemDto { Order = 1, DataField = "UserId", ColSpan=1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
+ new EditingFormItemDto { Order = 2, DataField = "LikeCount", ColSpan=1, EditorType2 = EditorTypes.dxNumberBox },
+ new EditingFormItemDto { Order = 3, DataField = "IsLiked", ColSpan=1, EditorType2 = EditorTypes.dxCheckBox },
+ new EditingFormItemDto { Order = 4, DataField = "IsOwnPost", ColSpan=1, EditorType2 = EditorTypes.dxCheckBox },
+ new EditingFormItemDto { Order = 5, DataField = "Content", ColSpan=2, EditorType2 = EditorTypes.dxHtmlEditor, EditorOptions = EditorOptionValues.HtmlEditorOptions },
]}
}),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[]
diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs
index 1cdb54f..1b6fdd9 100644
--- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs
+++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs
@@ -766,6 +766,7 @@ public static class PlatformConsts
public const string dxTagBox = "dxTagBox";
public const string dxTextArea = "dxTextArea";
public const string dxTextBox = "dxTextBox";
+ public const string dxImageViewer = "dxImageViewer";
public const string dxImageUpload = "dxImageUpload";
}
diff --git a/ui/src/proxy/form/models.ts b/ui/src/proxy/form/models.ts
index 01cd1da..709806b 100644
--- a/ui/src/proxy/form/models.ts
+++ b/ui/src/proxy/form/models.ts
@@ -837,6 +837,7 @@ export enum SubFormTabTypeEnum {
export enum PlatformEditorTypes {
dxTagBox = 'dxTagBox',
dxGridBox = 'dxGridBox',
+ dxImageViewer = 'dxImageViewer',
dxImageUpload = 'dxImageUpload',
}
diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx b/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx
index d73138f..c797d28 100644
--- a/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx
+++ b/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx
@@ -373,7 +373,7 @@ function JsonRowOpDialogEditForm({
placeholder="Order"
/>
-
+
-
+
+ ) : formItem.editorType2 === PlatformEditorTypes.dxImageViewer ? (
+ (
+
+ )}
+ label={{
+ text: translate('::' + formItem.colData?.captionName),
+ className: 'font-semibold',
+ }}
+ >
) : (
{
+ if (!value) return undefined
+ if (typeof value === 'object') return value as Record
+ if (typeof value !== 'string') return undefined
+
+ try {
+ const parsed = JSON.parse(value)
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
+ ? (parsed as Record)
+ : undefined
+ } catch {
+ return undefined
+ }
+}
+
+const normalizeImageSize = (value: unknown, fallback: number) => {
+ const size = Number(value)
+ return Number.isFinite(size) && size > 0 ? size : fallback
+}
+
+const getImageSource = (value: unknown) => {
+ if (!value) return ''
+ if (typeof value === 'string') return value.trim()
+
+ if (typeof value === 'object') {
+ const item = value as Record
+ return String(item.url ?? item.src ?? item.fileUrl ?? item.path ?? item.value ?? '').trim()
+ }
+
+ return String(value).trim()
+}
+
+const isProbablyBase64Image = (value: string) =>
+ value.length > 80 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
+
+const toImageSource = (value: string) => {
+ const trimmed = value.trim()
+ if (!trimmed) return ''
+ if (trimmed.startsWith('data:image/') || !isProbablyBase64Image(trimmed)) return trimmed
+ return `data:image/jpeg;base64,${trimmed}`
+}
+
+const splitImageString = (value: string) => {
+ const trimmed = value.trim()
+ if (!trimmed) return []
+ if (trimmed.startsWith('data:image/')) return [trimmed]
+
+ return trimmed
+ .split(/\r?\n|\|\s*/)
+ .flatMap((part) => {
+ const text = part.trim()
+ if (!text || text.startsWith('data:image/')) return text ? [text] : []
+ return text.split(',').map((item) => item.trim())
+ })
+ .map(toImageSource)
+ .filter(Boolean)
+}
+
+const normalizeImageValue = (value: unknown): string[] => {
+ if (Array.isArray(value)) {
+ return value.flatMap((item) => normalizeImageValue(item))
+ }
+
+ if (typeof value === 'string') {
+ const trimmed = value.trim()
+ if (!trimmed) return []
+
+ if (trimmed.startsWith('[')) {
+ try {
+ const parsed = JSON.parse(trimmed)
+ if (Array.isArray(parsed)) {
+ return parsed.flatMap((item) => normalizeImageValue(item))
+ }
+ } catch {
+ return [toImageSource(trimmed)]
+ }
+ }
+
+ return splitImageString(trimmed)
+ }
+
+ const source = getImageSource(value)
+ return source ? [toImageSource(source)] : []
+}
+
+const openImage = (url: string) => {
+ if (!url.startsWith('data:image/')) {
+ window.open(url, '_blank', 'noopener,noreferrer')
+ return
+ }
+
+ try {
+ const match = url.match(/^data:(image\/[^;]+);base64,(.*)$/)
+ if (!match) return
+
+ const binary = atob(match[2])
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
+ const blobUrl = URL.createObjectURL(new Blob([bytes], { type: match[1] }))
+ window.open(blobUrl, '_blank', 'noopener,noreferrer')
+ window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60000)
+ } catch {
+ window.open(url, '_blank', 'noopener,noreferrer')
+ }
+}
+
+const ImageViewerEditorComponent = ({
+ value,
+ options,
+ editorOptions,
+}: {
+ value?: any
+ options?: ImageUploadOptionsDto
+ editorOptions?: any
+}): ReactElement => {
+ const resolvedOptions = options ?? parseJsonObject(editorOptions) ?? editorOptions ?? {}
+ const urls = normalizeImageValue(value)
+ const thumbW = normalizeImageSize(resolvedOptions?.width, 80)
+ const thumbH = normalizeImageSize(resolvedOptions?.height, 80)
+
+ if (!urls.length) {
+ return -
+ }
+
+ return (
+
+ {urls.map((url, index) => (
+
+ ))}
+
+ )
+}
+
+export { ImageViewerEditorComponent }
diff --git a/ui/src/views/form/types.ts b/ui/src/views/form/types.ts
index 16c8175..6638f07 100644
--- a/ui/src/views/form/types.ts
+++ b/ui/src/views/form/types.ts
@@ -9,7 +9,11 @@ import {
} from '../../proxy/form/models'
import { Meta } from '@/proxy/routes/routes'
-export type EditorType2 = FormItemComponent | PlatformEditorTypes.dxGridBox | PlatformEditorTypes.dxImageUpload
+export type EditorType2 =
+ | FormItemComponent
+ | PlatformEditorTypes.dxGridBox
+ | PlatformEditorTypes.dxImageViewer
+ | PlatformEditorTypes.dxImageUpload
export type SimpleItemWithColData = Overwrite<
SimpleItem,
{
diff --git a/ui/src/views/form/useFormData.tsx b/ui/src/views/form/useFormData.tsx
index da15a13..ae813ec 100644
--- a/ui/src/views/form/useFormData.tsx
+++ b/ui/src/views/form/useFormData.tsx
@@ -275,7 +275,8 @@ const useGridData = (props: {
editorType:
i.editorType2 == PlatformEditorTypes.dxGridBox
? 'dxDropDownBox'
- : i.editorType2 == PlatformEditorTypes.dxImageUpload
+ : i.editorType2 == PlatformEditorTypes.dxImageUpload ||
+ i.editorType2 == PlatformEditorTypes.dxImageViewer
? undefined
: i.editorType2,
colSpan: i.colSpan,
@@ -283,6 +284,7 @@ const useGridData = (props: {
colData,
tagBoxOptions: i.tagBoxOptions,
gridBoxOptions: i.gridBoxOptions,
+ imageUploadOptions: i.imageUploadOptions,
editorScript: i.editorScript,
}
if (i.dataField.indexOf(':') >= 0) {
diff --git a/ui/src/views/list/GanttView.tsx b/ui/src/views/list/GanttView.tsx
index 1d58cd7..fc8f9cc 100644
--- a/ui/src/views/list/GanttView.tsx
+++ b/ui/src/views/list/GanttView.tsx
@@ -244,7 +244,7 @@ const GanttView = (props: GanttViewProps) => {
progressExpr={gridDto.gridOptions.ganttOptionDto?.progressExpr}
/>
-
+
diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx
index 1ef628d..881dee9 100644
--- a/ui/src/views/list/Grid.tsx
+++ b/ui/src/views/list/Grid.tsx
@@ -63,6 +63,7 @@ import {
} from './Utils'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
+import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { useFilters } from './useFilters'
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
@@ -1247,7 +1248,8 @@ const Grid = (props: GridProps) => {
editorType:
i.editorType2 == PlatformEditorTypes.dxGridBox
? 'dxDropDownBox'
- : i.editorType2 == PlatformEditorTypes.dxImageUpload
+ : i.editorType2 == PlatformEditorTypes.dxImageUpload ||
+ i.editorType2 == PlatformEditorTypes.dxImageViewer
? undefined
: i.editorType2,
colSpan: i.colSpan,
@@ -1255,6 +1257,14 @@ const Grid = (props: GridProps) => {
editorScript: i.editorScript,
}
+ if (i.editorType2 == PlatformEditorTypes.dxImageViewer) {
+ item.template = 'cellEditImageViewer'
+ item.editorOptions = {
+ ...item.editorOptions,
+ value: getValueByField(editingFormDataRef.current, i.dataField),
+ }
+ }
+
if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) }
}
@@ -1822,6 +1832,15 @@ const Grid = (props: GridProps) => {
+ (
+
+ )}
+ />
{
0 || filterToolbarData.length > 0}
- multiline
>
{toolbarData.map((item) => (
diff --git a/ui/src/views/list/SchedulerView.tsx b/ui/src/views/list/SchedulerView.tsx
index e6ac344..a10f1c1 100644
--- a/ui/src/views/list/SchedulerView.tsx
+++ b/ui/src/views/list/SchedulerView.tsx
@@ -412,7 +412,8 @@ const SchedulerView = (props: SchedulerViewProps) => {
editorType:
i.editorType2 == PlatformEditorTypes.dxGridBox
? 'dxDropDownBox'
- : i.editorType2 == PlatformEditorTypes.dxImageUpload
+ : i.editorType2 == PlatformEditorTypes.dxImageUpload ||
+ i.editorType2 == PlatformEditorTypes.dxImageViewer
? undefined
: i.editorType2,
colSpan: i.colSpan,
diff --git a/ui/src/views/list/Tree.tsx b/ui/src/views/list/Tree.tsx
index c92eee7..aecdbde 100644
--- a/ui/src/views/list/Tree.tsx
+++ b/ui/src/views/list/Tree.tsx
@@ -53,6 +53,7 @@ import {
setGridPanelColor,
} from './Utils'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
+import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { useFilters } from './useFilters'
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
@@ -1504,7 +1505,8 @@ const Tree = (props: TreeProps) => {
editorType:
i.editorType2 == PlatformEditorTypes.dxGridBox
? 'dxDropDownBox'
- : i.editorType2 == PlatformEditorTypes.dxImageUpload
+ : i.editorType2 == PlatformEditorTypes.dxImageUpload ||
+ i.editorType2 == PlatformEditorTypes.dxImageViewer
? undefined
: i.editorType2,
colSpan: i.colSpan,
@@ -1512,6 +1514,14 @@ const Tree = (props: TreeProps) => {
editorScript: i.editorScript,
}
+ if (i.editorType2 == PlatformEditorTypes.dxImageViewer) {
+ item.template = 'cellEditImageViewer'
+ item.editorOptions = {
+ ...item.editorOptions,
+ value: getValueByField(editingFormDataRef.current, i.dataField),
+ }
+ }
+
if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) }
}
@@ -1592,6 +1602,15 @@ const Tree = (props: TreeProps) => {
>
+ (
+
+ )}
+ />
{
0 || (filterToolbarData?.length ?? 0) > 0}
- multiline
>
{toolbarData?.map((item) => (
diff --git a/ui/src/views/list/editors/ImageViewerEditorComponent.tsx b/ui/src/views/list/editors/ImageViewerEditorComponent.tsx
new file mode 100644
index 0000000..d0f76cf
--- /dev/null
+++ b/ui/src/views/list/editors/ImageViewerEditorComponent.tsx
@@ -0,0 +1,198 @@
+import { ReactElement } from 'react'
+
+const parseJsonObject = (value: unknown) => {
+ if (!value) return undefined
+ if (typeof value === 'object') return value as Record
+ if (typeof value !== 'string') return undefined
+
+ try {
+ const parsed = JSON.parse(value)
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
+ ? (parsed as Record)
+ : undefined
+ } catch {
+ return undefined
+ }
+}
+
+const normalizeImageSize = (value: unknown, fallback: number) => {
+ const size = Number(value)
+ return Number.isFinite(size) && size > 0 ? size : fallback
+}
+
+const getImageSource = (value: unknown) => {
+ if (!value) return ''
+ if (typeof value === 'string') return value.trim()
+
+ if (typeof value === 'object') {
+ const item = value as Record
+ return String(item.url ?? item.src ?? item.fileUrl ?? item.path ?? item.value ?? '').trim()
+ }
+
+ return String(value).trim()
+}
+
+const isProbablyBase64Image = (value: string) =>
+ value.length > 80 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
+
+const toImageSource = (value: string) => {
+ const trimmed = value.trim()
+ if (!trimmed) return ''
+ if (trimmed.startsWith('data:image/') || !isProbablyBase64Image(trimmed)) return trimmed
+ return `data:image/jpeg;base64,${trimmed}`
+}
+
+const splitImageString = (value: string) => {
+ const trimmed = value.trim()
+ if (!trimmed) return []
+ if (trimmed.startsWith('data:image/')) return [trimmed]
+
+ return trimmed
+ .split(/\r?\n|\|\s*/)
+ .flatMap((part) => {
+ const text = part.trim()
+ if (!text || text.startsWith('data:image/')) return text ? [text] : []
+ return text.split(',').map((item) => item.trim())
+ })
+ .map(toImageSource)
+ .filter(Boolean)
+}
+
+const normalizeImageValue = (value: unknown): string[] => {
+ if (Array.isArray(value)) {
+ return value.flatMap((item) => normalizeImageValue(item))
+ }
+
+ if (typeof value === 'string') {
+ const trimmed = value.trim()
+ if (!trimmed) return []
+
+ if (trimmed.startsWith('[')) {
+ try {
+ const parsed = JSON.parse(trimmed)
+ if (Array.isArray(parsed)) {
+ return parsed.flatMap((item) => normalizeImageValue(item))
+ }
+ } catch {
+ return [toImageSource(trimmed)]
+ }
+ }
+
+ return splitImageString(trimmed)
+ }
+
+ const source = getImageSource(value)
+ return source ? [toImageSource(source)] : []
+}
+
+const openImage = (url: string) => {
+ if (!url.startsWith('data:image/')) {
+ window.open(url, '_blank', 'noopener,noreferrer')
+ return
+ }
+
+ try {
+ const match = url.match(/^data:(image\/[^;]+);base64,(.*)$/)
+ if (!match) return
+
+ const binary = atob(match[2])
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
+ const blobUrl = URL.createObjectURL(new Blob([bytes], { type: match[1] }))
+ window.open(blobUrl, '_blank', 'noopener,noreferrer')
+ window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60000)
+ } catch {
+ window.open(url, '_blank', 'noopener,noreferrer')
+ }
+}
+
+const resolveTemplateValue = (templateData: any) => {
+ const dataField =
+ templateData?.dataField ||
+ templateData?.item?.dataField ||
+ templateData?.editorOptions?.name ||
+ templateData?.name
+
+ const fieldCandidates = String(dataField || '')
+ .split(':')
+ .reduce(
+ (fields, field) => {
+ const value = field.trim()
+ return value ? [...fields, value] : fields
+ },
+ dataField ? [String(dataField)] : [],
+ )
+
+ const formDataSources = [
+ templateData?.component?.option?.('formData'),
+ templateData?.formData,
+ templateData?.data,
+ templateData?.fallbackFormData,
+ ].filter((source) => source && typeof source === 'object')
+
+ for (const formData of formDataSources) {
+ for (const field of fieldCandidates) {
+ if (Object.prototype.hasOwnProperty.call(formData, field)) {
+ return formData[field]
+ }
+ }
+
+ const actualKey = Object.keys(formData).find((key) =>
+ fieldCandidates.some((field) => key.toLowerCase() === field.toLowerCase()),
+ )
+ if (actualKey) {
+ return formData[actualKey]
+ }
+ }
+
+ return templateData?.value ?? templateData?.editorOptions?.value
+}
+
+const resolveTemplateOptions = (templateData: any) => {
+ const itemOptions =
+ templateData?.editorOptions?.imageUploadOptions ??
+ templateData?.imageUploadOptions ??
+ parseJsonObject(templateData?.editorOptions?.editorOptions) ??
+ parseJsonObject(templateData?.editorOptions)
+
+ return itemOptions && typeof itemOptions === 'object' ? itemOptions : {}
+}
+
+const ImageViewerEditorComponent = (templateData: any): ReactElement => {
+ const value = resolveTemplateValue(templateData)
+ const options = resolveTemplateOptions(templateData)
+ const urls = normalizeImageValue(value)
+ const thumbW = normalizeImageSize(options?.width, 80)
+ const thumbH = normalizeImageSize(options?.height, 80)
+
+ if (!urls.length) {
+ return -
+ }
+
+ return (
+
+ {urls.map((url, index) => (
+
+ ))}
+
+ )
+}
+
+export { ImageViewerEditorComponent }
diff --git a/ui/src/views/list/useListFormColumns.ts b/ui/src/views/list/useListFormColumns.ts
index 372b349..4286a0b 100644
--- a/ui/src/views/list/useListFormColumns.ts
+++ b/ui/src/views/list/useListFormColumns.ts
@@ -137,16 +137,10 @@ const cellTemplateImage = (
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 w: number = 40
- const h: number = 40
+ const urls = normalizeImageCellValue(cellInfo.value)
+ const imgOptions = getImageColumnOptions(cellInfo.column)
+ const w = normalizeImageSize(imgOptions.width, 40)
+ const h = normalizeImageSize(imgOptions.height, 40)
cellElement.style.cssText += 'display:flex;flex-wrap:wrap;align-items:center;gap:4px;'
cellElement.innerHTML = ''
@@ -161,12 +155,128 @@ const cellTemplateImage = (
img.addEventListener('mouseenter', (e) => showImgPreview(url, e as MouseEvent))
img.addEventListener('mousemove', (e) => showImgPreview(url, e as MouseEvent))
img.addEventListener('mouseleave', hideImgPreview)
+ img.addEventListener('click', () => openImageSource(url))
cellElement.appendChild(img)
})
}
}
+const normalizeImageSize = (value: unknown, fallback: number) => {
+ const size = Number(value)
+ return Number.isFinite(size) && size > 0 ? size : fallback
+}
+
+const parseJsonObject = (value: unknown) => {
+ if (!value) return undefined
+ if (typeof value === 'object') return value as Record
+ if (typeof value !== 'string') return undefined
+
+ try {
+ const parsed = JSON.parse(value)
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
+ ? (parsed as Record)
+ : undefined
+ } catch {
+ return undefined
+ }
+}
+
+const getImageColumnOptions = (column: unknown) => {
+ const col = column as any
+ return (
+ col?.extras?.imageUploadOptions ??
+ parseJsonObject(col?.extras?.editorOptions) ??
+ parseJsonObject(col?.editorOptions) ??
+ {}
+ )
+}
+
+const getImageSource = (value: unknown) => {
+ if (!value) return ''
+ if (typeof value === 'string') return value.trim()
+
+ if (typeof value === 'object') {
+ const item = value as Record
+ return String(item.url ?? item.src ?? item.fileUrl ?? item.path ?? item.value ?? '').trim()
+ }
+
+ return String(value).trim()
+}
+
+const isProbablyBase64Image = (value: string) =>
+ value.length > 80 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
+
+const toImageSource = (value: string) => {
+ const trimmed = value.trim()
+ if (!trimmed) return ''
+ if (trimmed.startsWith('data:image/') || !isProbablyBase64Image(trimmed)) return trimmed
+ return `data:image/jpeg;base64,${trimmed}`
+}
+
+const openImageSource = (url: string) => {
+ if (!url.startsWith('data:image/')) {
+ window.open(url, '_blank', 'noopener,noreferrer')
+ return
+ }
+
+ try {
+ const match = url.match(/^data:(image\/[^;]+);base64,(.*)$/)
+ if (!match) return
+
+ const binary = atob(match[2])
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
+ const blobUrl = URL.createObjectURL(new Blob([bytes], { type: match[1] }))
+ window.open(blobUrl, '_blank', 'noopener,noreferrer')
+ window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60000)
+ } catch {
+ window.open(url, '_blank', 'noopener,noreferrer')
+ }
+}
+
+const splitImageString = (value: string) => {
+ const trimmed = value.trim()
+ if (!trimmed) return []
+ if (trimmed.startsWith('data:image/')) return [trimmed]
+
+ return trimmed
+ .split(/\r?\n|\|\s*/)
+ .flatMap((part) => {
+ const text = part.trim()
+ if (!text || text.startsWith('data:image/')) return text ? [text] : []
+ return text.split(',').map((item) => item.trim())
+ })
+ .map(toImageSource)
+ .filter(Boolean)
+}
+
+const normalizeImageCellValue = (value: unknown): string[] => {
+ if (Array.isArray(value)) {
+ return value.flatMap((item) => normalizeImageCellValue(item))
+ }
+
+ if (typeof value === 'string') {
+ const trimmed = value.trim()
+ if (!trimmed) return []
+
+ if (trimmed.startsWith('[')) {
+ try {
+ const parsed = JSON.parse(trimmed)
+ if (Array.isArray(parsed)) {
+ return parsed.flatMap((item) => normalizeImageCellValue(item))
+ }
+ } catch {
+ return [toImageSource(trimmed)]
+ }
+ }
+
+ return splitImageString(trimmed)
+ }
+
+ const source = getImageSource(value)
+ return source ? [toImageSource(source)] : []
+}
+
function calculateFilterExpressionMultiValue(
this: DataGridTypes.Column,
filterValue: any,
@@ -777,11 +887,13 @@ const useListFormColumns = ({
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) {
+ const isImageUploadEditor = imageFormItem?.editorType2 === PlatformEditorTypes.dxImageUpload
+ const isImageViewerEditor = imageFormItem?.editorType2 === PlatformEditorTypes.dxImageViewer
+ if (isImageUploadEditor || isImageViewerEditor) {
// 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) {
+ let imageUploadOptions = imageFormItem?.imageUploadOptions
+ if (!imageUploadOptions && imageFormItem?.editorOptions) {
try {
imageUploadOptions = JSON.parse(imageFormItem.editorOptions)
} catch {
@@ -801,10 +913,12 @@ const useListFormColumns = ({
}
column.extras = {
multiValue: imageUploadOptions?.multiple ?? false,
- editorOptions: imageFormItem.editorOptions,
+ editorOptions: imageFormItem?.editorOptions ?? colData.editorOptions,
imageUploadOptions: imageUploadOptions,
}
- column.editCellTemplate = 'cellEditImageUpload'
+ if (isImageUploadEditor) {
+ column.editCellTemplate = 'cellEditImageUpload'
+ }
column.cellTemplate = cellTemplateImage as any
}
}