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 = 6, DataField = "PublishDate", ColSpan=1, EditorType2 = EditorTypes.dxDateBox },
|
||||||
new EditingFormItemDto { Order = 7, DataField = "ExpiryDate", ColSpan=1, IsRequired = true, 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 = 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[]
|
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[]
|
||||||
|
|
@ -2766,7 +2767,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
||||||
CaptionName = "App.Listform.ListformField.ImageUrl",
|
CaptionName = "App.Listform.ListformField.ImageUrl",
|
||||||
Width = 200,
|
Width = 200,
|
||||||
ListOrderNo = 5,
|
ListOrderNo = 5,
|
||||||
Visible = false,
|
Visible = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
AllowSearch = true,
|
AllowSearch = true,
|
||||||
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
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 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 DateTimeFormat = "{ \"format\": \"dd/MM/yyyy HH:mm\", \"displayFormat\" : \"dd/MM/yyyy HH:mm\" }";
|
||||||
public static string SliderOptions = "{\"tooltip\": { \"enabled\": true }}";
|
public static string SliderOptions = "{\"tooltip\": { \"enabled\": true }}";
|
||||||
|
public static string ImageUploadOptions = "{\"width\": 80, \"height\": 80, \"multiple\": true}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class EditorScriptValues
|
public static class EditorScriptValues
|
||||||
|
|
@ -738,6 +739,7 @@ public static class PlatformConsts
|
||||||
public const string dxTagBox = "dxTagBox";
|
public const string dxTagBox = "dxTagBox";
|
||||||
public const string dxTextArea = "dxTextArea";
|
public const string dxTextArea = "dxTextArea";
|
||||||
public const string dxTextBox = "dxTextBox";
|
public const string dxTextBox = "dxTextBox";
|
||||||
|
public const string dxImageUpload = "dxImageUpload";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class CustomEndpointConsts
|
public static class CustomEndpointConsts
|
||||||
|
|
|
||||||
|
|
@ -1052,7 +1052,6 @@ public class PlatformDbContext :
|
||||||
b.Property(x => x.Title).IsRequired().HasMaxLength(256);
|
b.Property(x => x.Title).IsRequired().HasMaxLength(256);
|
||||||
b.Property(x => x.Excerpt).IsRequired().HasMaxLength(512);
|
b.Property(x => x.Excerpt).IsRequired().HasMaxLength(512);
|
||||||
b.Property(x => x.Content).IsRequired().HasMaxLength(4096);
|
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.Category).IsRequired().HasMaxLength(64);
|
||||||
b.Property(x => x.PublishDate).IsRequired();
|
b.Property(x => x.PublishDate).IsRequired();
|
||||||
b.Property(x => x.Attachments).HasMaxLength(2048);
|
b.Property(x => x.Attachments).HasMaxLength(2048);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
||||||
namespace Sozsoft.Platform.Migrations
|
namespace Sozsoft.Platform.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PlatformDbContext))]
|
[DbContext(typeof(PlatformDbContext))]
|
||||||
[Migration("20260505120031_Initial")]
|
[Migration("20260506093149_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -603,8 +603,7 @@ namespace Sozsoft.Platform.Migrations
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<string>("ImageUrl")
|
b.Property<string>("ImageUrl")
|
||||||
.HasMaxLength(512)
|
.HasColumnType("nvarchar(max)");
|
||||||
.HasColumnType("nvarchar(512)");
|
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
b.Property<bool>("IsDeleted")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
@ -471,7 +471,7 @@ namespace Sozsoft.Platform.Migrations
|
||||||
Title = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
Title = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||||
Excerpt = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
Excerpt = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
||||||
Content = table.Column<string>(type: "nvarchar(max)", maxLength: 4096, 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),
|
Category = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
PublishDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
PublishDate = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
|
@ -600,8 +600,7 @@ namespace Sozsoft.Platform.Migrations
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<string>("ImageUrl")
|
b.Property<string>("ImageUrl")
|
||||||
.HasMaxLength(512)
|
.HasColumnType("nvarchar(max)");
|
||||||
.HasColumnType("nvarchar(512)");
|
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
b.Property<bool>("IsDeleted")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,15 @@ export interface TagBoxOptionsDto {
|
||||||
acceptCustomValue: boolean
|
acceptCustomValue: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageUploadOptionsDto {
|
||||||
|
uploadUrl?: string
|
||||||
|
accept?: string
|
||||||
|
multiple?: boolean
|
||||||
|
maxFileSize?: number
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface RowDto {
|
export interface RowDto {
|
||||||
rowHeight: string
|
rowHeight: string
|
||||||
whiteSpace: string
|
whiteSpace: string
|
||||||
|
|
@ -480,6 +489,7 @@ export interface EditingFormItemDto {
|
||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
gridBoxOptions?: GridBoxOptionsDto
|
gridBoxOptions?: GridBoxOptionsDto
|
||||||
tagBoxOptions?: TagBoxOptionsDto
|
tagBoxOptions?: TagBoxOptionsDto
|
||||||
|
imageUploadOptions?: ImageUploadOptionsDto
|
||||||
editorScript?: string
|
editorScript?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -822,6 +832,7 @@ export enum SubFormTabTypeEnum {
|
||||||
export enum PlatformEditorTypes {
|
export enum PlatformEditorTypes {
|
||||||
dxTagBox = 'dxTagBox',
|
dxTagBox = 'dxTagBox',
|
||||||
dxGridBox = 'dxGridBox',
|
dxGridBox = 'dxGridBox',
|
||||||
|
dxImageUpload = 'dxImageUpload',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GanttScaleTypeEnum {
|
export enum GanttScaleTypeEnum {
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ export const columnEditorTypeListOptions = [
|
||||||
{ value: 'dxDateRangeBox', label: 'dxDateRangeBox' },
|
{ value: 'dxDateRangeBox', label: 'dxDateRangeBox' },
|
||||||
{ value: 'dxDropDownBox', label: 'dxDropDownBox' },
|
{ value: 'dxDropDownBox', label: 'dxDropDownBox' },
|
||||||
{ value: PlatformEditorTypes.dxGridBox, label: PlatformEditorTypes.dxGridBox },
|
{ value: PlatformEditorTypes.dxGridBox, label: PlatformEditorTypes.dxGridBox },
|
||||||
|
{ value: PlatformEditorTypes.dxImageUpload, label: PlatformEditorTypes.dxImageUpload },
|
||||||
{ value: 'dxHtmlEditor', label: 'dxHtmlEditor' },
|
{ value: 'dxHtmlEditor', label: 'dxHtmlEditor' },
|
||||||
{ value: 'dxLookup', label: 'dxLookup' },
|
{ value: 'dxLookup', label: 'dxLookup' },
|
||||||
{ value: 'dxNumberBox', label: 'dxNumberBox' },
|
{ value: 'dxNumberBox', label: 'dxNumberBox' },
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import { FieldDataChangedEvent, GroupItem } from 'devextreme/ui/form'
|
import { FieldDataChangedEvent, GroupItem } from 'devextreme/ui/form'
|
||||||
import { Dispatch, RefObject, useEffect, useRef } from 'react'
|
import { Dispatch, RefObject, useEffect, useRef } from 'react'
|
||||||
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
||||||
|
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
||||||
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
||||||
import { RowMode, SimpleItemWithColData } from './types'
|
import { RowMode, SimpleItemWithColData } from './types'
|
||||||
import { PlatformEditorTypes } from '@/proxy/form/models'
|
import { PlatformEditorTypes } from '@/proxy/form/models'
|
||||||
|
|
@ -211,6 +212,32 @@ const FormDevExpress = (props: {
|
||||||
className: 'font-semibold',
|
className: 'font-semibold',
|
||||||
}}
|
}}
|
||||||
></SimpleItemDx>
|
></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
|
<SimpleItemDx
|
||||||
cssClass="font-semibold"
|
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 {
|
import {
|
||||||
ColumnFormatDto,
|
ColumnFormatDto,
|
||||||
GridBoxOptionsDto,
|
GridBoxOptionsDto,
|
||||||
|
ImageUploadOptionsDto,
|
||||||
PlatformEditorTypes,
|
PlatformEditorTypes,
|
||||||
TagBoxOptionsDto,
|
TagBoxOptionsDto,
|
||||||
} from '../../proxy/form/models'
|
} from '../../proxy/form/models'
|
||||||
import { Meta } from '@/proxy/routes/routes'
|
import { Meta } from '@/proxy/routes/routes'
|
||||||
|
|
||||||
export type EditorType2 = FormItemComponent | PlatformEditorTypes.dxGridBox
|
export type EditorType2 = FormItemComponent | PlatformEditorTypes.dxGridBox | PlatformEditorTypes.dxImageUpload
|
||||||
export type SimpleItemWithColData = Overwrite<
|
export type SimpleItemWithColData = Overwrite<
|
||||||
SimpleItem,
|
SimpleItem,
|
||||||
{
|
{
|
||||||
colData?: ColumnFormatDto
|
colData?: ColumnFormatDto
|
||||||
tagBoxOptions?: TagBoxOptionsDto
|
tagBoxOptions?: TagBoxOptionsDto
|
||||||
gridBoxOptions?: GridBoxOptionsDto
|
gridBoxOptions?: GridBoxOptionsDto
|
||||||
|
imageUploadOptions?: ImageUploadOptionsDto
|
||||||
canRead: boolean
|
canRead: boolean
|
||||||
canCreate: boolean
|
canCreate: boolean
|
||||||
canUpdate: boolean
|
canUpdate: boolean
|
||||||
|
|
|
||||||
|
|
@ -110,14 +110,22 @@ const AnnouncementModal: React.FC<AnnouncementModalProps> = ({
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
||||||
{/* Image if exists */}
|
{/* Images if exist */}
|
||||||
{announcement.imageUrl && (
|
{announcement.imageUrl && (() => {
|
||||||
<img
|
const images = announcement.imageUrl.split('|').filter(Boolean)
|
||||||
src={announcement.imageUrl}
|
return images.length > 0 ? (
|
||||||
alt={announcement.title}
|
<div className={`mb-6 ${images.length > 1 ? 'grid grid-cols-2 gap-2' : ''}`}>
|
||||||
className="w-full rounded-lg mb-6"
|
{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 */}
|
{/* Full Content */}
|
||||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ import {
|
||||||
setGridPanelColor,
|
setGridPanelColor,
|
||||||
} from './Utils'
|
} from './Utils'
|
||||||
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
||||||
|
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
||||||
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
||||||
import { useFilters } from './useFilters'
|
import { useFilters } from './useFilters'
|
||||||
import { useToolbar } from './useToolbar'
|
import { useToolbar } from './useToolbar'
|
||||||
|
|
@ -955,7 +956,11 @@ const Grid = (props: GridProps) => {
|
||||||
name: i.dataField,
|
name: i.dataField,
|
||||||
editorType2: i.editorType2,
|
editorType2: i.editorType2,
|
||||||
editorType:
|
editorType:
|
||||||
i.editorType2 == PlatformEditorTypes.dxGridBox ? 'dxDropDownBox' : i.editorType2,
|
i.editorType2 == PlatformEditorTypes.dxGridBox
|
||||||
|
? 'dxDropDownBox'
|
||||||
|
: i.editorType2 == PlatformEditorTypes.dxImageUpload
|
||||||
|
? undefined
|
||||||
|
: i.editorType2,
|
||||||
colSpan: i.colSpan,
|
colSpan: i.colSpan,
|
||||||
isRequired: i.isRequired,
|
isRequired: i.isRequired,
|
||||||
editorOptions,
|
editorOptions,
|
||||||
|
|
@ -1357,6 +1362,7 @@ const Grid = (props: GridProps) => {
|
||||||
></Editing>
|
></Editing>
|
||||||
<Template name={'cellEditTagBox'} render={TagBoxEditorComponent} />
|
<Template name={'cellEditTagBox'} render={TagBoxEditorComponent} />
|
||||||
<Template name={'cellEditGridBox'} render={GridBoxEditorComponent} />
|
<Template name={'cellEditGridBox'} render={GridBoxEditorComponent} />
|
||||||
|
<Template name={'cellEditImageUpload'} render={ImageUploadEditorComponent} />
|
||||||
<Template name="extraFilters">
|
<Template name="extraFilters">
|
||||||
<GridExtraFilterToolbar
|
<GridExtraFilterToolbar
|
||||||
filters={gridDto?.gridOptions.extraFilterDto ?? []}
|
filters={gridDto?.gridOptions.extraFilterDto ?? []}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { DataGridTypes } from 'devextreme-react/data-grid'
|
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 {
|
interface IGridColumnData extends DataGridTypes.Column {
|
||||||
colData?: ColumnFormatDto
|
colData?: ColumnFormatDto
|
||||||
|
|
@ -8,6 +13,7 @@ interface IGridColumnData extends DataGridTypes.Column {
|
||||||
editorOptions?: string
|
editorOptions?: string
|
||||||
tagBoxOptions?: TagBoxOptionsDto
|
tagBoxOptions?: TagBoxOptionsDto
|
||||||
gridBoxOptions?: GridBoxOptionsDto
|
gridBoxOptions?: GridBoxOptionsDto
|
||||||
|
imageUploadOptions?: ImageUploadOptionsDto
|
||||||
}
|
}
|
||||||
caption?: string
|
caption?: string
|
||||||
isBand?: boolean
|
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(
|
function calculateFilterExpressionMultiValue(
|
||||||
this: DataGridTypes.Column,
|
this: DataGridTypes.Column,
|
||||||
filterValue: any,
|
filterValue: any,
|
||||||
|
|
@ -641,6 +668,44 @@ const useListFormColumns = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// #endregion
|
// #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) {
|
if (colData.validationRuleDto) {
|
||||||
// for server side validation : https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/DataValidation/jQuery/Light/
|
// for server side validation : https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/DataValidation/jQuery/Light/
|
||||||
column.validationRules = colData.validationRuleDto as ValidationRule[]
|
column.validationRules = colData.validationRuleDto as ValidationRule[]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue