Grid, Tree ve Gantt için ImageViewer sütun yapısı eklendi

This commit is contained in:
Sedat ÖZTÜRK 2026-06-10 17:56:00 +03:00
parent 92058ed5e9
commit 6098758f34
16 changed files with 564 additions and 31 deletions

View file

@ -30,7 +30,7 @@ public class EditingFormDto
public class EditingFormItemDto
{
/// <summary>
/// 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';
/// </summary>
[JsonPropertyName("order")]
public int Order { get; set; }

View file

@ -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[]

View file

@ -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";
}

View file

@ -837,6 +837,7 @@ export enum SubFormTabTypeEnum {
export enum PlatformEditorTypes {
dxTagBox = 'dxTagBox',
dxGridBox = 'dxGridBox',
dxImageViewer = 'dxImageViewer',
dxImageUpload = 'dxImageUpload',
}

View file

@ -373,7 +373,7 @@ function JsonRowOpDialogEditForm({
placeholder="Order"
/>
</div>
<div className="w-3/12 ml-2">
<div className="w-2/12 ml-2">
<Field
type="text"
autoFocus={true}
@ -405,7 +405,7 @@ function JsonRowOpDialogEditForm({
)}
</Field>
</div>
<div className="w-2/12 ml-2 flex gap-2">
<div className="w-3/12 ml-2 flex gap-2">
<Field
type="text"
autoComplete="off"

View file

@ -190,6 +190,7 @@ export const columnEditorTypeListOptions = [
{ value: 'dxDateRangeBox', label: 'dxDateRangeBox' },
{ value: 'dxDropDownBox', label: 'dxDropDownBox' },
{ value: PlatformEditorTypes.dxGridBox, label: PlatformEditorTypes.dxGridBox },
{ value: PlatformEditorTypes.dxImageViewer, label: PlatformEditorTypes.dxImageViewer },
{ value: PlatformEditorTypes.dxImageUpload, label: PlatformEditorTypes.dxImageUpload },
{ value: 'dxHtmlEditor', label: 'dxHtmlEditor' },
{ value: 'dxLookup', label: 'dxLookup' },

View file

@ -10,6 +10,7 @@ import { FieldDataChangedEvent, GroupItem } from 'devextreme/ui/form'
import { Dispatch, RefObject, useEffect, useRef, useState } from 'react'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { RowMode, SimpleItemWithColData } from './types'
import { PlatformEditorTypes } from '@/proxy/form/models'
@ -606,6 +607,26 @@ const FormDevExpress = (props: {
className: 'font-semibold',
}}
></SimpleItemDx>
) : formItem.editorType2 === PlatformEditorTypes.dxImageViewer ? (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
dataField={formItem.dataField}
name={formItem.name}
colSpan={formItem.colSpan}
isRequired={formItem.isRequired}
render={() => (
<ImageViewerEditorComponent
value={formData[formItem.dataField!]}
options={formItem.imageUploadOptions}
editorOptions={getEditorOptions(formItem)}
/>
)}
label={{
text: translate('::' + formItem.colData?.captionName),
className: 'font-semibold',
}}
></SimpleItemDx>
) : (
<SimpleItemDx
cssClass="font-semibold"

View file

@ -0,0 +1,154 @@
import { ImageUploadOptionsDto } from '@/proxy/form/models'
import { ReactElement } from 'react'
const parseJsonObject = (value: unknown) => {
if (!value) return undefined
if (typeof value === 'object') return value as Record<string, any>
if (typeof value !== 'string') return undefined
try {
const parsed = JSON.parse(value)
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as Record<string, any>)
: 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<string, unknown>
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 <div className="text-gray-400 px-1 py-2">-</div>
}
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
{urls.map((url, index) => (
<button
key={`${url}-${index}`}
type="button"
onClick={() => openImage(url)}
style={{ border: 0, background: 'transparent', padding: 0, cursor: 'zoom-in' }}
>
<img
src={url}
alt=""
style={{
width: thumbW,
height: thumbH,
objectFit: 'cover',
borderRadius: 4,
border: '1px solid #ddd',
display: 'block',
}}
/>
</button>
))}
</div>
)
}
export { ImageViewerEditorComponent }

View file

@ -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,
{

View file

@ -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) {

View file

@ -244,7 +244,7 @@ const GanttView = (props: GanttViewProps) => {
progressExpr={gridDto.gridOptions.ganttOptionDto?.progressExpr}
/>
<Toolbar multiline>
<Toolbar>
<Item name="undo" locateInMenu="auto" />
<Item name="redo" locateInMenu="auto" />
<Item name="separator" locateInMenu="auto" />

View file

@ -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) => {
<Template name={'cellEditTagBox'} render={TagBoxEditorComponent} />
<Template name={'cellEditGridBox'} render={GridBoxEditorComponent} />
<Template name={'cellEditImageUpload'} render={ImageUploadEditorComponent} />
<Template
name={'cellEditImageViewer'}
render={(data) => (
<ImageViewerEditorComponent
{...data}
fallbackFormData={editingFormDataRef.current}
/>
)}
/>
<Template name="extraFilters">
<GridExtraFilterToolbar
filters={gridDto?.gridOptions.extraFilterDto ?? []}
@ -1831,7 +1850,6 @@ const Grid = (props: GridProps) => {
</Template>
<Toolbar
visible={toolbarData.length > 0 || filterToolbarData.length > 0}
multiline
>
{toolbarData.map((item) => (
<Item key={item.name} {...item}></Item>

View file

@ -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,

View file

@ -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) => {
></Editing>
<Template name={'cellEditTagBox'} render={TagBoxEditorComponent} />
<Template name={'cellEditGridBox'} render={GridBoxEditorComponent} />
<Template
name={'cellEditImageViewer'}
render={(data) => (
<ImageViewerEditorComponent
{...data}
fallbackFormData={editingFormDataRef.current}
/>
)}
/>
<Template name="extraFilters">
<GridExtraFilterToolbar
filters={gridDto?.gridOptions.extraFilterDto ?? []}
@ -1601,7 +1620,6 @@ const Tree = (props: TreeProps) => {
</Template>
<Toolbar
visible={(toolbarData?.length ?? 0) > 0 || (filterToolbarData?.length ?? 0) > 0}
multiline
>
{toolbarData?.map((item) => (
<Item key={item.name} {...item}></Item>

View file

@ -0,0 +1,198 @@
import { ReactElement } from 'react'
const parseJsonObject = (value: unknown) => {
if (!value) return undefined
if (typeof value === 'object') return value as Record<string, any>
if (typeof value !== 'string') return undefined
try {
const parsed = JSON.parse(value)
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as Record<string, any>)
: 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<string, unknown>
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<string[]>(
(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 <div className="text-gray-400 px-1 py-2">-</div>
}
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
{urls.map((url, index) => (
<button
key={`${url}-${index}`}
type="button"
onClick={() => openImage(url)}
style={{ border: 0, background: 'transparent', padding: 0, cursor: 'zoom-in' }}
>
<img
src={url}
alt=""
style={{
width: thumbW,
height: thumbH,
objectFit: 'cover',
borderRadius: 4,
border: '1px solid #ddd',
display: 'block',
}}
/>
</button>
))}
</div>
)
}
export { ImageViewerEditorComponent }

View file

@ -137,16 +137,10 @@ const cellTemplateImage = (
cellInfo: DataGridTypes.ColumnCellTemplateData<any, any>,
) => {
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<string, any>
if (typeof value !== 'string') return undefined
try {
const parsed = JSON.parse(value)
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as Record<string, any>)
: 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<string, unknown>
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
}
}