Grid, Tree ve Gantt için ImageViewer sütun yapısı eklendi
This commit is contained in:
parent
92058ed5e9
commit
6098758f34
16 changed files with 564 additions and 31 deletions
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -837,6 +837,7 @@ export enum SubFormTabTypeEnum {
|
|||
export enum PlatformEditorTypes {
|
||||
dxTagBox = 'dxTagBox',
|
||||
dxGridBox = 'dxGridBox',
|
||||
dxImageViewer = 'dxImageViewer',
|
||||
dxImageUpload = 'dxImageUpload',
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
154
ui/src/views/form/editors/ImageViewerEditorComponent.tsx
Normal file
154
ui/src/views/form/editors/ImageViewerEditorComponent.tsx
Normal 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 }
|
||||
|
|
@ -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,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
198
ui/src/views/list/editors/ImageViewerEditorComponent.tsx
Normal file
198
ui/src/views/list/editors/ImageViewerEditorComponent.tsx
Normal 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 }
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue