Claude güncellemesi Editor Options Builder Modal güncellemeleri

This commit is contained in:
Sedat ÖZTÜRK 2026-08-09 01:14:57 +03:00
parent 38e3ef5797
commit a1f5e5e539
9 changed files with 2233 additions and 950 deletions

View file

@ -8,21 +8,31 @@ import { FaSlidersH } from 'react-icons/fa'
import { bool, number, object, string } from 'yup'
import { dbSourceTypeOptions, listFormAlignmentOptions } from '../options'
import EditorOptionsBuilderDialog from '../json-row-operations/EditorOptionsBuilderDialog'
import { isValidJsonText } from '../json-row-operations/editor-options/jsonUtils'
import { FormFieldEditProps } from './FormFields'
import { tooltipFormatListOptions } from '@/proxy/admin/list-form/options'
const schema = object().shape({
fieldName: string().required().max(100),
captionName: string(),
placeHolder: string(),
bandName: string(),
isActive: bool().required(),
visible: bool().required(),
allowSearch: bool().required(),
captionName: string().nullable(),
placeHolder: string().nullable(),
bandName: string().nullable(),
// Bu alanlar veritabanında null olabiliyor; `required()` bırakıldığında form
// sessizce submit edilmiyor ve "kaydettim ama değişmedi" durumu oluşuyordu.
isActive: bool().nullable(),
visible: bool().nullable(),
allowSearch: bool().nullable(),
sourceDbType: number().required(),
alignment: string(),
format: string(),
editorOptions: string(),
// Format listesindeki "None" seçeneği null döndürüyor.
alignment: string().nullable(),
format: string().nullable(),
editorOptions: string()
.nullable()
.test(
'is-json-object',
'editorOptions geçerli bir JSON nesnesi olmalıdır.',
(value) => !value?.trim() || isValidJsonText(value),
),
})
function FormFieldTabDetails({
@ -103,13 +113,18 @@ function FormFieldTabDetails({
</div>
</Card>
<Card>
<FormItem label={translate('::ListForms.ListFormEdit.CustomValueType')}>
<FormItem
label={translate('::ListForms.ListFormEdit.CustomValueType')}
invalid={!!errors.sourceDbType && !!touched.sourceDbType}
errorMessage={errors.sourceDbType as string}
>
{/* Temizlenirse zorunlu alan doğrulaması formu sessizce bloke ediyordu. */}
<Field type="text" name="sourceDbType">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
isClearable={false}
options={dbSourceTypeOptions}
value={dbSourceTypeOptions.filter(
(option: any) => option.value === values.sourceDbType,
@ -180,6 +195,7 @@ function FormFieldTabDetails({
<EditorOptionsBuilderDialog
isOpen={isEditorOptionsDialogOpen}
value={values.editorOptions}
editorType={values.editorType2}
onClose={() => setIsEditorOptionsDialogOpen(false)}
onApply={(val) => setFieldValue('editorOptions', val)}
/>

View file

@ -0,0 +1,256 @@
import { useEffect, useState } from 'react'
import { FaExclamationTriangle, FaUndo } from 'react-icons/fa'
import { coerceNumber, coerceSize, leafToText } from './jsonUtils'
import type { OptionSpec } from './optionSpecs'
export const controlClass =
'w-full min-w-0 h-9 px-2 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-100 focus:outline-none focus:border-indigo-400 disabled:opacity-60'
const textAreaClass = controlClass.replace('h-9', 'h-24 py-1 font-mono text-xs')
type OptionFieldProps = {
spec: OptionSpec
value: unknown
onChange: (value: unknown) => void
}
/** Değerin spec ile uyuşup uyuşmadığını söyler; uyuşmuyorsa alan kilitlenir. */
const detectMismatch = (spec: OptionSpec, value: unknown): string | undefined => {
if (value === undefined || value === null) return undefined
const isObjectLike = typeof value === 'object'
if (spec.type === 'json') return undefined
if (spec.type === 'stringList') {
return Array.isArray(value) ? undefined : 'Bu alan dizi bekliyor, mevcut değer dizi değil.'
}
if (isObjectLike) {
return 'Bu yol şu anda nesne/dizi değeri tutuyor. Ham JSON sekmesinden düzenleyin.'
}
if (spec.type === 'number' && typeof value !== 'number') {
return `Sayı bekleniyor, mevcut değer ${typeof value}. Yeni değer girildiğinde sayıya çevrilir.`
}
if (spec.type === 'boolean' && typeof value !== 'boolean') {
return `Boolean bekleniyor, mevcut değer ${typeof value}. Listeden seçim yaparsan düzelir.`
}
return undefined
}
const BooleanControl = ({ value, onChange }: { value: unknown; onChange: (v: unknown) => void }) => {
const current = value === true || value === 'true' ? 'true' : value === false || value === 'false' ? 'false' : ''
return (
<select
className={controlClass}
value={current}
onChange={(event) => {
if (!event.target.value) return onChange(undefined)
onChange(event.target.value === 'true')
}}
>
<option value=""> tanımsız </option>
<option value="true">true</option>
<option value="false">false</option>
</select>
)
}
const SelectControl = ({
spec,
value,
onChange,
}: {
spec: OptionSpec
value: unknown
onChange: (v: unknown) => void
}) => {
const current = value === undefined || value === null ? '' : String(value)
const known = spec.choices?.some((choice) => String(choice.value) === current)
return (
<select
className={controlClass}
value={current}
onChange={(event) => {
const raw = event.target.value
if (!raw) return onChange(undefined)
const choice = spec.choices?.find((item) => String(item.value) === raw)
onChange(choice ? choice.value : raw)
}}
>
<option value=""> tanımsız </option>
{spec.choices?.map((choice) => (
<option key={String(choice.value)} value={String(choice.value)}>
{choice.label}
</option>
))}
{current && !known && <option value={current}>{current} (mevcut değer)</option>}
</select>
)
}
const JsonControl = ({ value, onChange }: { value: unknown; onChange: (v: unknown) => void }) => {
const serialized = value === undefined ? '' : JSON.stringify(value, null, 2)
const [draft, setDraft] = useState(serialized)
const [invalid, setInvalid] = useState(false)
useEffect(() => {
setDraft(serialized)
setInvalid(false)
}, [serialized])
return (
<div>
<textarea
className={`${textAreaClass} ${invalid ? '!border-red-400' : ''}`}
value={draft}
spellCheck={false}
placeholder='{"enabled": true}'
onChange={(event) => setDraft(event.target.value)}
onBlur={() => {
const trimmed = draft.trim()
if (!trimmed) {
setInvalid(false)
onChange(undefined)
return
}
try {
onChange(JSON.parse(trimmed))
setInvalid(false)
} catch {
setInvalid(true)
}
}}
/>
{invalid && <p className="mt-1 text-[11px] text-red-500">Geçersiz JSON, değer kaydedilmedi.</p>}
</div>
)
}
const StringListControl = ({
spec,
value,
onChange,
}: {
spec: OptionSpec
value: unknown
onChange: (v: unknown) => void
}) => (
<input
className={controlClass}
value={Array.isArray(value) ? value.join(', ') : leafToText(value)}
placeholder={spec.placeholder}
onChange={(event) => {
const items = event.target.value
.split(',')
.map((item) => item.trim())
.filter(Boolean)
onChange(items.length ? items : undefined)
}}
/>
)
const OptionField = ({ spec, value, onChange }: OptionFieldProps) => {
const mismatch = detectMismatch(spec, value)
const isSet = value !== undefined
// Nesne/dizi tutan bir yol basit girdiyle düzenlenemez; ham JSON'a yönlendiriyoruz.
// Basit tip uyuşmazlıkları (metin yazılmış sayı gibi) düzenlenebilir kalır.
const locked = !!mismatch && spec.type !== 'json' && !!value && typeof value === 'object'
const renderControl = () => {
if (locked) {
return <input disabled readOnly className={controlClass} value={leafToText(value)} />
}
switch (spec.type) {
case 'boolean':
return <BooleanControl value={value} onChange={onChange} />
case 'select':
return <SelectControl spec={spec} value={value} onChange={onChange} />
case 'json':
return <JsonControl value={value} onChange={onChange} />
case 'stringList':
return <StringListControl spec={spec} value={value} onChange={onChange} />
case 'number':
return (
<input
className={controlClass}
type="number"
value={typeof value === 'number' ? value : leafToText(value)}
placeholder={spec.placeholder}
onChange={(event) => onChange(coerceNumber(event.target.value))}
/>
)
case 'size':
return (
<input
className={controlClass}
value={leafToText(value)}
placeholder={spec.placeholder}
onChange={(event) => onChange(coerceSize(event.target.value))}
/>
)
default:
return (
<input
className={controlClass}
value={leafToText(value)}
placeholder={spec.placeholder}
onChange={(event) => onChange(event.target.value === '' ? undefined : event.target.value)}
/>
)
}
}
return (
<div
className={`min-w-0 rounded-md py-0.5 pl-2 transition-colors ${
isSet
? 'border-l-2 border-indigo-400 bg-indigo-50/40 dark:bg-indigo-950/20'
: 'border-l-2 border-transparent'
}`}
>
<div className="mb-1 flex items-center gap-1">
<span
className={`min-w-0 truncate text-xs ${isSet ? 'font-semibold text-indigo-600 dark:text-indigo-300' : 'text-gray-500'}`}
title={spec.help ? `${spec.path}${spec.help}` : spec.path}
>
{spec.label}
</span>
{spec.platform && (
<span
className="rounded bg-emerald-100 px-1 text-[10px] text-emerald-700 dark:bg-emerald-900 dark:text-emerald-200"
title="Bu alan backend tarafından tipli olarak okunur; yanlış tipte yazılırsa yok sayılır."
>
platform
</span>
)}
{isSet && (
<button
type="button"
className="ml-auto shrink-0 text-[10px] text-gray-400 hover:text-red-500"
title="Bu ayarı JSON'dan kaldır"
onClick={() => onChange(undefined)}
>
<FaUndo />
</button>
)}
</div>
{renderControl()}
{mismatch && (
<p className="mt-1 flex items-start gap-1 text-[11px] text-amber-600">
<FaExclamationTriangle className="mt-[2px] shrink-0" />
<span>{mismatch}</span>
</p>
)}
{/* Yardım metni tek satıra kısaltılır; tamamı tooltip'te durur. */}
{!mismatch && spec.help && (
<p className="mt-1 truncate text-[11px] text-gray-400" title={spec.help}>
{spec.help}
</p>
)}
</div>
)
}
export default OptionField

View file

@ -0,0 +1,181 @@
/**
* editorOptions JSON'u üzerinde güvenli okuma/yazma yardımcıları.
*
* Builder her değişiklikte tüm nesneyi immutable şekilde yeniden üretir; böylece
* React state güncellemeleri kaçmaz ve "değeri sildim ama JSON'da duruyor"
* türünden hatalar oluşmaz.
*/
export type JsonObject = Record<string, unknown>
export type LeafValueType = 'string' | 'number' | 'boolean' | 'json'
export type ParseResult = {
/** Ayrıştırılabildiyse nesne, aksi halde boş nesne. */
data: JsonObject
/** Ayrıştırma başarısızsa kullanıcıya gösterilecek mesaj. */
error?: string
}
const isPlainObject = (value: unknown): value is JsonObject =>
!!value && typeof value === 'object' && !Array.isArray(value)
/** `a.b.c` yolunu parçalara ayırır, boş parçaları eler. */
export const splitPath = (path: string) =>
path
.split('.')
.map((key) => key.trim())
.filter(Boolean)
export const parseOptionsJson = (value?: string): ParseResult => {
const trimmed = value?.trim()
if (!trimmed || trimmed.toLowerCase() === 'null') {
return { data: {} }
}
try {
const parsed = JSON.parse(trimmed)
if (!isPlainObject(parsed)) {
return { data: {}, error: 'editorOptions bir JSON nesnesi ({ ... }) olmalıdır.' }
}
return { data: parsed }
} catch (error) {
return { data: {}, error: (error as Error).message }
}
}
export const getByPath = (target: JsonObject, path: string): unknown =>
splitPath(path).reduce<unknown>((cursor, key) => {
if (!isPlainObject(cursor)) return undefined
return cursor[key]
}, target)
/** Yol üzerindeki değeri yazar; ara nesneler yoksa oluşturur. Yeni nesne döner. */
export const setByPath = (target: JsonObject, path: string, value: unknown): JsonObject => {
const keys = splitPath(path)
if (!keys.length) return target
const root: JsonObject = { ...target }
let cursor: JsonObject = root
for (let index = 0; index < keys.length - 1; index++) {
const key = keys[index]
const next = cursor[key]
cursor[key] = isPlainObject(next) ? { ...next } : {}
cursor = cursor[key] as JsonObject
}
cursor[keys[keys.length - 1]] = value
return root
}
/**
* Yol üzerindeki değeri siler ve geride kalan boş ara nesneleri de temizler.
* Aksi halde `{"format":{}}` gibi DevExtreme'in anlamadığı artıklar kalıyordu.
*/
export const unsetByPath = (target: JsonObject, path: string): JsonObject => {
const keys = splitPath(path)
if (!keys.length) return target
const remove = (node: JsonObject, index: number): JsonObject => {
const key = keys[index]
if (!Object.prototype.hasOwnProperty.call(node, key)) return node
if (index === keys.length - 1) {
const { [key]: _removed, ...rest } = node
return rest
}
const child = node[key]
if (!isPlainObject(child)) return node
const nextChild = remove(child, index + 1)
if (Object.keys(nextChild).length === 0) {
const { [key]: _removed, ...rest } = node
return rest
}
return { ...node, [key]: nextChild }
}
return remove(target, 0)
}
/** `undefined` yazmak yerine yolu tamamen kaldırır. */
export const applyPathValue = (target: JsonObject, path: string, value: unknown): JsonObject =>
value === undefined ? unsetByPath(target, path) : setByPath(target, path, value)
/**
* Nesnedeki tüm yaprak yolları döner. Diziler yaprak kabul edilir; aksi halde
* `toolbar.items.0.name` gibi kullanışsız yollar üretilirdi.
*/
export const collectLeafPaths = (node: JsonObject, prefix = ''): string[] =>
Object.entries(node).flatMap(([key, value]) => {
const path = prefix ? `${prefix}.${key}` : key
if (isPlainObject(value)) {
const children = collectLeafPaths(value, path)
// Boş nesne de kaybolmasın diye kendisini yaprak sayıyoruz.
return children.length ? children : [path]
}
return [path]
})
/** Sayı gibi görünen metni sayıya çevirir; `100%` gibi CSS değerlerini korur. */
export const coerceSize = (raw: string): number | string | undefined => {
const trimmed = raw.trim()
if (!trimmed) return undefined
return /^-?\d+(\.\d+)?$/.test(trimmed) ? Number(trimmed) : trimmed
}
export const coerceNumber = (raw: string): number | undefined => {
const trimmed = raw.trim()
if (!trimmed) return undefined
const parsed = Number(trimmed)
return Number.isNaN(parsed) ? undefined : parsed
}
export const inferLeafType = (value: unknown): LeafValueType => {
if (typeof value === 'boolean') return 'boolean'
if (typeof value === 'number') return 'number'
if (typeof value === 'string') return 'string'
return 'json'
}
/** Yaprak değerini serbest metin girdisinde göstermek için metne çevirir. */
export const leafToText = (value: unknown): string => {
if (value === undefined || value === null) return ''
if (typeof value === 'string') return value
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
return JSON.stringify(value)
}
/** Serbest metni seçilen tipe göre değere çevirir. */
export const textToLeaf = (text: string, type: LeafValueType): unknown => {
if (type === 'boolean') return text === 'true'
if (type === 'number') return coerceNumber(text)
if (type === 'json') {
try {
return JSON.parse(text)
} catch {
return text
}
}
return text
}
export const isValidJsonText = (text: string) => {
const trimmed = text.trim()
if (!trimmed) return true
try {
const parsed = JSON.parse(trimmed)
return isPlainObject(parsed)
} catch {
return false
}
}
export const stringifyOptions = (options: JsonObject) =>
Object.keys(options).length ? JSON.stringify(options) : ''
export const prettyStringifyOptions = (options: JsonObject) =>
Object.keys(options).length ? JSON.stringify(options, null, 2) : ''

View file

@ -0,0 +1,902 @@
/**
* editorOptions builder'ın veri sözlüğü.
*
* Buradaki her kayıt, DevExtreme editörüne (veya platformun kendi editörlerine)
* gerçekten geçirilen bir ayarı tarif eder. UI tamamen bu listeden üretilir;
* yeni bir ayar eklemek için tek yapılması gereken buraya bir satır yazmaktır.
*/
export type OptionInputType =
| 'boolean' // üç durumlu: tanımsız / true / false
| 'number'
| 'text'
| 'select'
| 'size' // sayı ya da `100%` gibi CSS değeri
| 'stringList' // virgülle ayrılmış -> string[]
| 'json'
export type OptionChoice = { value: string | number; label: string }
export type OptionSpec = {
/** editorOptions içindeki yol. Örn: `format.precision` */
path: string
label: string
type: OptionInputType
group: OptionGroupKey
/**
* Bu ayarın anlamlı olduğu editör tipleri. Boş bırakılırsa tüm editörlerde
* gösterilir.
*/
editors?: string[]
help?: string
placeholder?: string
choices?: OptionChoice[]
/**
* Backend'in tipli DTO'ya (GridBoxOptionsDto / TagBoxOptionsDto /
* ImageUploadOptionsDto) deserialize ettiği alanlar. Bunlar yanlış tipte
* yazılırsa sessizce yok sayılır, bu yüzden ayrıca işaretliyoruz.
*/
platform?: boolean
}
export type OptionGroupKey =
| 'common'
| 'appearance'
| 'text'
| 'number'
| 'date'
| 'dropdown'
| 'tagBox'
| 'gridBox'
| 'image'
| 'choice'
| 'slider'
| 'html'
| 'grid'
export const optionGroups: { key: OptionGroupKey; title: string; description: string }[] = [
{
key: 'common',
title: 'Genel Davranış',
description: 'Tüm editörlerde geçerli olan durum, yetki ve doğrulama ayarları.',
},
{
key: 'appearance',
title: 'Görünüm ve Boyut',
description: 'Etiket, boyut ve stil ayarları. Boyutlar sayı veya 100% gibi CSS değeri olabilir.',
},
{ key: 'text', title: 'Metin / Maske', description: 'TextBox, TextArea ve Autocomplete ayarları.' },
{ key: 'number', title: 'Sayı', description: 'NumberBox için sınır, adım ve format ayarları.' },
{ key: 'date', title: 'Tarih / Saat', description: 'DateBox, Calendar ve DateRangeBox ayarları.' },
{
key: 'dropdown',
title: 'Açılır Liste / Arama',
description: 'SelectBox, Lookup, TagBox ve GridBox için ortak liste ve arama ayarları.',
},
{ key: 'tagBox', title: 'TagBox', description: 'Çoklu seçim editörüne özel ayarlar.' },
{
key: 'gridBox',
title: 'GridBox',
description: 'Tablo görünümlü açılır seçici. Bu alanlar backend tarafından GridBoxOptions olarak okunur.',
},
{
key: 'image',
title: 'Görsel Yükleme / Önizleme',
description: 'ImageUpload ve ImageViewer editörleri için yükleme ve küçük resim ayarları.',
},
{ key: 'choice', title: 'Onay / Seçim', description: 'CheckBox, Switch, RadioGroup ve ColorBox ayarları.' },
{ key: 'slider', title: 'Kaydırıcı', description: 'Slider ve RangeSlider ayarları.' },
{ key: 'html', title: 'HTML Editör', description: 'dxHtmlEditor araç çubuğu ve görsel ayarları.' },
{
key: 'grid',
title: 'Liste Sütunu Etkisi',
description:
'Bu alanlar düzenleme editörünün yanı sıra listedeki sütun gösterimini de etkiler.',
},
]
/* ------------------------------------------------------------------ *
* Editör tipi kümeleri
* ------------------------------------------------------------------ */
const TEXT_EDITORS = ['dxTextBox', 'dxTextArea', 'dxAutocomplete']
const DROPDOWN_EDITORS = [
'dxSelectBox',
'dxLookup',
'dxTagBox',
'dxDropDownBox',
'dxGridBox',
'dxAutocomplete',
]
const DATE_EDITORS = ['dxDateBox', 'dxCalendar', 'dxDateRangeBox']
const IMAGE_EDITORS = ['dxImageUpload', 'dxImageViewer']
const SLIDER_EDITORS = ['dxSlider', 'dxRangeSlider']
/** Metin girişi olmayan editörlerde placeholder/mask alanlarını gizlemek için. */
const NON_INPUT_EDITORS = ['dxCheckBox', 'dxSwitch', 'dxRadioGroup', ...SLIDER_EDITORS]
const booleanHelp = 'Boş bırakılırsa JSON\'a yazılmaz ve DevExtreme varsayılanı geçerli olur.'
/* ------------------------------------------------------------------ *
* Ayar sözlüğü
* ------------------------------------------------------------------ */
export const optionSpecs: OptionSpec[] = [
// ── Genel ────────────────────────────────────────────────────────
{
path: 'readOnly',
label: 'readOnly',
type: 'boolean',
group: 'common',
help: 'Alan görünür ama değiştirilemez. ' + booleanHelp,
},
{
path: 'disabled',
label: 'disabled',
type: 'boolean',
group: 'common',
help: 'Alan pasifleşir ve forma dahil edilmez. ' + booleanHelp,
},
{
path: 'visible',
label: 'visible',
type: 'boolean',
group: 'common',
help: 'Editörü tamamen gizler. ' + booleanHelp,
},
{ path: 'hint', label: 'hint', type: 'text', group: 'common', help: 'Fare ile üzerine gelince çıkan ipucu.' },
{ path: 'tabIndex', label: 'tabIndex', type: 'number', group: 'common', help: 'Tab ile gezinme sırası.' },
{
path: 'valueChangeEvent',
label: 'valueChangeEvent',
type: 'text',
group: 'common',
placeholder: 'change / input / keyup',
help: 'Değerin hangi DOM olayında işleneceği. Anlık kaydetme için input.',
},
{
path: 'validationMessageMode',
label: 'validationMessageMode',
type: 'select',
group: 'common',
choices: [
{ value: 'auto', label: 'auto' },
{ value: 'always', label: 'always' },
{ value: 'hidden', label: 'hidden' },
],
},
{
path: 'validationMessagePosition',
label: 'validationMessagePosition',
type: 'select',
group: 'common',
choices: [
{ value: 'top', label: 'top' },
{ value: 'left', label: 'left' },
{ value: 'right', label: 'right' },
{ value: 'bottom', label: 'bottom' },
],
},
{
path: 'elementAttr.class',
label: 'elementAttr.class',
type: 'text',
group: 'common',
placeholder: 'my-editor-class',
help: 'Editör kök elemanına eklenecek CSS sınıfı.',
},
{
path: 'inputAttr.aria-label',
label: 'inputAttr.aria-label',
type: 'text',
group: 'common',
help: 'Erişilebilirlik etiketi.',
},
{
path: 'inputAttr.style',
label: 'inputAttr.style',
type: 'text',
group: 'common',
placeholder: 'text-align: right',
help: 'Girdi alanına doğrudan inline CSS uygular. Sayısal alanları sağa yaslamak için kullanışlıdır.',
},
// ── Görünüm ──────────────────────────────────────────────────────
{
path: 'width',
label: 'width',
type: 'size',
group: 'appearance',
placeholder: '100% veya 240',
help: 'Sadece rakam yazarsan sayı olarak kaydedilir (240). Yüzde için 100% yaz.',
},
{ path: 'height', label: 'height', type: 'size', group: 'appearance', placeholder: '200' },
{
path: 'placeholder',
label: 'placeholder',
type: 'text',
group: 'appearance',
help: 'Alan boşken görünen metin. Sütunun PlaceHolder alanını ezer.',
},
{ path: 'label', label: 'label', type: 'text', group: 'appearance', help: 'Editör içi etiket metni.' },
{
path: 'labelMode',
label: 'labelMode',
type: 'select',
group: 'appearance',
choices: [
{ value: 'static', label: 'static' },
{ value: 'floating', label: 'floating' },
{ value: 'hidden', label: 'hidden' },
{ value: 'outside', label: 'outside' },
],
},
{
path: 'stylingMode',
label: 'stylingMode',
type: 'select',
group: 'appearance',
choices: [
{ value: 'outlined', label: 'outlined' },
{ value: 'filled', label: 'filled' },
{ value: 'underlined', label: 'underlined' },
],
},
{
path: 'showClearButton',
label: 'showClearButton',
type: 'boolean',
group: 'appearance',
help: 'Temizleme (x) butonu. ' + booleanHelp,
},
// ── Metin / Maske ────────────────────────────────────────────────
{
path: 'mode',
label: 'mode',
type: 'select',
group: 'text',
editors: TEXT_EDITORS,
choices: [
{ value: 'text', label: 'text' },
{ value: 'email', label: 'email' },
{ value: 'password', label: 'password' },
{ value: 'search', label: 'search' },
{ value: 'tel', label: 'tel' },
{ value: 'url', label: 'url' },
],
},
{ path: 'maxLength', label: 'maxLength', type: 'number', group: 'text', editors: TEXT_EDITORS },
{
path: 'mask',
label: 'mask',
type: 'text',
group: 'text',
editors: TEXT_EDITORS,
placeholder: '(000) 000-0000',
},
{ path: 'maskChar', label: 'maskChar', type: 'text', group: 'text', editors: TEXT_EDITORS, placeholder: '_' },
{
path: 'maskInvalidMessage',
label: 'maskInvalidMessage',
type: 'text',
group: 'text',
editors: TEXT_EDITORS,
},
{
path: 'showMaskMode',
label: 'showMaskMode',
type: 'select',
group: 'text',
editors: TEXT_EDITORS,
choices: [
{ value: 'always', label: 'always' },
{ value: 'onFocus', label: 'onFocus' },
],
},
{
path: 'useMaskedValue',
label: 'useMaskedValue',
type: 'boolean',
group: 'text',
editors: TEXT_EDITORS,
help: 'true ise maske karakterleri de veritabanına yazılır. ' + booleanHelp,
},
{
path: 'maskRules.X',
label: 'maskRules.X',
type: 'text',
group: 'text',
editors: TEXT_EDITORS,
placeholder: '[0-9]',
help: 'Maskede X karakterinin karşılığı olan düzenli ifade.',
},
{ path: 'spellcheck', label: 'spellcheck', type: 'boolean', group: 'text', editors: TEXT_EDITORS },
{
path: 'autoResizeEnabled',
label: 'autoResizeEnabled',
type: 'boolean',
group: 'text',
editors: ['dxTextArea'],
help: 'İçerik büyüdükçe alan uzar. ' + booleanHelp,
},
{ path: 'minHeight', label: 'minHeight', type: 'size', group: 'text', editors: ['dxTextArea'] },
{ path: 'maxHeight', label: 'maxHeight', type: 'size', group: 'text', editors: ['dxTextArea'] },
// ── Sayı ─────────────────────────────────────────────────────────
{ path: 'min', label: 'min', type: 'number', group: 'number', editors: ['dxNumberBox'] },
{ path: 'max', label: 'max', type: 'number', group: 'number', editors: ['dxNumberBox'] },
{ path: 'step', label: 'step', type: 'number', group: 'number', editors: ['dxNumberBox'] },
{
path: 'showSpinButtons',
label: 'showSpinButtons',
type: 'boolean',
group: 'number',
editors: ['dxNumberBox'],
},
{
path: 'useLargeSpinButtons',
label: 'useLargeSpinButtons',
type: 'boolean',
group: 'number',
editors: ['dxNumberBox'],
},
{
path: 'format.type',
label: 'format.type',
type: 'select',
group: 'number',
editors: ['dxNumberBox'],
help: 'Sayısal format tipi. Seçilirse format alanı nesne olur.',
choices: [
{ value: 'fixedPoint', label: 'fixedPoint' },
{ value: 'decimal', label: 'decimal' },
{ value: 'currency', label: 'currency' },
{ value: 'percent', label: 'percent' },
{ value: 'exponential', label: 'exponential' },
{ value: 'largeNumber', label: 'largeNumber' },
{ value: 'thousands', label: 'thousands' },
{ value: 'millions', label: 'millions' },
{ value: 'billions', label: 'billions' },
],
},
{
path: 'format.precision',
label: 'format.precision',
type: 'number',
group: 'number',
editors: ['dxNumberBox'],
help: 'Ondalık basamak sayısı.',
},
{
path: 'format.currency',
label: 'format.currency',
type: 'text',
group: 'number',
editors: ['dxNumberBox'],
placeholder: 'TRY / USD / EUR',
},
{
path: 'invalidValueMessage',
label: 'invalidValueMessage',
type: 'text',
group: 'number',
editors: ['dxNumberBox'],
},
{
path: 'useMaskBehavior',
label: 'useMaskBehavior',
type: 'boolean',
group: 'number',
editors: ['dxNumberBox', 'dxDateBox'],
help: 'Girdiyi formatlı maske olarak yönetir. ' + booleanHelp,
},
// ── Tarih ────────────────────────────────────────────────────────
{
path: 'type',
label: 'type',
type: 'select',
group: 'date',
editors: DATE_EDITORS,
choices: [
{ value: 'date', label: 'date' },
{ value: 'datetime', label: 'datetime' },
{ value: 'time', label: 'time' },
],
},
{
path: 'pickerType',
label: 'pickerType',
type: 'select',
group: 'date',
editors: DATE_EDITORS,
choices: [
{ value: 'calendar', label: 'calendar' },
{ value: 'list', label: 'list' },
{ value: 'native', label: 'native' },
{ value: 'rollers', label: 'rollers' },
],
},
{
path: 'displayFormat',
label: 'displayFormat',
type: 'text',
group: 'date',
editors: DATE_EDITORS,
placeholder: 'dd/MM/yyyy',
help: 'Ekranda gösterilecek biçim. Listedeki sütun biçimini de ezer.',
},
{
path: 'dateSerializationFormat',
label: 'dateSerializationFormat',
type: 'text',
group: 'date',
editors: DATE_EDITORS,
placeholder: 'yyyy-MM-ddTHH:mm:ss',
help: 'Veritabanına yazılacak biçim. Tarih kaymalarının çoğu bu alandan kaynaklanır.',
},
{
path: 'interval',
label: 'interval',
type: 'number',
group: 'date',
editors: DATE_EDITORS,
help: 'Saat listesinde dakika aralığı.',
},
{
path: 'min',
label: 'min (tarih)',
type: 'text',
group: 'date',
editors: DATE_EDITORS,
placeholder: '2020-01-01',
},
{
path: 'max',
label: 'max (tarih)',
type: 'text',
group: 'date',
editors: DATE_EDITORS,
placeholder: '2030-12-31',
},
{
path: 'openOnFieldClick',
label: 'openOnFieldClick',
type: 'boolean',
group: 'date',
editors: DATE_EDITORS,
},
{
path: 'applyValueMode',
label: 'applyValueMode',
type: 'select',
group: 'date',
editors: DATE_EDITORS,
choices: [
{ value: 'instantly', label: 'instantly' },
{ value: 'useButtons', label: 'useButtons' },
],
},
{
path: 'invalidDateMessage',
label: 'invalidDateMessage',
type: 'text',
group: 'date',
editors: DATE_EDITORS,
},
{
path: 'calendarOptions.firstDayOfWeek',
label: 'calendarOptions.firstDayOfWeek',
type: 'number',
group: 'date',
editors: DATE_EDITORS,
help: '0 Pazar, 1 Pazartesi.',
},
{
path: 'calendarOptions.zoomLevel',
label: 'calendarOptions.zoomLevel',
type: 'select',
group: 'date',
editors: DATE_EDITORS,
choices: [
{ value: 'month', label: 'month' },
{ value: 'year', label: 'year' },
{ value: 'decade', label: 'decade' },
{ value: 'century', label: 'century' },
],
},
// ── Açılır liste / arama ─────────────────────────────────────────
{
path: 'searchEnabled',
label: 'searchEnabled',
type: 'boolean',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
platform: true,
help: 'Listede arama kutusu. TagBox için backend TagBoxOptions olarak da okur. ' + booleanHelp,
},
{
path: 'searchMode',
label: 'searchMode',
type: 'select',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
choices: [
{ value: 'contains', label: 'contains' },
{ value: 'startswith', label: 'startswith' },
],
},
{
path: 'searchExpr',
label: 'searchExpr',
type: 'text',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
placeholder: 'name',
help: 'Aramanın hangi alanda yapılacağı.',
},
{
path: 'searchTimeout',
label: 'searchTimeout',
type: 'number',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
},
{
path: 'minSearchLength',
label: 'minSearchLength',
type: 'number',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
},
{
path: 'showDataBeforeSearch',
label: 'showDataBeforeSearch',
type: 'boolean',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
},
{
path: 'acceptCustomValue',
label: 'acceptCustomValue',
type: 'boolean',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
platform: true,
help: 'Listede olmayan değerin yazılmasına izin verir. ' + booleanHelp,
},
{
path: 'noDataText',
label: 'noDataText',
type: 'text',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
placeholder: 'Kayıt bulunamadı',
},
{
path: 'deferRendering',
label: 'deferRendering',
type: 'boolean',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
help: 'Liste yalnızca açıldığında render edilir. ' + booleanHelp,
},
{
path: 'wrapItemText',
label: 'wrapItemText',
type: 'boolean',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
},
{
path: 'dropDownOptions.width',
label: 'dropDownOptions.width',
type: 'size',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
},
{
path: 'dropDownOptions.height',
label: 'dropDownOptions.height',
type: 'size',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
},
{
path: 'dropDownOptions.hideOnOutsideClick',
label: 'dropDownOptions.hideOnOutsideClick',
type: 'boolean',
group: 'dropdown',
editors: DROPDOWN_EDITORS,
},
// ── TagBox ───────────────────────────────────────────────────────
{
path: 'showSelectionControls',
label: 'showSelectionControls',
type: 'boolean',
group: 'tagBox',
editors: ['dxTagBox'],
platform: true,
help: 'Liste öğelerinde onay kutusu gösterir. Boş bırakılırsa platform varsayılanı true. ',
},
{
path: 'maxDisplayedTags',
label: 'maxDisplayedTags',
type: 'number',
group: 'tagBox',
editors: ['dxTagBox'],
platform: true,
},
{
path: 'showMultiTagOnly',
label: 'showMultiTagOnly',
type: 'boolean',
group: 'tagBox',
editors: ['dxTagBox'],
platform: true,
},
{
path: 'applyValueMode',
label: 'applyValueMode',
type: 'select',
group: 'tagBox',
editors: ['dxTagBox'],
platform: true,
help: 'Boş bırakılırsa platform varsayılanı useButtons.',
choices: [
{ value: 'instantly', label: 'instantly' },
{ value: 'useButtons', label: 'useButtons' },
],
},
{ path: 'multiline', label: 'multiline', type: 'boolean', group: 'tagBox', editors: ['dxTagBox'] },
{
path: 'hideSelectedItems',
label: 'hideSelectedItems',
type: 'boolean',
group: 'tagBox',
editors: ['dxTagBox'],
},
// ── GridBox (platform) ───────────────────────────────────────────
{
path: 'columns',
label: 'columns',
type: 'stringList',
group: 'gridBox',
editors: ['dxGridBox'],
platform: true,
placeholder: 'key, name, group',
help: 'Açılır tabloda gösterilecek sütunlar. Virgülle ayır; JSON\'a dizi olarak yazılır.',
},
{
path: 'selectionMode',
label: 'selectionMode',
type: 'select',
group: 'gridBox',
editors: ['dxGridBox'],
platform: true,
help: 'multiple seçilirse liste sütunu da çoklu değer olarak çalışır.',
choices: [
{ value: 'single', label: 'single' },
{ value: 'multiple', label: 'multiple' },
{ value: 'none', label: 'none' },
],
},
{
path: 'filterRowVisible',
label: 'filterRowVisible',
type: 'boolean',
group: 'gridBox',
editors: ['dxGridBox'],
platform: true,
help: 'Açılır tabloda filtre satırı. ' + booleanHelp,
},
// ── Görsel ───────────────────────────────────────────────────────
{
path: 'uploadUrl',
label: 'uploadUrl',
type: 'text',
group: 'image',
editors: IMAGE_EDITORS,
platform: true,
help: 'Boş bırakılırsa görsel base64 olarak saklanır.',
},
{
path: 'fileFieldName',
label: 'fileFieldName',
type: 'text',
group: 'image',
editors: IMAGE_EDITORS,
placeholder: 'file',
help: 'Yükleme isteğindeki form alan adı.',
},
{
path: 'accept',
label: 'accept',
type: 'text',
group: 'image',
editors: IMAGE_EDITORS,
platform: true,
placeholder: 'image/*',
},
{
path: 'multiple',
label: 'multiple',
type: 'boolean',
group: 'image',
editors: IMAGE_EDITORS,
platform: true,
help: 'Çoklu görsel. Metin olarak "true" yazılırsa da çalışır ama boolean tercih edilmeli.',
},
{
path: 'maxFileSize',
label: 'maxFileSize',
type: 'number',
group: 'image',
editors: IMAGE_EDITORS,
platform: true,
},
// ── Onay / seçim ─────────────────────────────────────────────────
{
path: 'text',
label: 'text',
type: 'text',
group: 'choice',
editors: ['dxCheckBox'],
help: 'Onay kutusunun yanında görünen metin.',
},
{ path: 'switchedOnText', label: 'switchedOnText', type: 'text', group: 'choice', editors: ['dxSwitch'] },
{ path: 'switchedOffText', label: 'switchedOffText', type: 'text', group: 'choice', editors: ['dxSwitch'] },
{
path: 'layout',
label: 'layout',
type: 'select',
group: 'choice',
editors: ['dxRadioGroup'],
choices: [
{ value: 'horizontal', label: 'horizontal' },
{ value: 'vertical', label: 'vertical' },
],
},
{
path: 'editAlphaChannel',
label: 'editAlphaChannel',
type: 'boolean',
group: 'choice',
editors: ['dxColorBox'],
},
{ path: 'keyStep', label: 'keyStep', type: 'number', group: 'choice', editors: ['dxColorBox'] },
// ── Kaydırıcı ────────────────────────────────────────────────────
{ path: 'min', label: 'min', type: 'number', group: 'slider', editors: SLIDER_EDITORS },
{ path: 'max', label: 'max', type: 'number', group: 'slider', editors: SLIDER_EDITORS },
{ path: 'step', label: 'step', type: 'number', group: 'slider', editors: SLIDER_EDITORS },
{
path: 'tooltip.enabled',
label: 'tooltip.enabled',
type: 'boolean',
group: 'slider',
editors: SLIDER_EDITORS,
},
{
path: 'tooltip.showMode',
label: 'tooltip.showMode',
type: 'select',
group: 'slider',
editors: SLIDER_EDITORS,
choices: [
{ value: 'onHover', label: 'onHover' },
{ value: 'always', label: 'always' },
],
},
{ path: 'showRange', label: 'showRange', type: 'boolean', group: 'slider', editors: SLIDER_EDITORS },
// ── HTML editör ──────────────────────────────────────────────────
{
path: 'valueType',
label: 'valueType',
type: 'select',
group: 'html',
editors: ['dxHtmlEditor'],
choices: [
{ value: 'html', label: 'html' },
{ value: 'markdown', label: 'markdown' },
],
},
{
path: 'toolbar.multiline',
label: 'toolbar.multiline',
type: 'boolean',
group: 'html',
editors: ['dxHtmlEditor'],
},
{
path: 'toolbar.items',
label: 'toolbar.items',
type: 'json',
group: 'html',
editors: ['dxHtmlEditor'],
help: 'Araç çubuğu tanımı. Hazır ayarlardaki "htmlEditor toolbar" bunu doldurur.',
},
{
path: 'mediaResizing.enabled',
label: 'mediaResizing.enabled',
type: 'boolean',
group: 'html',
editors: ['dxHtmlEditor'],
},
{
path: 'imageUpload.fileUploadMode',
label: 'imageUpload.fileUploadMode',
type: 'select',
group: 'html',
editors: ['dxHtmlEditor'],
choices: [
{ value: 'base64', label: 'base64' },
{ value: 'server', label: 'server' },
{ value: 'both', label: 'both' },
],
},
{
path: 'imageUpload.uploadUrl',
label: 'imageUpload.uploadUrl',
type: 'text',
group: 'html',
editors: ['dxHtmlEditor'],
},
// ── Liste sütununu da etkileyenler ───────────────────────────────
{
path: 'format',
label: 'format (metin)',
type: 'text',
group: 'grid',
placeholder: 'dd/MM/yyyy veya currency',
help: 'Metin olarak yazılırsa listedeki sütun biçimini de ezer. Sayı formatı için Sayı bölümündeki format.type alanını kullan.',
},
{
path: 'encodeHtml',
label: 'encodeHtml',
type: 'boolean',
group: 'grid',
help: 'false yapılırsa hücre içeriği HTML olarak render edilir. ' + booleanHelp,
},
{
path: 'buttons',
label: 'buttons',
type: 'json',
group: 'grid',
help: 'Editör içi butonlar. options.onClick metin olarak yazılırsa çalışma anında fonksiyona çevrilir.',
},
]
/** Aynı yolu paylaşan spec'lerden aktif editöre en uygun olanı seçer. */
export const resolveSpecs = (editorType?: string): OptionSpec[] => {
const matches = (spec: OptionSpec) =>
!spec.editors || (!!editorType && spec.editors.includes(editorType))
const chosen = new Map<string, OptionSpec>()
optionSpecs.forEach((spec) => {
const current = chosen.get(spec.path)
if (!current) {
chosen.set(spec.path, spec)
return
}
// Editöre özel tanım, genel tanımı ezer.
if (matches(spec) && !matches(current)) {
chosen.set(spec.path, spec)
}
})
return [...chosen.values()]
}
export const isSpecRelevant = (spec: OptionSpec, editorType?: string) =>
!spec.editors || !editorType || spec.editors.includes(editorType)

View file

@ -0,0 +1,267 @@
/**
* Hazır editorOptions kalıpları. Her kalıp mevcut JSON ile birleştirilir,
* diğer ayarları silmez.
*/
import type { JsonObject } from './jsonUtils'
export type EditorOptionsPreset = {
key: string
label: string
description: string
/** Yalnızca bu editör tiplerinde önerilir. Boşsa her zaman gösterilir. */
editors?: string[]
value: JsonObject
}
const htmlToolbarItems = [
'undo',
'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[] = [
{
key: 'readOnly',
label: 'Salt okunur',
description: 'readOnly: true — alan görünür ama değiştirilemez.',
value: { readOnly: true },
},
{
key: 'disabled',
label: 'Pasif',
description: 'disabled: true — alan tamamen pasifleşir.',
value: { disabled: true },
},
{
key: 'rightAligned',
label: 'Sağa yaslı',
description: 'Girdi metnini sağa yaslar. Tutar/miktar alanları için.',
value: { inputAttr: { style: 'text-align: right' } },
},
{
key: 'fixedPoint2',
label: 'Ondalık (2 hane)',
description: 'format.type fixedPoint, precision 2.',
editors: ['dxNumberBox'],
value: { format: { type: 'fixedPoint', precision: 2 } },
},
{
key: 'numberSpin2',
label: 'Ondalık + spin',
description: 'precision 2, mask davranışı ve artır/azalt butonları.',
editors: ['dxNumberBox'],
value: {
format: { type: 'fixedPoint', precision: 2 },
useMaskBehavior: true,
showSpinButtons: true,
},
},
{
key: 'currency',
label: 'Para birimi (TRY)',
description: 'currency formatı, 2 ondalık.',
editors: ['dxNumberBox'],
value: { format: { type: 'currency', precision: 2, currency: 'TRY' } },
},
{
key: 'date',
label: 'Tarih dd/MM/yyyy',
description: 'Görünüm ve veritabanı biçimini birlikte ayarlar.',
editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'],
value: {
type: 'date',
displayFormat: 'dd/MM/yyyy',
dateSerializationFormat: 'yyyy-MM-dd',
},
},
{
key: 'dateTime',
label: 'Tarih + saat',
description: 'dd/MM/yyyy HH:mm görünümü, ISO serileştirme.',
editors: ['dxDateBox', 'dxCalendar', 'dxDateRangeBox'],
value: {
type: 'datetime',
displayFormat: 'dd/MM/yyyy HH:mm',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
},
},
{
key: 'timePicker',
label: 'Saat seçici',
description: '5 dakika aralıklı liste tipi saat seçimi.',
editors: ['dxDateBox'],
value: {
type: 'time',
pickerType: 'list',
displayFormat: 'HH:mm',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
interval: 5,
width: '100%',
},
},
{
key: 'phoneMask',
label: 'Telefon maskesi',
description: 'Maske, yer tutucu ve hata mesajı.',
editors: ['dxTextBox', 'dxAutocomplete'],
value: {
mask: '(000) 000-0000',
maskInvalidMessage: 'Lütfen geçerli bir telefon numarası girin',
useMaskedValue: false,
maskRules: { X: '[0-9]' },
placeholder: '(555) 123-4567',
},
},
{
key: 'textAreaAuto',
label: 'Otomatik büyüyen alan',
description: 'autoResizeEnabled ve minimum yükseklik.',
editors: ['dxTextArea'],
value: { autoResizeEnabled: true, minHeight: 80, maxHeight: 320 },
},
{
key: 'searchableList',
label: 'Aranabilir liste',
description: 'İçinde geçen kayıtları arar, 300 ms gecikme.',
editors: ['dxSelectBox', 'dxLookup', 'dxTagBox', 'dxDropDownBox', 'dxGridBox', 'dxAutocomplete'],
value: { searchEnabled: true, searchMode: 'contains', searchTimeout: 300, showClearButton: true },
},
{
key: 'tagBoxInstant',
label: 'TagBox anlık seçim',
description: 'Onay kutulu, butonsuz, anında uygulanan çoklu seçim.',
editors: ['dxTagBox'],
value: {
showSelectionControls: true,
applyValueMode: 'instantly',
searchEnabled: true,
showClearButton: true,
},
},
{
key: 'gridBoxSingle',
label: 'GridBox tekli seçim',
description: 'key/name sütunları, filtre satırıık, tek seçim.',
editors: ['dxGridBox'],
value: {
columns: ['key', 'name'],
selectionMode: 'single',
filterRowVisible: true,
height: 320,
width: 520,
showClearButton: true,
},
},
{
key: 'gridBoxMulti',
label: 'GridBox çoklu seçim',
description: 'Çoklu seçim; liste sütunu da çoklu değer olarak çalışır.',
editors: ['dxGridBox'],
value: {
columns: ['key', 'name'],
selectionMode: 'multiple',
filterRowVisible: true,
height: 360,
width: 560,
},
},
{
key: 'imageMulti',
label: 'Çoklu görsel 80x80',
description: 'Çoklu yükleme ve 80x80 küçük resim.',
editors: ['dxImageUpload', 'dxImageViewer'],
value: { width: 80, height: 80, multiple: true, accept: 'image/*' },
},
{
key: 'htmlToolbar',
label: 'HtmlEditor araç çubuğu',
description: 'Tam araç çubuğu, görsel yükleme ve boyutlandırma.',
editors: ['dxHtmlEditor'],
value: buildHtmlEditorOptions(),
},
{
key: 'height100',
label: 'Yükseklik 100',
description: 'height: 100',
value: { height: 100 },
},
{
key: 'height200',
label: 'Yükseklik 200',
description: 'height: 200',
value: { height: 200 },
},
]

View file

@ -801,7 +801,9 @@ const CardView = (props: CardViewProps) => {
}
}
if (column.placeHolder) {
// Grid ve Form'da editorOptions.placeholder kolonun PlaceHolder alanını ezer;
// burada tersi olduğu için aynı yapılandırma CardView'da farklı davranıyordu.
if (column.placeHolder && editorOptions.placeholder === undefined) {
editorOptions.placeholder = translate('::' + column.placeHolder)
}

View file

@ -32,6 +32,7 @@ import { RowMode } from '../form/types'
import { GridColumnData } from './GridColumnData'
import GridFilterDialogs from './GridFilterDialogs'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { useFilters } from './useFilters'
@ -635,6 +636,7 @@ const Tree = (props: TreeProps) => {
/>
<Template name="cellEditTagBox" render={TagBoxEditorComponent} />
<Template name="cellEditGridBox" render={GridBoxEditorComponent} />
<Template name="cellEditImageUpload" render={ImageUploadEditorComponent} />
<Template
name="cellEditImageViewer"
render={(data: any) => (

View file

@ -697,7 +697,11 @@ const useListFormColumns = ({
// #region lookup ayarlari
if (colData.lookupDto?.dataSourceType) {
// UiColumnEditorTemplateTypeEnum : None:0, Table:1, TagBox:2
const formItem = colData.editOrderNo != null ? colData : undefined
// NOT: Buradaki seçim yalnızca editorType2'ye bakar. Önceden ek olarak
// `editOrderNo != null` şartı vardı; düzenleme formunda yer almayan
// kolonlarda TagBox/GridBox seçenekleri sessizce yok sayılıyor ve
// hücre içi düzenleme çalışmıyordu.
const formItem = colData
if (formItem?.editorType2 === PlatformEditorTypes.dxTagBox) {
column.extras = {
multiValue: true,
@ -746,7 +750,9 @@ const useListFormColumns = ({
// #region image upload editor
if (!colData.lookupDto?.dataSourceType) {
const imageFormItem = colData.editOrderNo != null ? colData : undefined
// Görsel kolonları da düzenleme formunda olmasa bile küçük resim
// gösterebilmeli; bu yüzden editOrderNo şartı kaldırıldı.
const imageFormItem = colData
const isImageUploadEditor =
imageFormItem?.editorType2 === PlatformEditorTypes.dxImageUpload
const isImageViewerEditor =