Claude güncellemesi Editor Options Builder Modal güncellemeleri
This commit is contained in:
parent
38e3ef5797
commit
a1f5e5e539
9 changed files with 2233 additions and 950 deletions
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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) : ''
|
||||
|
|
@ -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)
|
||||
|
|
@ -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ı açı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 },
|
||||
},
|
||||
]
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
Loading…
Reference in a new issue