Grid üzerine ImageUploadAndViewer sütun tipi eklendi
This commit is contained in:
parent
10404ab63a
commit
3219265c12
16 changed files with 496 additions and 19 deletions
|
|
@ -2682,6 +2682,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
new EditingFormItemDto { Order = 6, DataField = "PublishDate", ColSpan=1, EditorType2 = EditorTypes.dxDateBox },
|
||||
new EditingFormItemDto { Order = 7, DataField = "ExpiryDate", ColSpan=1, IsRequired = true, EditorType2 = EditorTypes.dxDateBox },
|
||||
new EditingFormItemDto { Order = 8, DataField = "IsPinned", ColSpan=1, EditorType2 = EditorTypes.dxCheckBox },
|
||||
new EditingFormItemDto { Order = 9, DataField = "ImageUrl", ColSpan=1, EditorType2 = EditorTypes.dxImageUpload, EditorOptions = EditorOptionValues.ImageUploadOptions},
|
||||
]}
|
||||
}),
|
||||
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[]
|
||||
|
|
@ -2766,7 +2767,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
CaptionName = "App.Listform.ListformField.ImageUrl",
|
||||
Width = 200,
|
||||
ListOrderNo = 5,
|
||||
Visible = false,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
AllowSearch = true,
|
||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ public static class PlatformConsts
|
|||
public static string DateFormat = "{ \"format\": \"dd/MM/yyyy\", \"displayFormat\" : \"dd/MM/yyyy\" }";
|
||||
public static string DateTimeFormat = "{ \"format\": \"dd/MM/yyyy HH:mm\", \"displayFormat\" : \"dd/MM/yyyy HH:mm\" }";
|
||||
public static string SliderOptions = "{\"tooltip\": { \"enabled\": true }}";
|
||||
public static string ImageUploadOptions = "{\"width\": 80, \"height\": 80, \"multiple\": true}";
|
||||
}
|
||||
|
||||
public static class EditorScriptValues
|
||||
|
|
@ -738,6 +739,7 @@ public static class PlatformConsts
|
|||
public const string dxTagBox = "dxTagBox";
|
||||
public const string dxTextArea = "dxTextArea";
|
||||
public const string dxTextBox = "dxTextBox";
|
||||
public const string dxImageUpload = "dxImageUpload";
|
||||
}
|
||||
|
||||
public static class CustomEndpointConsts
|
||||
|
|
|
|||
|
|
@ -1052,7 +1052,6 @@ public class PlatformDbContext :
|
|||
b.Property(x => x.Title).IsRequired().HasMaxLength(256);
|
||||
b.Property(x => x.Excerpt).IsRequired().HasMaxLength(512);
|
||||
b.Property(x => x.Content).IsRequired().HasMaxLength(4096);
|
||||
b.Property(x => x.ImageUrl).HasMaxLength(512);
|
||||
b.Property(x => x.Category).IsRequired().HasMaxLength(64);
|
||||
b.Property(x => x.PublishDate).IsRequired();
|
||||
b.Property(x => x.Attachments).HasMaxLength(2048);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260505120031_Initial")]
|
||||
[Migration("20260506093149_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -603,8 +603,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
|
|
@ -471,7 +471,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
Title = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Excerpt = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
||||
Content = table.Column<string>(type: "nvarchar(max)", maxLength: 4096, nullable: false),
|
||||
ImageUrl = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
ImageUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Category = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
PublishDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
|
|
@ -600,8 +600,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
|
|
|
|||
|
|
@ -325,6 +325,15 @@ export interface TagBoxOptionsDto {
|
|||
acceptCustomValue: boolean
|
||||
}
|
||||
|
||||
export interface ImageUploadOptionsDto {
|
||||
uploadUrl?: string
|
||||
accept?: string
|
||||
multiple?: boolean
|
||||
maxFileSize?: number
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export interface RowDto {
|
||||
rowHeight: string
|
||||
whiteSpace: string
|
||||
|
|
@ -480,6 +489,7 @@ export interface EditingFormItemDto {
|
|||
isRequired?: boolean
|
||||
gridBoxOptions?: GridBoxOptionsDto
|
||||
tagBoxOptions?: TagBoxOptionsDto
|
||||
imageUploadOptions?: ImageUploadOptionsDto
|
||||
editorScript?: string
|
||||
}
|
||||
|
||||
|
|
@ -822,6 +832,7 @@ export enum SubFormTabTypeEnum {
|
|||
export enum PlatformEditorTypes {
|
||||
dxTagBox = 'dxTagBox',
|
||||
dxGridBox = 'dxGridBox',
|
||||
dxImageUpload = 'dxImageUpload',
|
||||
}
|
||||
|
||||
export enum GanttScaleTypeEnum {
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ export const columnEditorTypeListOptions = [
|
|||
{ value: 'dxDateRangeBox', label: 'dxDateRangeBox' },
|
||||
{ value: 'dxDropDownBox', label: 'dxDropDownBox' },
|
||||
{ value: PlatformEditorTypes.dxGridBox, label: PlatformEditorTypes.dxGridBox },
|
||||
{ value: PlatformEditorTypes.dxImageUpload, label: PlatformEditorTypes.dxImageUpload },
|
||||
{ value: 'dxHtmlEditor', label: 'dxHtmlEditor' },
|
||||
{ value: 'dxLookup', label: 'dxLookup' },
|
||||
{ value: 'dxNumberBox', label: 'dxNumberBox' },
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
import { FieldDataChangedEvent, GroupItem } from 'devextreme/ui/form'
|
||||
import { Dispatch, RefObject, useEffect, useRef } from 'react'
|
||||
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
||||
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
||||
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
||||
import { RowMode, SimpleItemWithColData } from './types'
|
||||
import { PlatformEditorTypes } from '@/proxy/form/models'
|
||||
|
|
@ -211,6 +212,32 @@ const FormDevExpress = (props: {
|
|||
className: 'font-semibold',
|
||||
}}
|
||||
></SimpleItemDx>
|
||||
) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? (
|
||||
<SimpleItemDx
|
||||
cssClass="font-semibold"
|
||||
key={'formItem-' + i}
|
||||
dataField={formItem.dataField}
|
||||
name={formItem.name}
|
||||
colSpan={formItem.colSpan}
|
||||
isRequired={formItem.isRequired}
|
||||
render={() => (
|
||||
<ImageUploadEditorComponent
|
||||
value={formData[formItem.dataField!]}
|
||||
options={formItem.imageUploadOptions}
|
||||
onValueChanged={(val: any) => {
|
||||
setFormData({ ...formData, [formItem.dataField!]: val })
|
||||
}}
|
||||
editorOptions={{
|
||||
...formItem.editorOptions,
|
||||
...(mode === 'view' ? { readOnly: true } : {}),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
label={{
|
||||
text: translate('::' + formItem.colData?.captionName),
|
||||
className: 'font-semibold',
|
||||
}}
|
||||
></SimpleItemDx>
|
||||
) : (
|
||||
<SimpleItemDx
|
||||
cssClass="font-semibold"
|
||||
|
|
|
|||
177
ui/src/views/form/editors/ImageUploadEditorComponent.tsx
Normal file
177
ui/src/views/form/editors/ImageUploadEditorComponent.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { ImageUploadOptionsDto } from '@/proxy/form/models'
|
||||
import { ReactElement, useRef, useState } from 'react'
|
||||
import { FaSpinner, FaUpload } from 'react-icons/fa'
|
||||
|
||||
const ImageUploadEditorComponent = ({
|
||||
value,
|
||||
options,
|
||||
onValueChanged,
|
||||
editorOptions,
|
||||
}: {
|
||||
value: any
|
||||
options?: ImageUploadOptionsDto
|
||||
onValueChanged: (val: any) => void
|
||||
editorOptions?: any
|
||||
}): ReactElement => {
|
||||
const readOnly: boolean = editorOptions?.readOnly ?? false
|
||||
|
||||
// options önce prop olarak gelen ImageUploadOptionsDto'dan, sonra editorOptions JSON'undan okunur
|
||||
const resolvedOptions: ImageUploadOptionsDto = options ?? editorOptions ?? {}
|
||||
|
||||
// JSON'dan string olarak gelebilir: "true" / "false"
|
||||
const isMultiple: boolean =
|
||||
resolvedOptions.multiple === true || (resolvedOptions.multiple as any) === 'true'
|
||||
|
||||
const thumbW: number = resolvedOptions.width ?? 40
|
||||
const thumbH: number = resolvedOptions.height ?? 40
|
||||
|
||||
const initialUrls: string[] = value
|
||||
? Array.isArray(value)
|
||||
? (value as string[]).filter(Boolean)
|
||||
: [value as string].filter(Boolean)
|
||||
: []
|
||||
|
||||
const [urls, setUrls] = useState<string[]>(initialUrls)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const removeImage = (index: number) => {
|
||||
const newUrls = urls.filter((_, i) => i !== index)
|
||||
setUrls(newUrls)
|
||||
const newValue = isMultiple
|
||||
? newUrls.length > 0
|
||||
? newUrls
|
||||
: null
|
||||
: (newUrls[0] ?? null)
|
||||
onValueChanged(newValue)
|
||||
}
|
||||
|
||||
const toBase64 = (file: File): Promise<string> =>
|
||||
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 (resolvedOptions.uploadUrl) {
|
||||
const formData = new FormData()
|
||||
const fileFieldName: string = (resolvedOptions as any).fileFieldName ?? 'file'
|
||||
formData.append(fileFieldName, file)
|
||||
const res = await fetch(resolvedOptions.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 yoksa 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)
|
||||
onValueChanged(newValue)
|
||||
} catch (e) {
|
||||
console.error('ImageUploadEditorComponent upload error:', e)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
if (inputRef.current) inputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
|
||||
{urls.map((url, i) => (
|
||||
<div key={i} style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<img
|
||||
src={url}
|
||||
alt=""
|
||||
style={{
|
||||
width: thumbW,
|
||||
height: thumbH,
|
||||
objectFit: 'cover',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<button
|
||||
type="button"
|
||||
title="Sil"
|
||||
onClick={() => removeImage(i)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
right: -6,
|
||||
background: '#dc3545',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
width: 18,
|
||||
height: 18,
|
||||
cursor: 'pointer',
|
||||
fontSize: 11,
|
||||
lineHeight: '18px',
|
||||
padding: 0,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!readOnly && (
|
||||
<>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept={resolvedOptions.accept ?? 'image/*'}
|
||||
multiple={isMultiple}
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => handleFiles(e.target.files)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
title="Görsel Yükle"
|
||||
disabled={uploading}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
background: uploading ? '#aaa' : '#0078d4',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
cursor: uploading ? 'not-allowed' : 'pointer',
|
||||
fontSize: 13,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{uploading ? (
|
||||
<FaSpinner style={{ fontSize: 14 }} className="spin" />
|
||||
) : (
|
||||
<FaUpload style={{ fontSize: 14 }} />
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ImageUploadEditorComponent }
|
||||
|
|
@ -3,18 +3,20 @@ import { Overwrite } from '../../utils/types'
|
|||
import {
|
||||
ColumnFormatDto,
|
||||
GridBoxOptionsDto,
|
||||
ImageUploadOptionsDto,
|
||||
PlatformEditorTypes,
|
||||
TagBoxOptionsDto,
|
||||
} from '../../proxy/form/models'
|
||||
import { Meta } from '@/proxy/routes/routes'
|
||||
|
||||
export type EditorType2 = FormItemComponent | PlatformEditorTypes.dxGridBox
|
||||
export type EditorType2 = FormItemComponent | PlatformEditorTypes.dxGridBox | PlatformEditorTypes.dxImageUpload
|
||||
export type SimpleItemWithColData = Overwrite<
|
||||
SimpleItem,
|
||||
{
|
||||
colData?: ColumnFormatDto
|
||||
tagBoxOptions?: TagBoxOptionsDto
|
||||
gridBoxOptions?: GridBoxOptionsDto
|
||||
imageUploadOptions?: ImageUploadOptionsDto
|
||||
canRead: boolean
|
||||
canCreate: boolean
|
||||
canUpdate: boolean
|
||||
|
|
|
|||
|
|
@ -110,14 +110,22 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({
|
|||
|
||||
{/* Content */}
|
||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
||||
{/* Image if exists */}
|
||||
{announcement.imageUrl && (
|
||||
<img
|
||||
src={announcement.imageUrl}
|
||||
alt={announcement.title}
|
||||
className="w-full rounded-lg mb-6"
|
||||
/>
|
||||
)}
|
||||
{/* Images if exist */}
|
||||
{announcement.imageUrl && (() => {
|
||||
const images = announcement.imageUrl.split('|').filter(Boolean)
|
||||
return images.length > 0 ? (
|
||||
<div className={`mb-6 ${images.length > 1 ? 'grid grid-cols-2 gap-2' : ''}`}>
|
||||
{images.map((img, idx) => (
|
||||
<img
|
||||
key={idx}
|
||||
src={img.startsWith('data:') ? img : `data:image/jpeg;base64,${img}`}
|
||||
alt={`${announcement.title} ${images.length > 1 ? idx + 1 : ''}`.trim()}
|
||||
className="w-full rounded-lg object-cover"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null
|
||||
})()}
|
||||
|
||||
{/* Full Content */}
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
></Editing>
|
||||
<Template name={'cellEditTagBox'} render={TagBoxEditorComponent} />
|
||||
<Template name={'cellEditGridBox'} render={GridBoxEditorComponent} />
|
||||
<Template name={'cellEditImageUpload'} render={ImageUploadEditorComponent} />
|
||||
<Template name="extraFilters">
|
||||
<GridExtraFilterToolbar
|
||||
filters={gridDto?.gridOptions.extraFilterDto ?? []}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { DataGridTypes } from 'devextreme-react/data-grid'
|
||||
import { ColumnFormatDto, GridBoxOptionsDto, TagBoxOptionsDto } from '../../proxy/form/models'
|
||||
import {
|
||||
ColumnFormatDto,
|
||||
GridBoxOptionsDto,
|
||||
ImageUploadOptionsDto,
|
||||
TagBoxOptionsDto,
|
||||
} from '../../proxy/form/models'
|
||||
|
||||
interface IGridColumnData extends DataGridTypes.Column {
|
||||
colData?: ColumnFormatDto
|
||||
|
|
@ -8,6 +13,7 @@ interface IGridColumnData extends DataGridTypes.Column {
|
|||
editorOptions?: string
|
||||
tagBoxOptions?: TagBoxOptionsDto
|
||||
gridBoxOptions?: GridBoxOptionsDto
|
||||
imageUploadOptions?: ImageUploadOptionsDto
|
||||
}
|
||||
caption?: string
|
||||
isBand?: boolean
|
||||
|
|
|
|||
174
ui/src/views/list/editors/ImageUploadEditorComponent.tsx
Normal file
174
ui/src/views/list/editors/ImageUploadEditorComponent.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { ReactElement, useRef, useState } from 'react'
|
||||
import { FaSpinner, FaUpload } from 'react-icons/fa'
|
||||
|
||||
const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
||||
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<HTMLInputElement>(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<string[]>(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<string> =>
|
||||
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 (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
|
||||
{urls.map((url, i) => (
|
||||
<div key={i} style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<img
|
||||
src={url}
|
||||
alt=""
|
||||
style={{
|
||||
width: thumbW,
|
||||
height: thumbH,
|
||||
objectFit: 'cover',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
title="Sil"
|
||||
onClick={() => removeImage(i)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
right: -6,
|
||||
background: '#dc3545',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
width: 18,
|
||||
height: 18,
|
||||
cursor: 'pointer',
|
||||
fontSize: 11,
|
||||
lineHeight: '18px',
|
||||
padding: 0,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept={options.accept ?? 'image/*'}
|
||||
multiple={isMultiple}
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => handleFiles(e.target.files)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
title="Görsel Yükle"
|
||||
disabled={uploading}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
background: uploading ? '#aaa' : '#0078d4',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
cursor: uploading ? 'not-allowed' : 'pointer',
|
||||
fontSize: 13,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{uploading ? (
|
||||
<FaSpinner style={{ fontSize: 14 }} className="spin" />
|
||||
) : (
|
||||
<FaUpload style={{ fontSize: 14 }} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ImageUploadEditorComponent }
|
||||
|
|
@ -65,6 +65,33 @@ const cellTemplateMultiValue = (
|
|||
}
|
||||
}
|
||||
|
||||
const cellTemplateImage = (
|
||||
cellElement: HTMLElement,
|
||||
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 imgs = urls
|
||||
.map(
|
||||
(url) =>
|
||||
`<img src="${url}" alt="" style="width:${w}px;height:${h}px;object-fit:cover;border-radius:4px;border:1px solid #ddd;margin:2px;vertical-align:middle;display:inline-block;" />`,
|
||||
)
|
||||
.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[]
|
||||
|
|
|
|||
Loading…
Reference in a new issue