diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/EditingFormDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/EditingFormDto.cs index 9a53ace..9e62922 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/EditingFormDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/GridOptionsDto/EditingFormDto.cs @@ -30,7 +30,7 @@ public class EditingFormDto public class EditingFormItemDto { /// - /// 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'; /// [JsonPropertyName("order")] public int Order { get; set; } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index 7c4e1fb..82c864a 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -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[] diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index 1cdb54f..1b6fdd9 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -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"; } diff --git a/ui/src/proxy/form/models.ts b/ui/src/proxy/form/models.ts index 01cd1da..709806b 100644 --- a/ui/src/proxy/form/models.ts +++ b/ui/src/proxy/form/models.ts @@ -837,6 +837,7 @@ export enum SubFormTabTypeEnum { export enum PlatformEditorTypes { dxTagBox = 'dxTagBox', dxGridBox = 'dxGridBox', + dxImageViewer = 'dxImageViewer', dxImageUpload = 'dxImageUpload', } diff --git a/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx b/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx index d73138f..c797d28 100644 --- a/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx +++ b/ui/src/views/admin/listForm/edit/json-row-operations/JsonRowOpDialogEditForm.tsx @@ -373,7 +373,7 @@ function JsonRowOpDialogEditForm({ placeholder="Order" /> -
+
-
+
+ ) : formItem.editorType2 === PlatformEditorTypes.dxImageViewer ? ( + ( + + )} + label={{ + text: translate('::' + formItem.colData?.captionName), + className: 'font-semibold', + }} + > ) : ( { + if (!value) return undefined + if (typeof value === 'object') return value as Record + if (typeof value !== 'string') return undefined + + try { + const parsed = JSON.parse(value) + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? (parsed as Record) + : 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 + 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
-
+ } + + return ( +
+ {urls.map((url, index) => ( + + ))} +
+ ) +} + +export { ImageViewerEditorComponent } diff --git a/ui/src/views/form/types.ts b/ui/src/views/form/types.ts index 16c8175..6638f07 100644 --- a/ui/src/views/form/types.ts +++ b/ui/src/views/form/types.ts @@ -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, { diff --git a/ui/src/views/form/useFormData.tsx b/ui/src/views/form/useFormData.tsx index da15a13..ae813ec 100644 --- a/ui/src/views/form/useFormData.tsx +++ b/ui/src/views/form/useFormData.tsx @@ -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) { diff --git a/ui/src/views/list/GanttView.tsx b/ui/src/views/list/GanttView.tsx index 1d58cd7..fc8f9cc 100644 --- a/ui/src/views/list/GanttView.tsx +++ b/ui/src/views/list/GanttView.tsx @@ -244,7 +244,7 @@ const GanttView = (props: GanttViewProps) => { progressExpr={gridDto.gridOptions.ganttOptionDto?.progressExpr} /> - + diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx index 1ef628d..881dee9 100644 --- a/ui/src/views/list/Grid.tsx +++ b/ui/src/views/list/Grid.tsx @@ -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) => {