From 3219265c12b45c8c4c6b1bf8b6e9afca405c9541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 6 May 2026 13:55:34 +0300 Subject: [PATCH] =?UTF-8?q?Grid=20=C3=BCzerine=20ImageUploadAndViewer=20s?= =?UTF-8?q?=C3=BCtun=20tipi=20eklendi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Seeds/ListFormSeeder_Administration.cs | 3 +- .../PlatformConsts.cs | 2 + .../EntityFrameworkCore/PlatformDbContext.cs | 1 - ....cs => 20260506093149_Initial.Designer.cs} | 5 +- ...1_Initial.cs => 20260506093149_Initial.cs} | 2 +- .../PlatformDbContextModelSnapshot.cs | 3 +- ui/src/proxy/form/models.ts | 11 ++ ui/src/views/admin/listForm/edit/options.ts | 1 + ui/src/views/form/FormDevExpress.tsx | 27 +++ .../editors/ImageUploadEditorComponent.tsx | 177 ++++++++++++++++++ ui/src/views/form/types.ts | 4 +- .../intranet/widgets/AnnouncementModal.tsx | 24 ++- ui/src/views/list/Grid.tsx | 8 +- ui/src/views/list/GridColumnData.ts | 8 +- .../editors/ImageUploadEditorComponent.tsx | 174 +++++++++++++++++ ui/src/views/list/useListFormColumns.ts | 65 +++++++ 16 files changed, 496 insertions(+), 19 deletions(-) rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260505120031_Initial.Designer.cs => 20260506093149_Initial.Designer.cs} (99%) rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260505120031_Initial.cs => 20260506093149_Initial.cs} (99%) create mode 100644 ui/src/views/form/editors/ImageUploadEditorComponent.tsx create mode 100644 ui/src/views/list/editors/ImageUploadEditorComponent.tsx diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index 332fc10..4195c4d 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -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, diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index 760a598..be5a096 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -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 diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index 3393e85..e5aca43 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -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); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260505120031_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.Designer.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260505120031_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.Designer.cs index c9f5311..18479f2 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260505120031_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260505120031_Initial")] + [Migration("20260506093149_Initial")] partial class Initial { /// @@ -603,8 +603,7 @@ namespace Sozsoft.Platform.Migrations .HasColumnType("datetime2"); b.Property("ImageUrl") - .HasMaxLength(512) - .HasColumnType("nvarchar(512)"); + .HasColumnType("nvarchar(max)"); b.Property("IsDeleted") .ValueGeneratedOnAdd() diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260505120031_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260505120031_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.cs index d0866c1..19009c9 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260505120031_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260506093149_Initial.cs @@ -471,7 +471,7 @@ namespace Sozsoft.Platform.Migrations Title = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), Excerpt = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: false), Content = table.Column(type: "nvarchar(max)", maxLength: 4096, nullable: false), - ImageUrl = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: true), + ImageUrl = table.Column(type: "nvarchar(max)", nullable: true), Category = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), UserId = table.Column(type: "uniqueidentifier", nullable: true), PublishDate = table.Column(type: "datetime2", nullable: false), diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index 82b0bb4..445beb6 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -600,8 +600,7 @@ namespace Sozsoft.Platform.Migrations .HasColumnType("datetime2"); b.Property("ImageUrl") - .HasMaxLength(512) - .HasColumnType("nvarchar(512)"); + .HasColumnType("nvarchar(max)"); b.Property("IsDeleted") .ValueGeneratedOnAdd() diff --git a/ui/src/proxy/form/models.ts b/ui/src/proxy/form/models.ts index 5e4bce0..eb744f2 100644 --- a/ui/src/proxy/form/models.ts +++ b/ui/src/proxy/form/models.ts @@ -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 { diff --git a/ui/src/views/admin/listForm/edit/options.ts b/ui/src/views/admin/listForm/edit/options.ts index c2b2bc6..19122f5 100644 --- a/ui/src/views/admin/listForm/edit/options.ts +++ b/ui/src/views/admin/listForm/edit/options.ts @@ -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' }, diff --git a/ui/src/views/form/FormDevExpress.tsx b/ui/src/views/form/FormDevExpress.tsx index b114f73..76491af 100644 --- a/ui/src/views/form/FormDevExpress.tsx +++ b/ui/src/views/form/FormDevExpress.tsx @@ -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', }} > + ) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? ( + ( + { + setFormData({ ...formData, [formItem.dataField!]: val }) + }} + editorOptions={{ + ...formItem.editorOptions, + ...(mode === 'view' ? { readOnly: true } : {}), + }} + /> + )} + label={{ + text: translate('::' + formItem.colData?.captionName), + className: 'font-semibold', + }} + > ) : ( 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(initialUrls) + const [uploading, setUploading] = useState(false) + const inputRef = useRef(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 => + 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 ( +
+ {urls.map((url, i) => ( +
+ + {!readOnly && ( + + )} +
+ ))} + + {!readOnly && ( + <> + handleFiles(e.target.files)} + /> + + + )} +
+ ) +} + +export { ImageUploadEditorComponent } diff --git a/ui/src/views/form/types.ts b/ui/src/views/form/types.ts index 90df0ef..16c8175 100644 --- a/ui/src/views/form/types.ts +++ b/ui/src/views/form/types.ts @@ -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 diff --git a/ui/src/views/intranet/widgets/AnnouncementModal.tsx b/ui/src/views/intranet/widgets/AnnouncementModal.tsx index be7de6f..a48e0b9 100644 --- a/ui/src/views/intranet/widgets/AnnouncementModal.tsx +++ b/ui/src/views/intranet/widgets/AnnouncementModal.tsx @@ -110,14 +110,22 @@ const AnnouncementModal: React.FC = ({ {/* Content */}
- {/* Image if exists */} - {announcement.imageUrl && ( - {announcement.title} - )} + {/* Images if exist */} + {announcement.imageUrl && (() => { + const images = announcement.imageUrl.split('|').filter(Boolean) + return images.length > 0 ? ( +
1 ? 'grid grid-cols-2 gap-2' : ''}`}> + {images.map((img, idx) => ( + {`${announcement.title} 1 ? idx + 1 : ''}`.trim()} + className="w-full rounded-lg object-cover" + /> + ))} +
+ ) : null + })()} {/* Full Content */}
diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx index c5e5134..e667f36 100644 --- a/ui/src/views/list/Grid.tsx +++ b/ui/src/views/list/Grid.tsx @@ -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) => { >