Html Toolbar yetenekli hale getirdik

This commit is contained in:
Sedat ÖZTÜRK 2026-09-24 11:42:44 +03:00
parent c102114a3a
commit 9ceacf5d64
17 changed files with 185 additions and 249 deletions

View file

@ -921,7 +921,7 @@ anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir.
| `dxColorBox` | Renk | String (16) | `editAlphaChannel`, `keyStep` | | `dxColorBox` | Renk | String (16) | `editAlphaChannel`, `keyStep` |
| `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` | | `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` |
| `dxRangeSlider` | Aralık kaydırıcı | Sayı | `min`, `max`, `showRange` | | `dxRangeSlider` | Aralık kaydırıcı | Sayı | `min`, `max`, `showRange` |
| `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode` | | `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.tabs`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode`, `tableResizing.enabled`, `tableContextMenu.enabled`. Boş `editorOptions` da tam araç çubuğu, dosyadan/URL'den resim, resim boyutlandırma ve tablo araçlarıyla gelir (`ui/src/views/shared/htmlEditorOptions.ts`); yazılan değer varsayılanın üstüne tek seviye birleştirilir, yalnızca farkı yaz. |
| `dxImageUpload` | Görsel yükleme (platform) | String (16) | `uploadUrl`\*, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* | | `dxImageUpload` | Görsel yükleme (platform) | String (16) | `uploadUrl`\*, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* |
| `dxImageViewer` | Görsel gösterimi (platform) | String (16) | `width`, `height` | | `dxImageViewer` | Görsel gösterimi (platform) | String (16) | `width`, `height` |
@ -2086,7 +2086,7 @@ Dizi elemanları yaprak sayılmaz; `toolbar.items` tek bir `json` alanıdır.
| Görsel | `uploadUrl`\*, `fileFieldName`, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\* | `dxImageUpload`, `dxImageViewer` | | Görsel | `uploadUrl`\*, `fileFieldName`, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\* | `dxImageUpload`, `dxImageViewer` |
| Seçim | `text` (`dxCheckBox`), `switchedOnText`/`switchedOffText` (`dxSwitch`), `layout` (`dxRadioGroup`), `editAlphaChannel`/`keyStep` (`dxColorBox`) | ilgili editör | | Seçim | `text` (`dxCheckBox`), `switchedOnText`/`switchedOffText` (`dxSwitch`), `layout` (`dxRadioGroup`), `editAlphaChannel`/`keyStep` (`dxColorBox`) | ilgili editör |
| Kaydırıcı | `min`, `max`, `step`, `tooltip.enabled`, `tooltip.showMode`, `showRange` | `dxSlider`, `dxRangeSlider` | | Kaydırıcı | `min`, `max`, `step`, `tooltip.enabled`, `tooltip.showMode`, `showRange` | `dxSlider`, `dxRangeSlider` |
| HTML | `valueType`, `toolbar.multiline`, `toolbar.items`, `mediaResizing.enabled`, `imageUpload.fileUploadMode`, `imageUpload.uploadUrl` | `dxHtmlEditor` | | HTML | `valueType`, `toolbar.multiline`, `toolbar.items`, `mediaResizing.enabled`, `imageUpload.tabs`, `imageUpload.fileUploadMode`, `imageUpload.uploadUrl`, `tableResizing.enabled`, `tableContextMenu.enabled` | `dxHtmlEditor` |
| Liste sütunu | `format` (metin hâli), `encodeHtml`, `buttons` | hepsi — bu üçü **grid sütununu** da etkiler | | Liste sütunu | `format` (metin hâli), `encodeHtml`, `buttons` | hepsi — bu üçü **grid sütununu** da etkiler |
`\*` işaretli anahtarlar backend'de tipli DTO'ya (`GridBoxOptionsDto`, `TagBoxOptionsDto`, `\*` işaretli anahtarlar backend'de tipli DTO'ya (`GridBoxOptionsDto`, `TagBoxOptionsDto`,

View file

@ -567,7 +567,7 @@ public class PlatformDbContext :
b.Property(x => x.Type).IsRequired().HasMaxLength(64); b.Property(x => x.Type).IsRequired().HasMaxLength(64);
b.Property(x => x.Subject).IsRequired().HasMaxLength(256); b.Property(x => x.Subject).IsRequired().HasMaxLength(256);
b.Property(x => x.Content).IsRequired().HasMaxLength(2048); b.Property(x => x.Content).IsRequired();
b.Property(x => x.EntityName).IsRequired().HasMaxLength(128); b.Property(x => x.EntityName).IsRequired().HasMaxLength(128);
b.Property(x => x.EntityId).IsRequired().HasMaxLength(64); b.Property(x => x.EntityId).IsRequired().HasMaxLength(64);
}); });
@ -1186,7 +1186,7 @@ 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();
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);
@ -1302,7 +1302,7 @@ public class PlatformDbContext :
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.SocialPost)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.SocialPost)), Prefix.DbSchema);
b.ConfigureByConvention(); b.ConfigureByConvention();
b.Property(x => x.Content).IsRequired().HasMaxLength(4096); b.Property(x => x.Content).IsRequired();
b.Property(x => x.LikeCount).HasDefaultValue(0); b.Property(x => x.LikeCount).HasDefaultValue(0);
b.Property(x => x.IsOwnPost).HasDefaultValue(false); b.Property(x => x.IsOwnPost).HasDefaultValue(false);
b.Property(x => x.IsLiked).HasDefaultValue(false); b.Property(x => x.IsLiked).HasDefaultValue(false);

View file

@ -855,7 +855,6 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content") b.Property<string>("Content")
.IsRequired() .IsRequired()
.HasMaxLength(4096)
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime") b.Property<DateTime>("CreationTime")
@ -4305,8 +4304,7 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content") b.Property<string>("Content")
.IsRequired() .IsRequired()
.HasMaxLength(2048) .HasColumnType("nvarchar(max)");
.HasColumnType("nvarchar(2048)");
b.Property<DateTime>("CreationTime") b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2") .HasColumnType("datetime2")
@ -5624,7 +5622,6 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content") b.Property<string>("Content")
.IsRequired() .IsRequired()
.HasMaxLength(4096)
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime") b.Property<DateTime>("CreationTime")

View file

@ -519,7 +519,7 @@ namespace Sozsoft.Platform.Migrations
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
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)", nullable: false),
ImageUrl = table.Column<string>(type: "nvarchar(max)", 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),
@ -662,7 +662,7 @@ namespace Sozsoft.Platform.Migrations
EntityId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false), EntityId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
Type = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false), Type = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
Subject = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false), Subject = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
Content = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false), Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
FilesJson = table.Column<string>(type: "nvarchar(max)", nullable: true), FilesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false), CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
@ -802,7 +802,7 @@ namespace Sozsoft.Platform.Migrations
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Content = table.Column<string>(type: "nvarchar(max)", maxLength: 4096, nullable: false), Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
LikeCount = table.Column<int>(type: "int", nullable: false, defaultValue: 0), LikeCount = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
IsLiked = table.Column<bool>(type: "bit", nullable: false, defaultValue: false), IsLiked = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
IsOwnPost = table.Column<bool>(type: "bit", nullable: false, defaultValue: false), IsOwnPost = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),

View file

@ -852,7 +852,6 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content") b.Property<string>("Content")
.IsRequired() .IsRequired()
.HasMaxLength(4096)
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime") b.Property<DateTime>("CreationTime")
@ -4302,8 +4301,7 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content") b.Property<string>("Content")
.IsRequired() .IsRequired()
.HasMaxLength(2048) .HasColumnType("nvarchar(max)");
.HasColumnType("nvarchar(2048)");
b.Property<DateTime>("CreationTime") b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2") .HasColumnType("datetime2")
@ -5621,7 +5619,6 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("Content") b.Property<string>("Content")
.IsRequired() .IsRequired()
.HasMaxLength(4096)
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreationTime") b.Property<DateTime>("CreationTime")

View file

@ -1,27 +0,0 @@
export const sizeValues = ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt']
export const fontValues = [
'Arial',
'Courier New',
'Georgia',
'Impact',
'Lucida Console',
'Tahoma',
'Times New Roman',
'Verdana',
]
export const headerValues = [false, 1, 2, 3, 4, 5]
export const fontSizeOptions = {
inputAttr: {
'aria-label': 'Font size',
},
}
export const fontFamilyOptions = {
inputAttr: {
'aria-label': 'Font family',
},
}
export const headerOptions = {
inputAttr: {
'aria-label': 'Font family',
},
}

View file

@ -939,6 +939,27 @@ export const optionSpecs: OptionSpec[] = [
group: 'html', group: 'html',
editors: ['dxHtmlEditor'], editors: ['dxHtmlEditor'],
}, },
{
path: 'imageUpload.tabs',
label: 'imageUpload.tabs',
type: 'json',
group: 'html',
editors: ['dxHtmlEditor'],
},
{
path: 'tableResizing.enabled',
label: 'tableResizing.enabled',
type: 'boolean',
group: 'html',
editors: ['dxHtmlEditor'],
},
{
path: 'tableContextMenu.enabled',
label: 'tableContextMenu.enabled',
type: 'boolean',
group: 'html',
editors: ['dxHtmlEditor'],
},
// ── Liste sütununu da etkileyenler ─────────────────────────────── // ── Liste sütununu da etkileyenler ───────────────────────────────
{ {

View file

@ -2,6 +2,7 @@
* Hazır editorOptions kalıpları. Her kalıp mevcut JSON ile birleştirilir, * Hazır editorOptions kalıpları. Her kalıp mevcut JSON ile birleştirilir,
* diğer ayarları silmez. * diğer ayarları silmez.
*/ */
import { buildHtmlEditorDefaults } from '@/views/shared/htmlEditorOptions'
import type { JsonObject } from './jsonUtils' import type { JsonObject } from './jsonUtils'
export type EditorOptionsPreset = { export type EditorOptionsPreset = {
@ -13,84 +14,8 @@ export type EditorOptionsPreset = {
value: JsonObject value: JsonObject
} }
const htmlToolbarItems = [ const buildHtmlEditorOptions = (): JsonObject =>
'undo', JSON.parse(JSON.stringify(buildHtmlEditorDefaults())) as JsonObject
'redo',
'separator',
'size',
'font',
'separator',
'bold',
'italic',
'strike',
'underline',
'separator',
'alignLeft',
'alignCenter',
'alignRight',
'alignJustify',
'separator',
'orderedList',
'bulletList',
'separator',
'header',
'separator',
'color',
'background',
'separator',
'link',
'image',
'separator',
'clear',
'codeBlock',
'blockquote',
'separator',
'insertTable',
'deleteTable',
'insertRowAbove',
'insertRowBelow',
'deleteRow',
'insertColumnLeft',
'insertColumnRight',
'deleteColumn',
'cellProperties',
'tableProperties',
]
const buildHtmlEditorOptions = (): JsonObject => ({
mediaResizing: { enabled: true },
imageUpload: { tabs: ['file', 'url'], fileUploadMode: 'base64' },
toolbar: {
multiline: true,
items: htmlToolbarItems.map((name) => {
if (name === 'size') {
return {
name,
acceptedValues: ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt'],
options: { inputAttr: { 'aria-label': 'Font size' } },
}
}
if (name === 'font') {
return {
name,
acceptedValues: [
'Arial',
'Courier New',
'Georgia',
'Impact',
'Lucida Console',
'Tahoma',
'Times New Roman',
'Verdana',
],
options: { inputAttr: { 'aria-label': 'Font family' } },
}
}
if (name === 'header') return { name, acceptedValues: [false, 1, 2, 3, 4, 5] }
return { name }
}),
},
})
export const editorOptionsPresets: EditorOptionsPreset[] = [ export const editorOptionsPresets: EditorOptionsPreset[] = [
{ {

View file

@ -6,8 +6,8 @@ import { FaFileAlt, FaFileUpload, FaPlus, FaTrash } from 'react-icons/fa'
import { Field, FieldProps, Form, Formik } from 'formik' import { Field, FieldProps, Form, Formik } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { noteService } from '@/services/note.service' import { noteService } from '@/services/note.service'
import { HtmlEditor, ImageUpload, MediaResizing } from 'devextreme-react/html-editor' import { HtmlEditor } from 'devextreme-react/html-editor'
import { HtmlEditorToolbar } from '@/views/shared/HtmlEditorToolbarItems' import { htmlEditorDefaults } from '@/views/shared/htmlEditorOptions'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@ -147,15 +147,12 @@ function NoteModalContent({
<Field name="content"> <Field name="content">
{({ field }: FieldProps) => ( {({ field }: FieldProps) => (
<HtmlEditor <HtmlEditor
{...htmlEditorDefaults}
value={field.value} value={field.value}
onValueChanged={(e) => setFieldValue('content', e.value)} onValueChanged={(e) => setFieldValue('content', e.value)}
height={isMaximized ? '50vh' : 220} height={isMaximized ? '50vh' : 220}
placeholder={translate('::App.ListFormNoteModal.NoteModalContent')} placeholder={translate('::App.ListFormNoteModal.NoteModalContent')}
> />
<MediaResizing enabled={true} />
<ImageUpload fileUploadMode="base64" />
<HtmlEditorToolbar />
</HtmlEditor>
)} )}
</Field> </Field>
</FormItem> </FormItem>

View file

@ -7,7 +7,11 @@ import { captionize } from 'devextreme/core/utils/inflector'
import type { GroupItem } from 'devextreme/ui/form' import type { GroupItem } from 'devextreme/ui/form'
import type { ColumnFormatDto, GridDto } from '@/proxy/form/models' import type { ColumnFormatDto, GridDto } from '@/proxy/form/models'
import { getEditingFormGroups } from '../../list/Utils' import { getEditingFormGroups } from '../../list/Utils'
import { isFormItemVisible, resolveEditorType } from '../../list/shared/editingForm' import {
isFormItemVisible,
resolveEditorType,
withEditorTypeDefaults,
} from '../../list/shared/editingForm'
import type { GridColumnData } from '../../list/GridColumnData' import type { GridColumnData } from '../../list/GridColumnData'
import type { RowMode, SimpleItemWithColData } from '../types' import type { RowMode, SimpleItemWithColData } from '../types'
@ -71,7 +75,10 @@ const toFormItem = (
editorType2: colFormat.editorType2, editorType2: colFormat.editorType2,
editorType: resolveEditorType(colFormat.editorType2), editorType: resolveEditorType(colFormat.editorType2),
colSpan: colFormat.colSpan, colSpan: colFormat.colSpan,
editorOptions: { ...lookupEditorOptions, ...parsedEditorOptions }, editorOptions: withEditorTypeDefaults(colFormat.editorType2, {
...lookupEditorOptions,
...parsedEditorOptions,
}),
colData: colFormat, colData: colFormat,
tagBoxOptions: colFormat.tagBoxOptions, tagBoxOptions: colFormat.tagBoxOptions,
gridBoxOptions: colFormat.gridBoxOptions, gridBoxOptions: colFormat.gridBoxOptions,

View file

@ -1,19 +1,12 @@
import { FaTimes } from 'react-icons/fa' import { FaTimes } from 'react-icons/fa'
import { Formik, Form, Field, FieldProps } from 'formik' import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor' import { HtmlEditor } from 'devextreme-react/html-editor'
import { htmlEditorDefaults } from '@/views/shared/htmlEditorOptions'
import { useStoreState } from '@/store/store' import { useStoreState } from '@/store/store'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Button, FormContainer, FormItem } from '@/components/ui' import { Button, FormContainer, FormItem } from '@/components/ui'
import { stripHtml } from './utils' import { stripHtml } from './utils'
import {
fontFamilyOptions,
fontSizeOptions,
fontValues,
headerOptions,
headerValues,
sizeValues,
} from '@/proxy/reports/data'
interface PostFormValues { interface PostFormValues {
content: string content: string
@ -112,6 +105,7 @@ export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostM
<Field name="content"> <Field name="content">
{({ field }: FieldProps) => ( {({ field }: FieldProps) => (
<HtmlEditor <HtmlEditor
{...htmlEditorDefaults}
value={field.value} value={field.value}
height={300} height={300}
placeholder={ placeholder={
@ -120,59 +114,7 @@ export function CreatePostModal({ onClose, onSubmit, parentPostId }: CreatePostM
: translate('::App.ForumPostManagement.MessageNew') : translate('::App.ForumPostManagement.MessageNew')
} }
onValueChanged={(e) => setFieldValue('content', e.value)} onValueChanged={(e) => setFieldValue('content', e.value)}
>
<MediaResizing enabled={true} />
<ImageUpload fileUploadMode="base64" />
<Toolbar multiline={true}>
<Item name="undo" />
<Item name="redo" />
<Item name="separator" />
<Item name="size" acceptedValues={sizeValues} options={fontSizeOptions} />
<Item
name="font"
acceptedValues={fontValues}
options={fontFamilyOptions}
/> />
<Item name="separator" />
<Item name="bold" />
<Item name="italic" />
<Item name="strike" />
<Item name="underline" />
<Item name="separator" />
<Item name="alignLeft" />
<Item name="alignCenter" />
<Item name="alignRight" />
<Item name="alignJustify" />
<Item name="separator" />
<Item name="orderedList" />
<Item name="bulletList" />
<Item name="separator" />
<Item
name="header"
acceptedValues={headerValues}
options={headerOptions}
/>
<Item name="separator" />
<Item name="color" />
<Item name="background" />
<Item name="separator" />
<Item name="link" />
<Item name="image" />
<Item name="separator" />
<Item name="clear" />
<Item name="codeBlock" />
<Item name="blockquote" />
<Item name="separator" />
<Item name="insertTable" />
<Item name="deleteTable" />
<Item name="insertRowAbove" />
<Item name="insertRowBelow" />
<Item name="deleteRow" />
<Item name="insertColumnLeft" />
<Item name="insertColumnRight" />
<Item name="deleteColumn" />
</Toolbar>
</HtmlEditor>
)} )}
</Field> </Field>
</FormItem> </FormItem>

View file

@ -57,7 +57,7 @@ import {
getMobilePopupPosition, getMobilePopupPosition,
shouldUseMobileEditPopup, shouldUseMobileEditPopup,
} from './shared/editPopup' } from './shared/editPopup'
import { getValueByField, resolveEditorType } from './shared/editingForm' import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm'
import { createEditingFormHandlers } from './shared/editingFormScripts' import { createEditingFormHandlers } from './shared/editingFormScripts'
import { flattenColumns } from './shared/columns' import { flattenColumns } from './shared/columns'
import { getInitialExtraFilters, mergeFilterTriplets } from './shared/hooks' import { getInitialExtraFilters, mergeFilterTriplets } from './shared/hooks'
@ -811,6 +811,7 @@ const CardView = (props: CardViewProps) => {
} catch (error) { } catch (error) {
console.error('EditorOptions parse error:', item.fieldName, error) console.error('EditorOptions parse error:', item.fieldName, error)
} }
editorOptions = withEditorTypeDefaults(item.editorType2, editorOptions)
if (column.sourceDbType === DbTypeEnum.Date) { if (column.sourceDbType === DbTypeEnum.Date) {
editorOptions = { editorOptions = {

View file

@ -39,7 +39,7 @@ import {
getMobilePopupPosition, getMobilePopupPosition,
shouldUseMobileEditPopup, shouldUseMobileEditPopup,
} from './shared/editPopup' } from './shared/editPopup'
import { getValueByField, resolveEditorType } from './shared/editingForm' import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm'
import { flattenGridColumns } from './shared/columns' import { flattenGridColumns } from './shared/columns'
import { useListFormCustomSources, useListFormGridDto, useWidgetGroupHeight } from './shared/hooks' import { useListFormCustomSources, useListFormGridDto, useWidgetGroupHeight } from './shared/hooks'
@ -567,6 +567,7 @@ const SchedulerView = (props: SchedulerViewProps) => {
} catch (err) { } catch (err) {
console.log(err) console.log(err)
} }
editorOptions = withEditorTypeDefaults(i.editorType2, editorOptions)
const listFormField = i const listFormField = i
const listFormColumn = listFormColumns.find( const listFormColumn = listFormColumns.find(

View file

@ -2,8 +2,8 @@ import type { GridDto, TodoOptionDto } from '@/proxy/form/models'
import type { IdentityUserDto } from '@/proxy/admin/models' import type { IdentityUserDto } from '@/proxy/admin/models'
import type { DataGridRef } from 'devextreme-react/data-grid' import type { DataGridRef } from 'devextreme-react/data-grid'
import type CustomStore from 'devextreme/data/custom_store' import type CustomStore from 'devextreme/data/custom_store'
import { HtmlEditor, ImageUpload, MediaResizing } from 'devextreme-react/html-editor' import { HtmlEditor } from 'devextreme-react/html-editor'
import { HtmlEditorToolbar } from '@/views/shared/HtmlEditorToolbarItems' import { htmlEditorDefaults } from '@/views/shared/htmlEditorOptions'
import Toolbar, { Item as ToolbarItem } from 'devextreme-react/toolbar' import Toolbar, { Item as ToolbarItem } from 'devextreme-react/toolbar'
import TagBox from 'devextreme-react/tag-box' import TagBox from 'devextreme-react/tag-box'
import { AVATAR_URL } from '@/constants/app.constant' import { AVATAR_URL } from '@/constants/app.constant'
@ -1089,6 +1089,7 @@ const TodoBoardContent = ({
<div className="flex min-h-[260px] flex-1 flex-col pt-4"> <div className="flex min-h-[260px] flex-1 flex-col pt-4">
{activeTab === 'description' && options.descriptionExpr && ( {activeTab === 'description' && options.descriptionExpr && (
<HtmlEditor <HtmlEditor
{...htmlEditorDefaults}
height="100%" height="100%"
placeholder={translate('::App.ListFormTodoBoard.DescriptionPlaceholder')} placeholder={translate('::App.ListFormTodoBoard.DescriptionPlaceholder')}
readOnly={!canUpdate} readOnly={!canUpdate}
@ -1099,11 +1100,7 @@ const TodoBoardContent = ({
[options.descriptionExpr!]: event.value, [options.descriptionExpr!]: event.value,
})) }))
} }
> />
<MediaResizing enabled={true} />
<ImageUpload fileUploadMode="base64" />
<HtmlEditorToolbar />
</HtmlEditor>
)} )}
{activeTab === 'timesheet' && options.timesheetExpr && ( {activeTab === 'timesheet' && options.timesheetExpr && (
<div className="flex min-h-0 flex-1 flex-col gap-2"> <div className="flex min-h-0 flex-1 flex-col gap-2">

View file

@ -15,6 +15,7 @@ import {
} from '@/proxy/form/models' } from '@/proxy/form/models'
import { scanBarcode } from '@/components/shared/BarcodeScanner/scanBarcode' import { scanBarcode } from '@/components/shared/BarcodeScanner/scanBarcode'
import { getNextSequenceValue } from '@/services/form.service' import { getNextSequenceValue } from '@/services/form.service'
import { mergeHtmlEditorOptions } from '@/views/shared/htmlEditorOptions'
import type { EditorType2, RowMode, SimpleItemWithColData } from '../../form/types' import type { EditorType2, RowMode, SimpleItemWithColData } from '../../form/types'
import { import {
autoNumber, autoNumber,
@ -423,6 +424,15 @@ export const resolveEditorType = (editorType2?: EditorType2) => {
return editorType2 return editorType2
} }
/** Editör tipinin platform varsayılanlarını yapılandırmadan gelen editorOptions'ın altına serer. */
export const withEditorTypeDefaults = <T extends Record<string, any>>(
editorType2: EditorType2 | undefined,
editorOptions: T,
): T =>
editorType2 === 'dxHtmlEditor'
? mergeHtmlEditorOptions(editorOptions)
: editorOptions
type FormItemMapperParams = { type FormItemMapperParams = {
mode: RowMode mode: RowMode
translate: (key: string) => string translate: (key: string) => string
@ -484,11 +494,11 @@ export const createFormItemMapper = ({
? { readOnly: true } ? { readOnly: true }
: {} : {}
const editorOptions: EditorOptionsWithButtons = { const editorOptions: EditorOptionsWithButtons = withEditorTypeDefaults(colFormat.editorType2, {
...defaultEditorOptions, ...defaultEditorOptions,
...parsedEditorOptions, ...parsedEditorOptions,
...forcedEditorOptions, ...forcedEditorOptions,
} })
if (editorOptions.buttons) { if (editorOptions.buttons) {
editorOptions.buttons = editorOptions.buttons.map((btn: any) => { editorOptions.buttons = editorOptions.buttons.map((btn: any) => {

View file

@ -1,46 +0,0 @@
/**
* DevExtreme HtmlEditor için standart araç çubuğu.
* NoteModal ve TodoBoard içinde birebir aynı ~40 satırlık tanım tekrar ediyordu.
*/
import { Item, Toolbar } from 'devextreme-react/html-editor'
import {
fontFamilyOptions,
fontSizeOptions,
fontValues,
headerOptions,
headerValues,
sizeValues,
} from '@/proxy/reports/data'
export const HtmlEditorToolbar = () => (
<Toolbar multiline>
<Item name="undo" />
<Item name="redo" />
<Item name="separator" />
<Item name="size" acceptedValues={sizeValues} options={fontSizeOptions} />
<Item name="font" acceptedValues={fontValues} options={fontFamilyOptions} />
<Item name="separator" />
<Item name="bold" />
<Item name="italic" />
<Item name="underline" />
<Item name="strike" />
<Item name="separator" />
<Item name="orderedList" />
<Item name="bulletList" />
<Item name="separator" />
<Item name="header" acceptedValues={headerValues} options={headerOptions} />
<Item name="separator" />
<Item name="color" />
<Item name="background" />
<Item name="separator" />
<Item name="alignLeft" />
<Item name="alignCenter" />
<Item name="alignRight" />
<Item name="alignJustify" />
<Item name="separator" />
<Item name="link" />
<Item name="image" />
<Item name="separator" />
<Item name="clear" />
</Toolbar>
)

View file

@ -0,0 +1,114 @@
/**
* Uygulamadaki bütün HtmlEditor'ların ortak varsayılanı. Sabit ekranlar (not, todo, forum)
* `<HtmlEditor {...htmlEditorDefaults}>` ile, dinamik formlar `mergeHtmlEditorOptions` ile
* aynı yetenek setini alır; ListForm'daki "HTML toolbar" kalıbı da buradan beslenir.
* Değerler JSON'a yazılabilir kalmalıdır — kalıp editorOptions kolonuna olduğu gibi kaydedilir.
*/
import type { Properties as HtmlEditorProperties } from 'devextreme/ui/html_editor'
const sizeValues = ['8pt', '10pt', '12pt', '14pt', '18pt', '24pt', '36pt']
const fontValues = [
'Arial',
'Courier New',
'Georgia',
'Impact',
'Lucida Console',
'Segoe UI',
'Tahoma',
'Times New Roman',
'Verdana',
]
const headerValues = [false, 1, 2, 3, 4, 5, 6]
type ToolbarItem = NonNullable<NonNullable<HtmlEditorProperties['toolbar']>['items']>[number]
export const htmlToolbarItemNames = [
'undo',
'redo',
'separator',
'header',
'size',
'font',
'separator',
'bold',
'italic',
'underline',
'strike',
'superscript',
'subscript',
'separator',
'color',
'background',
'separator',
'alignLeft',
'alignCenter',
'alignRight',
'alignJustify',
'separator',
'orderedList',
'bulletList',
'decreaseIndent',
'increaseIndent',
'separator',
'blockquote',
'codeBlock',
'link',
'image',
'separator',
'insertTable',
'insertHeaderRow',
'insertRowAbove',
'insertRowBelow',
'deleteRow',
'insertColumnLeft',
'insertColumnRight',
'deleteColumn',
'cellProperties',
'tableProperties',
'deleteTable',
'separator',
'clear',
] as const
const toToolbarItem = (name: (typeof htmlToolbarItemNames)[number]): ToolbarItem => {
if (name === 'size') {
return { name, acceptedValues: sizeValues, options: { inputAttr: { 'aria-label': 'Font size' } } }
}
if (name === 'font') {
return { name, acceptedValues: fontValues, options: { inputAttr: { 'aria-label': 'Font family' } } }
}
if (name === 'header') {
return { name, acceptedValues: headerValues, options: { inputAttr: { 'aria-label': 'Header' } } }
}
return { name }
}
export const buildHtmlEditorDefaults = () =>
({
toolbar: { multiline: true, items: htmlToolbarItemNames.map(toToolbarItem) },
// Varsayılan sekme yalnızca 'url'dir; bilgisayardan resim seçmek için 'file' açıkça verilmeli.
imageUpload: { tabs: ['file', 'url'], fileUploadMode: 'base64' },
mediaResizing: { enabled: true },
tableResizing: { enabled: true },
tableContextMenu: { enabled: true },
}) satisfies HtmlEditorProperties
export const htmlEditorDefaults = buildHtmlEditorDefaults()
const nestedKeys = ['toolbar', 'imageUpload', 'mediaResizing', 'tableResizing', 'tableContextMenu'] as const
/**
* Yapılandırmadan gelen editorOptions'ı varsayılanın üstüne yazar. İç içe bloklar tek seviye
* birleştirilir; aksi hâlde yalnızca `imageUpload.fileUploadMode` yazan eski bir kayıt
* dosya sekmesini ve toolbar'ı sessizce kaybederdi.
*/
export const mergeHtmlEditorOptions = <T extends Record<string, any>>(options: T): T => {
const merged: Record<string, any> = { ...htmlEditorDefaults, ...options }
for (const key of nestedKeys) {
const own = options[key]
if (own && typeof own === 'object' && !Array.isArray(own)) {
merged[key] = { ...htmlEditorDefaults[key], ...own }
}
}
return merged as T
}