Grid, Tree, FormDevexpress komponentlerin yetenekleri eşitlendi

This commit is contained in:
Sedat ÖZTÜRK 2026-10-07 21:08:10 +03:00
parent 7e5322d831
commit 3922c46539
30 changed files with 784 additions and 804 deletions

View file

@ -1490,9 +1490,28 @@ cascade eksik yetenek değildir ve analizde `Ready to Start`'ı engellemez. Tam
| `CascadeFilterOperator` | `StaticData` cascade filtresinin operatörü; varsayılan `=` | | `CascadeFilterOperator` | `StaticData` cascade filtresinin operatörü; varsayılan `=` |
| `CascadeEmptyFields` | **Ebeveyn alanda** doldurulur; bu alan değişince boşaltılacak çocuk alanlar, virgülle. Ara halka (İl) hem çocuk hem ebeveyn olduğu için iki tarafı birden taşır | | `CascadeEmptyFields` | **Ebeveyn alanda** doldurulur; bu alan değişince boşaltılacak çocuk alanlar, virgülle. Ara halka (İl) hem çocuk hem ebeveyn olduğu için iki tarafı birden taşır |
Cascade liste popup'ında ve form sayfasında (`FormDevExpress`) aynı davranır. Form sayfasında çocuk Cascade liste popup'ında (Grid, Tree), satır içi düzenlemede ve form sayfasında (`FormDevExpress`)
aynı kuralla çalışır; kural tek yerdedir (`list/shared/cascade.ts`, `getCascadeClearFields`): bir alan
değişince ebeveyni o alan olan çocuklar **ve** alanın `CascadeEmptyFields` listesi boşaltılır. Kilit
ve temizleme platform editörlerinde (TagBox, GridBox, ImageUpload) de uygulanır. Form sayfasında çocuk
yalnızca ebeveynin değeri **gerçekten değiştiğinde** temizlenir ve listesi yeni ebeveyn değeriyle yalnızca ebeveynin değeri **gerçekten değiştiğinde** temizlenir ve listesi yeni ebeveyn değeriyle
yeniden kurulur; formun aynı değeri tekrar bildirmesi (veri forma geri verilince) yok sayılır. yeniden kurulur; formun aynı değeri tekrar bildirmesi (veri forma geri verilince) yok sayılır.
**Liste popup'ı ↔ form sayfası editör eşliği.** İkisi aynı ListForm tanımından beslendiği için
editör yetenekleri ortak koddan gelir; bir tarafa yeni yetenek ortak yardımcıya eklenerek verilir:
| Yetenek | Ortak kaynak |
| --- | --- |
| `EditorOptions` varsayılanları (tarih maskesi/biçimi, HtmlEditor, telefon) | `withEditorTypeDefaults` (`list/shared/editingForm.ts`) |
| Lookup varsayılanları (`valueExpr`, `displayExpr`, `searchEnabled`) | `getLookupEditorDefaults` |
| Buton `onClick` (fonksiyon metni, `@SCAN_BARCODE`) ve bağlamı | `resolveEditorButtons` + `withButtonContext` |
| Cascade temizleme/kilit | `list/shared/cascade.ts` |
| Grid/Tree `onEditorPreparing` gövdesi | `prepareEditingEditor` (`list/shared/editingFormScripts.ts`) |
| TagBox, GridBox, ImageUpload, ImageViewer | Tek uygulama `form/editors/*`; liste hücresi `list/editors/cellEditors.tsx` adaptörüyle bağlanır |
DataGrid/TreeList platform editörlerini `editCellTemplate` ile çizer ve `onEditorPreparing` onlar için
çalışmaz; salt-okunur/devre dışı kararı, cascade ve `EditorScript` bu editörlere
`createCustomEditorCellAdapter` ile verilir.
Adres zincirinin hazır örneği Kiracılar ekranıdır: Ülke › İl › İlçe › Mahalle; mahalle lookup'ı Adres zincirinin hazır örneği Kiracılar ekranıdır: Ülke › İl › İlçe › Mahalle; mahalle lookup'ı
`PostalCode`'u `group` olarak döndürür ve `copy("Group", "PostalCode")` tarifi posta kodunu doldurur. `PostalCode`'u `group` olarak döndürür ve `copy("Group", "PostalCode")` tarifi posta kodunu doldurur.
@ -2842,7 +2861,7 @@ Preset mevcut JSON ile **birleşir**, diğer ayarları silmez; aynı anahtar var
| Çoklu görsel | `width`, `height`, `multiple`, `accept:image/*` | | Çoklu görsel | `width`, `height`, `multiple`, `accept:image/*` |
| HTML tam araç çubuğu | `htmlEditorOptions.ts` varsayılanı: `toolbar.items` (tam liste), `toolbar.multiline`, `mediaResizing`, `imageUpload` (`tabs: file/url`, base64), `tableResizing`, `tableContextMenu` — boş `editorOptions` da aynı varsayılanı alır | | HTML tam araç çubuğu | `htmlEditorOptions.ts` varsayılanı: `toolbar.items` (tam liste), `toolbar.multiline`, `mediaResizing`, `imageUpload` (`tabs: file/url`, base64), `tableResizing`, `tableContextMenu` — boş `editorOptions` da aynı varsayılanı alır |
| Yükseklik 100 / 200 | `{"height":100}` · `{"height":200}` | | Yükseklik 100 / 200 | `{"height":100}` · `{"height":200}` |
| Barkod okutma butonu | `buttons[]` — kamera ikonlu buton; `onClick` = `"@SCAN_BARCODE"` token'ı. Yalnız `dxTextBox`. Token'ı `editingForm.ts` kamera diyaloğuna bağlar, okunan kod alana yazılır. HTTPS şart. | | Barkod okutma butonu | `buttons[]` — kamera ikonlu buton; `onClick` = `"@SCAN_BARCODE"` token'ı. Yalnız `dxTextBox`. Token'ı `resolveEditorButtons` (`editingForm.ts`) kamera diyaloğuna bağlar, okunan kod alana yazılır; liste popup'ı ve form sayfası aynı. HTTPS şart. Fonksiyon metni yazılan `onClick` olayla birlikte `formData`, `fieldName`, `mode` alır (popup'ta ek olarak `rowKey`, `rowIndex`). |
| Kamerayla foto çek | `{"accept":"image/*","capture":"environment"}` — yalnız `dxImageUpload`. Mobilde dosya seçici yerine **yalnızca** kamera açılır (`user` = ön kamera); masaüstünde yok sayılır. Ayarlanmazsa dokunmatik cihazda dosya seçicinin yanına ayrı bir kamera butonu gelir, yani ikisi birden kullanılabilir — bu kalıp kamerayı **zorunlu** kılmak içindir. | | Kamerayla foto çek | `{"accept":"image/*","capture":"environment"}` — yalnız `dxImageUpload`. Mobilde dosya seçici yerine **yalnızca** kamera açılır (`user` = ön kamera); masaüstünde yok sayılır. Ayarlanmazsa dokunmatik cihazda dosya seçicinin yanına ayrı bir kamera butonu gelir, yani ikisi birden kullanılabilir — bu kalıp kamerayı **zorunlu** kılmak içindir. |
#### 6.4.1 Telefon biçimleri (`format` token'ı) #### 6.4.1 Telefon biçimleri (`format` token'ı)
@ -2867,7 +2886,7 @@ varsayılanı ezer.
- Ülke alanı olan her ekranda yeni kayıt Türkiye ile açılır: `FormFieldsDefaultValueJson` → - Ülke alanı olan her ekranda yeni kayıt Türkiye ile açılır: `FormFieldsDefaultValueJson` →
`{ FieldName: "Country", FieldDbType: String, Value: "Türkiye", CustomValueType: Value }`. `{ FieldName: "Country", FieldDbType: String, Value: "Türkiye", CustomValueType: Value }`.
- Ülke alanı değişince maske yenilenir: Card/Scheduler/Form sayfasında formun `fieldDataChanged` olayıyla, - Ülke alanı değişince maske yenilenir: Card/Scheduler/Form sayfasında formun `fieldDataChanged` olayıyla,
Grid/Tree'de `onEditorPreparing`'deki `attachFieldValueBroadcast` olayıyla (DataGrid formu `fieldDataChanged` Grid/Tree'de `onEditorPreparing`'deki (`prepareEditingEditor`) `attachFieldValueBroadcast` olayıyla (DataGrid formu `fieldDataChanged`
yayınlamaz; `editorOptions.onValueChanged` da kullanılamaz — editör fabrikası onu kendi değer yazan yayınlamaz; `editorOptions.onValueChanged` da kullanılamaz — editör fabrikası onu kendi değer yazan
işleyicisinin üstüne birleştirir). Satır içi düzenlemede (form yok) maske satırın ülkesiyle açılır, değişikliği izlemez. işleyicisinin üstüne birleştirir). Satır içi düzenlemede (form yok) maske satırın ülkesiyle açılır, değişikliği izlemez.
- `phoneCountry` ekranında sabit uzunluk `pattern`'ı (`^[0-9]{10}$`) yazılmaz; uzunluğu maske denetler, - `phoneCountry` ekranında sabit uzunluk `pattern`'ı (`^[0-9]{10}$`) yazılmaz; uzunluğu maske denetler,

View file

@ -446,9 +446,13 @@ ve atanan kullanıcı alanlarından kurulur.
`dxDropDownBox`, `dxGridBox`, `dxAutocomplete`, `dxColorBox`, `dxHtmlEditor`, `dxSlider`, `dxDropDownBox`, `dxGridBox`, `dxAutocomplete`, `dxColorBox`, `dxHtmlEditor`, `dxSlider`,
`dxRangeSlider` — ayrıca görsel yükleme/görüntüleme editörleri. `dxRangeSlider` — ayrıca görsel yükleme/görüntüleme editörleri.
- **Lookup kaynakları:** `StaticData`, `Query`, `WebService`; ebeveyn–çocuk (cascade) desteklidir. - **Lookup kaynakları:** `StaticData`, `Query`, `WebService`; ebeveyn–çocuk (cascade) desteklidir.
Cascade liste popup'ında ve form sayfasında (`/admin/form/...`, `FormDevExpress`) aynı çalışır. Cascade liste popup'ında, satır içi düzenlemede ve form sayfasında (`/admin/form/...`, `FormDevExpress`)
aynı kuralla çalışır: alan değişince ebeveyni o alan olan çocuklar ve alanın `CascadeEmptyFields`
listesi boşaltılır.
Lookup'lı seçim kutusunda yazarak arama ikisinde de varsayılan açıktır; alanın `EditorOptions`'ında Lookup'lı seçim kutusunda yazarak arama ikisinde de varsayılan açıktır; alanın `EditorOptions`'ında
`{"searchEnabled": false}` kapatır. `{"searchEnabled": false}` kapatır.
- **Popup ↔ form sayfası eşliği:** `EditorOptions` varsayılanları, butonlar, `EditorScript`, cascade ve
platform editörleri (TagBox, GridBox, görsel) iki yerde ortak koddan gelir; tablo `lowcode.instructions.md` §4.3.4.
- **Doğrulama:** `ValidationRuleJson` ile required/range/pattern/custom kuralları. Kurallar liste popup'ında ve form sayfasında (`/admin/form/...`) aynı uygulanır; maske (`mask`) `EditorOptions` ile verilir. - **Doğrulama:** `ValidationRuleJson` ile required/range/pattern/custom kuralları. Kurallar liste popup'ında ve form sayfasında (`/admin/form/...`) aynı uygulanır; maske (`mask`) `EditorOptions` ile verilir.
- **Editör script:** `EditorScript` alanı ile alan değişiminde çalışan JS (örn. tarih farkından - **Editör script:** `EditorScript` alanı ile alan değişiminde çalışan JS (örn. tarih farkından
gün hesaplama, miktar × birim fiyat toplamı). Çalıştırma `utils/editorScriptRuntime.ts` üzerinden yapılır. gün hesaplama, miktar × birim fiyat toplamı). Çalıştırma `utils/editorScriptRuntime.ts` üzerinden yapılır.

View file

@ -231,6 +231,11 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
- **`phoneFormat.ts` ↔ `DynamicReportPhoneFormatter.cs`** — telefon token'larının (`phoneGlobal`, `phoneTR`, - **`phoneFormat.ts` ↔ `DynamicReportPhoneFormatter.cs`** — telefon token'larının (`phoneGlobal`, `phoneTR`,
`phoneIntl`, `phoneCountry`) görüntü kuralı iki yerde yazılır: liste/kart istemcide, dinamik rapor sunucuda. `phoneIntl`, `phoneCountry`) görüntü kuralı iki yerde yazılır: liste/kart istemcide, dinamik rapor sunucuda.
Biri değişirse diğeri de değişir; yoksa liste ile rapor aynı numarayı farklı yazar. Biri değişirse diğeri de değişir; yoksa liste ile rapor aynı numarayı farklı yazar.
- **Liste popup'ı (Grid/Tree) ↔ form sayfası (`FormDevExpress`)** — editör yetenekleri ortak yardımcılardadır
(`list/shared/editingForm.ts`, `editingFormScripts.ts`, `cascade.ts`); TagBox/GridBox/ImageUpload/ImageViewer'ın
tek uygulaması `form/editors/*`, liste hücresi yalnızca `list/editors/cellEditors.tsx` adaptörüdür. Bir tarafa
yetenek eklenirken ortak yardımcıya eklenir; tek tarafa yazılan yetenek eşliği bozar. Tablo:
`lowcode.instructions.md` §4.3.4.
- **`htmlEditorOptions.ts` ↔ `EditorOptions.Html`** — HtmlEditor varsayılanı iki yerde yazılır ve - **`htmlEditorOptions.ts` ↔ `EditorOptions.Html`** — HtmlEditor varsayılanı iki yerde yazılır ve
bloklar birebir aynıdır. İstemci iç içe blokları tek seviye birleştirdiği için seed edilen bloklar birebir aynıdır. İstemci iç içe blokları tek seviye birleştirdiği için seed edilen
`toolbar` varsayılanın yerine geçer; biri değişip diğeri değişmezse seed'li ekranlar eski araç `toolbar` varsayılanın yerine geçer; biri değişip diğeri değişmezse seed'li ekranlar eski araç

File diff suppressed because one or more lines are too long

View file

@ -19,7 +19,7 @@ import FormFieldOrderPanel from './FormFieldOrderPanel'
import type { FormFieldOrder } from './shared/formItems' import type { FormFieldOrder } from './shared/formItems'
/** Form sayfasının kullanıcı bazlı alan sırası (`useGridData`). */ /** Form sayfasının kullanıcı bazlı alan sırası (`useGridData`). */
export type FormFieldOrderControl = { type FormFieldOrderControl = {
value?: FormFieldOrder value?: FormFieldOrder
preview: (order: FormFieldOrder | undefined) => void preview: (order: FormFieldOrder | undefined) => void
save: (order: FormFieldOrder | undefined) => Promise<void> save: (order: FormFieldOrder | undefined) => Promise<void>

View file

@ -23,7 +23,9 @@ import {
setFormEditorReadOnly, setFormEditorReadOnly,
shouldRunEditorScriptOnContentReady, shouldRunEditorScriptOnContentReady,
translateCaption, translateCaption,
withButtonContext,
} from '../list/shared/editingForm' } from '../list/shared/editingForm'
import { getCascadeClearFields, isCascadeParentEmpty } from '../list/shared/cascade'
import { isTouchLikeDevice } from '../list/shared/editPopup' import { isTouchLikeDevice } from '../list/shared/editPopup'
import { flattenFormGroups, getCascadeParentFields } from './shared/formItems' import { flattenFormGroups, getCascadeParentFields } from './shared/formItems'
@ -164,11 +166,27 @@ const FormDevExpress = (props: {
} }
} }
/**
* Alan değeri değişince cascade kuralının boşalttığı alanları uygular: ebeveyni bu alan olan
* çocuklar ve alanın `CascadeEmptyFields` listesi (liste popup'ıyla aynı kural).
*/
const withCascadeCleared = (data: Record<string, any>, dataField: string) => {
const columns = flattenFormGroups(formItemsRef.current).map((item) => item.colData)
getCascadeClearFields(columns, dataField).forEach((field) => {
data[field] = null
})
return data
}
/** Özel editörlerin ortak değer değişimi akışı. */ /** Özel editörlerin ortak değer değişimi akışı. */
const handleCustomEditorChange = (formItem: SimpleItemWithColData, value: any) => { const handleCustomEditorChange = (formItem: SimpleItemWithColData, value: any) => {
if (!formItem.dataField) return if (!formItem.dataField) return
if (isSameValue(formDataRef.current?.[formItem.dataField], value)) return if (isSameValue(formDataRef.current?.[formItem.dataField], value)) return
formDataRef.current = { ...formDataRef.current, [formItem.dataField]: value } formDataRef.current = withCascadeCleared(
{ ...formDataRef.current, [formItem.dataField]: value },
formItem.dataField,
)
formDataSnapshotRef.current = { ...formDataRef.current }
setFormData(formDataRef.current) setFormData(formDataRef.current)
runEditorScript(formItem, value) runEditorScript(formItem, value)
} }
@ -182,9 +200,6 @@ const FormDevExpress = (props: {
...(index !== undefined && mode !== 'view' && !isTouchLikeDevice() ...(index !== undefined && mode !== 'view' && !isTouchLikeDevice()
? { autoFocus: index === 1 } ? { autoFocus: index === 1 }
: {}), : {}),
...(formItem.editorType === 'dxDateBox'
? { useMaskBehavior: true, openOnFieldClick: true, showClearButton: true }
: {}),
...(formItem.colData?.placeHolder ...(formItem.colData?.placeHolder
? { placeholder: translate('::' + formItem.colData.placeHolder) } ? { placeholder: translate('::' + formItem.colData.placeHolder) }
: {}), : {}),
@ -202,27 +217,14 @@ const FormDevExpress = (props: {
}, },
} }
: {}), : {}),
// onClick metni item üretilirken çözülür (`resolveEditorButtons`); burada kayıt bağlamı eklenir.
...(configuredButtons?.length ...(configuredButtons?.length
? { ? {
buttons: configuredButtons.map((button: any) => { buttons: withButtonContext(configuredButtons, () => ({
// Yapılandırmadan string olarak gelen onClick bir kez fonksiyona çevrilir.
if (typeof button?.options?.onClick !== 'string') return button
const originalClick = eval(`(${button.options.onClick})`)
return {
...button,
options: {
...button.options,
onClick: (e: any) =>
originalClick({
...e,
formData: formDataRef.current, formData: formDataRef.current,
fieldName: formItem.dataField, fieldName: formItem.dataField,
mode, mode,
}), })),
},
}
}),
} }
: {}), : {}),
} }
@ -266,7 +268,7 @@ const FormDevExpress = (props: {
editor.option( editor.option(
'disabled', 'disabled',
item.editorOptions?.disabled === true || item.editorOptions?.disabled === true ||
parentFields.some((parentField) => !formDataRef.current?.[parentField]), isCascadeParentEmpty(parentFields, formDataRef.current),
) )
} catch (err) { } catch (err) {
console.debug('Cascade disabled update skipped for', item.dataField, err) console.debug('Cascade disabled update skipped for', item.dataField, err)
@ -350,7 +352,15 @@ const FormDevExpress = (props: {
/** Platform editörünün render fonksiyonunu döner. */ /** Platform editörünün render fonksiyonunu döner. */
const renderCustomEditor = (formItem: SimpleItemWithColData): ReactNode => { const renderCustomEditor = (formItem: SimpleItemWithColData): ReactNode => {
const value = formData[formItem.dataField!] const value = formData[formItem.dataField!]
const editorOptions = getEditorOptions(formItem) const parentFields = getCascadeParentFields(formItem)
const editorOptions = {
...getEditorOptions(formItem),
// DevExtreme editörlerinde updateCascadeDisabledStates yapar; platform editörleri
// form.getEditor ile bulunamadığı için kilit burada verilir.
...(mode !== 'view' && parentFields.length && isCascadeParentEmpty(parentFields, formData)
? { disabled: true }
: {}),
}
const onChange = (next: any) => handleCustomEditorChange(formItem, next) const onChange = (next: any) => handleCustomEditorChange(formItem, next)
switch (formItem.editorType2) { switch (formItem.editorType2) {
@ -359,7 +369,6 @@ const FormDevExpress = (props: {
<TagBoxEditorComponent <TagBoxEditorComponent
value={value || EMPTY_VALUES} value={value || EMPTY_VALUES}
setDefaultValue={false} setDefaultValue={false}
values={formData}
options={formItem.tagBoxOptions} options={formItem.tagBoxOptions}
col={formItem.colData} col={formItem.colData}
onValueChanged={onChange} onValueChanged={onChange}
@ -370,7 +379,6 @@ const FormDevExpress = (props: {
return ( return (
<GridBoxEditorComponent <GridBoxEditorComponent
value={value || EMPTY_VALUES} value={value || EMPTY_VALUES}
values={formData}
options={formItem.gridBoxOptions} options={formItem.gridBoxOptions}
col={formItem.colData} col={formItem.colData}
onValueChanged={onChange} onValueChanged={onChange}
@ -477,14 +485,8 @@ const FormDevExpress = (props: {
// Aynı değer tekrar geldiğinde (formData forma geri verilince) state yenilenmez; aksi hâlde // Aynı değer tekrar geldiğinde (formData forma geri verilince) state yenilenmez; aksi hâlde
// her yenileme yeni bir olay doğurur ve döngüye girer. // her yenileme yeni bir olay doğurur ve döngüye girer.
if (!isSameValue(formDataSnapshotRef.current?.[e.dataField], e.value)) { if (!isSameValue(formDataSnapshotRef.current?.[e.dataField], e.value)) {
const newFormData = { ...formData, [e.dataField]: e.value }
// Parent alan değiştiğinde bağımlı (cascade) alanları temizle. // Parent alan değiştiğinde bağımlı (cascade) alanları temizle.
flattenFormGroups(formItemsRef.current) const newFormData = withCascadeCleared({ ...formData, [e.dataField]: e.value }, e.dataField)
.filter((item) => getCascadeParentFields(item).includes(e.dataField!))
.forEach((child) => {
newFormData[child.dataField!] = null
})
formDataSnapshotRef.current = { ...newFormData } formDataSnapshotRef.current = { ...newFormData }
formDataRef.current = newFormData formDataRef.current = newFormData

View file

@ -2,6 +2,7 @@ import { ColumnFormatDto, GridBoxOptionsDto } from '@/proxy/form/models'
import { ReactElement } from 'react' import { ReactElement } from 'react'
import { SharedGridBoxEditor } from '@/views/shared/SharedGridBoxEditor' import { SharedGridBoxEditor } from '@/views/shared/SharedGridBoxEditor'
/** Form sayfası ve liste düzenleme hücresinin ortak GridBox editörü. */
const GridBoxEditorComponent = ({ const GridBoxEditorComponent = ({
value, value,
options, options,
@ -11,7 +12,6 @@ const GridBoxEditorComponent = ({
}: { }: {
value: any value: any
options?: GridBoxOptionsDto options?: GridBoxOptionsDto
values: any // all row data
onValueChanged: (e: any) => void onValueChanged: (e: any) => void
col?: ColumnFormatDto col?: ColumnFormatDto
editorOptions: any editorOptions: any
@ -22,9 +22,13 @@ const GridBoxEditorComponent = ({
<SharedGridBoxEditor <SharedGridBoxEditor
value={value} value={value}
dataSource={editorOptions?.dataSource} dataSource={editorOptions?.dataSource}
lookup={lookupDto} lookup={{
valueExpr: lookupDto?.valueExpr?.toLowerCase(),
displayExpr: lookupDto?.displayExpr?.toLowerCase(),
}}
options={options} options={options}
editorOptions={editorOptions} editorOptions={editorOptions}
dropDownOptions={{ width: options?.width, hideOnOutsideClick: true }}
onValueChanged={onValueChanged} onValueChanged={onValueChanged}
/> />
) )

View file

@ -13,6 +13,51 @@ import { normalizeImageSize } from '../../list/shared/imageValue'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import { supportsPhotoCapture } from '@/utils/device' import { supportsPhotoCapture } from '@/utils/device'
/**
* Yüklenen değerin ham biçimini korur; ayraçlara göre bölmez.
* (Görüntüleme tarafındaki `normalizeImageValue` ile bilinçli olarak farklıdır:
* burada kullanıcı tarafından yazılmış virgüllü URL'ler bozulmamalıdır.)
*/
const normalizeUploadValue = (value: unknown): string[] => {
if (!value) return []
if (Array.isArray(value)) return value.flatMap(normalizeUploadValue)
if (typeof value === 'string') {
const trimmed = value.trim()
if (!trimmed) return []
if (trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed)
if (Array.isArray(parsed)) return parsed.flatMap(normalizeUploadValue)
} catch {
return [trimmed]
}
}
return [trimmed]
}
return [String(value)].filter(Boolean)
}
const resolveUploadedUrl = (data: unknown) => {
if (typeof data === 'string') return data
if (!data || typeof data !== 'object') return ''
const record = data as Record<string, unknown>
return String(record.url ?? record.fileUrl ?? record.path ?? '').trim()
}
const toBase64 = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = reject
reader.readAsDataURL(file)
})
/** Form sayfası ve liste düzenleme hücresinin ortak görsel yükleme editörü. */
const ImageUploadEditorComponent = ({ const ImageUploadEditorComponent = ({
value, value,
options, options,
@ -25,7 +70,7 @@ const ImageUploadEditorComponent = ({
editorOptions?: any editorOptions?: any
}): ReactElement => { }): ReactElement => {
const { translate } = useLocalization() const { translate } = useLocalization()
const readOnly: boolean = editorOptions?.readOnly ?? false const readOnly: boolean = editorOptions?.readOnly === true || editorOptions?.disabled === true
// options önce prop olarak gelen ImageUploadOptionsDto'dan, sonra editorOptions JSON'undan okunur // options önce prop olarak gelen ImageUploadOptionsDto'dan, sonra editorOptions JSON'undan okunur
const resolvedOptions: ImageUploadOptionsDto = options ?? editorOptions ?? {} const resolvedOptions: ImageUploadOptionsDto = options ?? editorOptions ?? {}
@ -40,10 +85,7 @@ const ImageUploadEditorComponent = ({
// aksi halde dokunmatik cihazda dosya seciciye ek olarak kamera sunulur. // aksi halde dokunmatik cihazda dosya seciciye ek olarak kamera sunulur.
const showCameraButton = !resolvedOptions.capture && supportsPhotoCapture() const showCameraButton = !resolvedOptions.capture && supportsPhotoCapture()
const valueUrls = useMemo<string[]>( const valueUrls = useMemo<string[]>(() => normalizeUploadValue(value), [value])
() => (Array.isArray(value) ? value : [value]).filter(Boolean) as string[],
[value],
)
const [urls, setUrls] = useState<string[]>(valueUrls) const [urls, setUrls] = useState<string[]>(valueUrls)
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
@ -63,14 +105,6 @@ const ImageUploadEditorComponent = ({
onValueChanged(newValue) onValueChanged(newValue)
} }
const toBase64 = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = reject
reader.readAsDataURL(file)
})
const handleFiles = async (files: FileList | null) => { const handleFiles = async (files: FileList | null) => {
if (!files?.length) return if (!files?.length) return
@ -87,7 +121,7 @@ const ImageUploadEditorComponent = ({
const res = await fetch(resolvedOptions.uploadUrl, { method: 'POST', body: formData }) const res = await fetch(resolvedOptions.uploadUrl, { method: 'POST', body: formData })
if (!res.ok) throw new Error(`Upload failed: ${res.status}`) if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
const data = await res.json() const data = await res.json()
uploadedUrls.push(String(data?.url ?? data?.fileUrl ?? data?.path ?? data ?? '').trim()) uploadedUrls.push(resolveUploadedUrl(data))
} else { } else {
// uploadUrl yoksa base64 olarak sakla // uploadUrl yoksa base64 olarak sakla
const base64 = await toBase64(file) const base64 = await toBase64(file)

View file

@ -14,7 +14,6 @@ const TagBoxEditorComponent = ({
value: any value: any
setDefaultValue: boolean setDefaultValue: boolean
options?: TagBoxOptionsDto options?: TagBoxOptionsDto
values: any // all row data
onValueChanged: (e: any) => void onValueChanged: (e: any) => void
col?: ColumnFormatDto col?: ColumnFormatDto
editorOptions: any editorOptions: any
@ -27,11 +26,6 @@ const TagBoxEditorComponent = ({
[lookupDto?.displayExpr], [lookupDto?.displayExpr],
) )
// const dataSource =
// typeof lookup?.dataSource === 'function'
// ? lookup.dataSource(values)
// : (lookup?.dataSource ?? [])
return ( return (
<TagBox <TagBox
{...editorOptions} {...editorOptions}

View file

@ -9,15 +9,18 @@ import type { GroupItem } from 'devextreme/ui/form'
import type { ColumnFormatDto, GridDto } from '@/proxy/form/models' import type { ColumnFormatDto, GridDto } from '@/proxy/form/models'
import { getEditingFormGroups } from '../../list/Utils' import { getEditingFormGroups } from '../../list/Utils'
import { import {
getLookupEditorDefaults,
isFormItemVisible, isFormItemVisible,
resolveEditorButtons,
resolveEditorType, resolveEditorType,
withEditorTypeDefaults, withEditorTypeDefaults,
} from '../../list/shared/editingForm' } from '../../list/shared/editingForm'
import { getCascadeParentFieldsOf } from '../../list/shared/cascade'
import type { GridColumnData } from '../../list/GridColumnData' import type { GridColumnData } from '../../list/GridColumnData'
import type { RowMode, SimpleItemWithColData } from '../types' import type { RowMode, SimpleItemWithColData } from '../types'
/** Grup / tab ağacındaki tüm veri alanlarını düz listeye indirger. */ /** Grup / tab ağacındaki tüm veri alanlarını düz listeye indirger. */
export const flattenFormItems = (items: any[] = []): SimpleItemWithColData[] => const flattenFormItems = (items: any[] = []): SimpleItemWithColData[] =>
items.flatMap((item) => [ items.flatMap((item) => [
...(item?.dataField ? [item] : []), ...(item?.dataField ? [item] : []),
...flattenFormItems(item?.items || []), ...flattenFormItems(item?.items || []),
@ -30,10 +33,7 @@ export const flattenFormGroups = (groups: GroupItem[] = []): SimpleItemWithColDa
/** Bir alanın cascade parent alan adlarını döner. */ /** Bir alanın cascade parent alan adlarını döner. */
export const getCascadeParentFields = (item: SimpleItemWithColData): string[] => export const getCascadeParentFields = (item: SimpleItemWithColData): string[] =>
item.colData?.lookupDto?.cascadeParentFields getCascadeParentFieldsOf(item.colData)
?.split(',')
.map((field: string) => field.trim())
.filter(Boolean) ?? []
type BuildFormItemsParams = { type BuildFormItemsParams = {
gridDto: GridDto gridDto: GridDto
@ -77,16 +77,18 @@ const toFormItem = (
console.error('EditorOptions parse error:', colFormat.fieldName, err) console.error('EditorOptions parse error:', colFormat.fieldName, err)
} }
// Liste popup'ıyla aynı lookup varsayılanları; veri kaynağını form sayfası kendisi kurar.
const lookupEditorOptions = colFormat.lookupDto?.dataSourceType const lookupEditorOptions = colFormat.lookupDto?.dataSourceType
? { ? {
...getLookupEditorDefaults(colFormat),
dataSource: getLookupDataSource(colFormat, formData), dataSource: getLookupDataSource(colFormat, formData),
valueExpr: colFormat.lookupDto?.valueExpr?.toLowerCase(),
displayExpr: colFormat.lookupDto?.displayExpr?.toLowerCase(),
// Grid popup'ındaki lookup kolonu aramayı varsayılan açar; form sayfası aynı davranır.
searchEnabled: true,
} }
: {} : {}
if (parsedEditorOptions.buttons) {
parsedEditorOptions.buttons = resolveEditorButtons(parsedEditorOptions.buttons)
}
const item: SimpleItemWithColData = { const item: SimpleItemWithColData = {
canRead: colFormat.canRead ?? false, canRead: colFormat.canRead ?? false,
canUpdate: colFormat.canUpdate ?? false, canUpdate: colFormat.canUpdate ?? false,
@ -99,10 +101,11 @@ const toFormItem = (
editorType2: colFormat.editorType2, editorType2: colFormat.editorType2,
editorType: resolveEditorType(colFormat.editorType2), editorType: resolveEditorType(colFormat.editorType2),
colSpan: colFormat.colSpan, colSpan: colFormat.colSpan,
editorOptions: withEditorTypeDefaults(colFormat.editorType2, { editorOptions: withEditorTypeDefaults(
...lookupEditorOptions, colFormat.editorType2,
...parsedEditorOptions, { ...lookupEditorOptions, ...parsedEditorOptions },
}), colFormat.sourceDbType,
),
// DevExtreme item seçeneklerini yerinde (deep extend) günceller; ekran tanımı (gridDto) paylaşılırsa // DevExtreme item seçeneklerini yerinde (deep extend) günceller; ekran tanımı (gridDto) paylaşılırsa
// sıra değişince bir alanın tanımı diğerininkiyle ezilir. Item'a tanımın kopyası verilir. // sıra değişince bir alanın tanımı diğerininkiyle ezilir. Item'a tanımın kopyası verilir.
colData: structuredClone(colFormat), colData: structuredClone(colFormat),

View file

@ -28,7 +28,7 @@ const isEmptyValue = (value: unknown) => value === undefined || value === null |
* StaticData için öğelerdeki `CascadeRelationField` alanı (varsayılan `group`) ilk ebeveynin * StaticData için öğelerdeki `CascadeRelationField` alanı (varsayılan `group`) ilk ebeveynin
* değeriyle karşılaştırılır; ebeveyn boşsa süzülmez. * değeriyle karşılaştırılır; ebeveyn boşsa süzülmez.
*/ */
export const buildCascadeFilters = (lookupDto: any, rowData: any): any[] | null => { const buildCascadeFilters = (lookupDto: any, rowData: any): any[] | null => {
const parentFields = splitFields(lookupDto?.cascadeParentFields) const parentFields = splitFields(lookupDto?.cascadeParentFields)
if (!parentFields.length) return null if (!parentFields.length) return null
@ -46,7 +46,7 @@ export const buildCascadeFilters = (lookupDto: any, rowData: any): any[] | null
return parentFields.map((field) => (isEmptyValue(rowData?.[field]) ? null : rowData[field])) return parentFields.map((field) => (isEmptyValue(rowData?.[field]) ? null : rowData[field]))
} }
export const createLookupStaticDataSource = ( const createLookupStaticDataSource = (
load: () => any, load: () => any,
filter: any[] | null, filter: any[] | null,
cacheKey: string, cacheKey: string,
@ -65,7 +65,7 @@ export const createLookupStaticDataSource = (
filter, filter,
}) })
export const createLookupQueryDataSource = ( const createLookupQueryDataSource = (
listFormCode?: string, listFormCode?: string,
listFormFieldName?: string, listFormFieldName?: string,
filters?: any[] | null, filters?: any[] | null,
@ -106,7 +106,7 @@ export const createLookupQueryDataSource = (
/** Gövde JSON şablonudur; değer tırnak içine yerleştiği için JSON kaçışlı yazılır. */ /** Gövde JSON şablonudur; değer tırnak içine yerleştiği için JSON kaçışlı yazılır. */
const toJsonText = (value: unknown) => (value === null ? 'null' : JSON.stringify(String(value)).slice(1, -1)) const toJsonText = (value: unknown) => (value === null ? 'null' : JSON.stringify(String(value)).slice(1, -1))
export const createLookupApiDataSource = ( const createLookupApiDataSource = (
listFormCode?: string, listFormCode?: string,
lookupQuery?: string, lookupQuery?: string,
filters?: any[] | null, filters?: any[] | null,

View file

@ -41,8 +41,7 @@ import { NotePanel } from '../form/notes/NotePanel'
import SubForms from '../form/SubForms' import SubForms from '../form/SubForms'
import GridFilterDialogs from './GridFilterDialogs' import GridFilterDialogs from './GridFilterDialogs'
import { GridExtraFilterToolbar } from './GridExtraFilterToolbar' import { GridExtraFilterToolbar } from './GridExtraFilterToolbar'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent' import { ImageUploadEditorComponent, ImageViewerEditorComponent } from './editors/cellEditors'
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
import { import {
autoNumber, autoNumber,
extractSearchParamsFields, extractSearchParamsFields,

View file

@ -37,10 +37,7 @@ import SubForms from '../form/SubForms'
import { RowMode } from '../form/types' import { RowMode } from '../form/types'
import { GridColumnData } from './GridColumnData' import { GridColumnData } from './GridColumnData'
import GridFilterDialogs from './GridFilterDialogs' import GridFilterDialogs from './GridFilterDialogs'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent' import { GridBoxEditorComponent, ImageUploadEditorComponent, ImageViewerEditorComponent, TagBoxEditorComponent } from './editors/cellEditors'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { useFilters } from './useFilters' import { useFilters } from './useFilters'
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar' import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
import { ImportDashboard } from '@/components/importManager/ImportDashboard' import { ImportDashboard } from '@/components/importManager/ImportDashboard'
@ -63,16 +60,14 @@ import {
buildUpdatePayload, buildUpdatePayload,
createFormItemMapper, createFormItemMapper,
expandJsonFormFields, expandJsonFormFields,
getActiveEditingForm,
getValueByField, getValueByField,
} from './shared/editingForm' } from './shared/editingForm'
import { import {
attachEditorButtonContext, createCustomEditorCellAdapter,
attachEditorValueChangedScript,
createEditingFormHandlers, createEditingFormHandlers,
prepareEditingEditor,
} from './shared/editingFormScripts' } from './shared/editingFormScripts'
import { attachFieldValueBroadcast } from '@/utils/phoneFormat' import { createCascadeMaps } from './shared/cascade'
import { attachCascadeBehavior, createCascadeMaps } from './shared/cascade'
import { createConditionalCellStyleHandler } from './shared/cellStyles' import { createConditionalCellStyleHandler } from './shared/cellStyles'
import { import {
useExtraFilters, useExtraFilters,
@ -343,61 +338,27 @@ const Grid = (props: GridProps) => {
const onEditorPreparing = useCallback( const onEditorPreparing = useCallback(
(editor: DataGridTypes.EditorPreparingEvent<any, any>) => { (editor: DataGridTypes.EditorPreparingEvent<any, any>) => {
if (editor.parentType !== 'dataRow' || !editor.dataField || !gridDto) return if (!gridDto) return
prepareEditingEditor({ editor, gridDto, mode, cascadeMaps, editingFormInstanceRef })
const columnFormat = gridDto.columnFormats.find(
(column) => column.fieldName === editor.dataField,
)
const isNewRow = Boolean((editor as any).row?.isNewRow) || mode === 'new'
if (columnFormat?.editorOptions) {
try {
editor.editorOptions = {
...editor.editorOptions,
...JSON.parse(columnFormat.editorOptions),
}
} catch (err) {
console.error('EditorOptions parse error:', columnFormat.fieldName, err)
}
}
const hasReadOnlyEditorOption = editor.editorOptions?.readOnly === true
const hasDisabledEditorOption = editor.editorOptions?.disabled === true
if (
(isNewRow && columnFormat?.allowAdding === false) ||
(!isNewRow && columnFormat?.allowEditing === false)
) {
editor.editorOptions.readOnly = true
} else if (isNewRow && columnFormat?.allowAdding === true) {
if (!hasReadOnlyEditorOption) editor.editorOptions.readOnly = false
if (!hasDisabledEditorOption) editor.editorOptions.disabled = false
}
const cascadeInfo = cascadeMaps.cascadeFieldsMap.get(editor.dataField)
if (cascadeInfo) {
attachCascadeBehavior({ editor, gridDto, cascadeInfo, maps: cascadeMaps })
}
// phoneCountry maskesi ülke alanını bu olaydan izler; DataGrid formu fieldDataChanged yayınlamaz.
attachFieldValueBroadcast(editor)
// Yalnızca düzenleme formunda yer alan alanların script'i çalıştırılır.
if (columnFormat?.editOrderNo != null && columnFormat.editorScript) {
attachEditorValueChangedScript({
editor,
editorScript: columnFormat.editorScript,
fieldName: columnFormat.fieldName,
editingFormInstanceRef,
getActiveEditingForm,
})
}
attachEditorButtonContext(editor)
}, },
[gridDto, cascadeMaps, mode], [gridDto, cascadeMaps, mode],
) )
// Şablon fonksiyonları DevExtreme'de bir kez bağlanır; mod ref üzerinden okunur.
const modeRef = useRef(mode)
modeRef.current = mode
const adaptCustomEditorCell = useMemo(
() =>
createCustomEditorCellAdapter({
getGridDto: () => gridDto,
getMode: () => modeRef.current,
cascadeMaps,
editingFormInstanceRef,
}),
[gridDto, cascadeMaps],
)
/* ======================= /* =======================
COLUMNS & DATASOURCE COLUMNS & DATASOURCE
======================= */ ======================= */
@ -665,9 +626,18 @@ const Grid = (props: GridProps) => {
items: editingFormItems, items: editingFormItems,
}} }}
/> />
<Template name="cellEditTagBox" render={TagBoxEditorComponent} /> <Template
<Template name="cellEditGridBox" render={GridBoxEditorComponent} /> name="cellEditTagBox"
<Template name="cellEditImageUpload" render={ImageUploadEditorComponent} /> render={(cell: any) => TagBoxEditorComponent(adaptCustomEditorCell(cell))}
/>
<Template
name="cellEditGridBox"
render={(cell: any) => GridBoxEditorComponent(adaptCustomEditorCell(cell))}
/>
<Template
name="cellEditImageUpload"
render={(cell: any) => ImageUploadEditorComponent(adaptCustomEditorCell(cell))}
/>
<Template <Template
name="cellEditImageViewer" name="cellEditImageViewer"
render={(data: any) => ( render={(data: any) => (

View file

@ -31,10 +31,7 @@ import PageTitle from '@/components/shared/PageTitle'
import { RowMode } from '../form/types' import { RowMode } from '../form/types'
import { GridColumnData } from './GridColumnData' import { GridColumnData } from './GridColumnData'
import GridFilterDialogs from './GridFilterDialogs' import GridFilterDialogs from './GridFilterDialogs'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent' import { GridBoxEditorComponent, ImageUploadEditorComponent, ImageViewerEditorComponent, TagBoxEditorComponent } from './editors/cellEditors'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { useFilters } from './useFilters' import { useFilters } from './useFilters'
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar' import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
import WidgetGroup from '@/components/ui/Widget/WidgetGroup' import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
@ -59,16 +56,14 @@ import {
buildUpdatePayload, buildUpdatePayload,
createFormItemMapper, createFormItemMapper,
expandJsonFormFields, expandJsonFormFields,
getActiveEditingForm,
getValueByField, getValueByField,
} from './shared/editingForm' } from './shared/editingForm'
import { import {
attachEditorButtonContext, createCustomEditorCellAdapter,
attachEditorValueChangedScript,
createEditingFormHandlers, createEditingFormHandlers,
prepareEditingEditor,
} from './shared/editingFormScripts' } from './shared/editingFormScripts'
import { attachFieldValueBroadcast } from '@/utils/phoneFormat' import { createCascadeMaps } from './shared/cascade'
import { attachCascadeBehavior, createCascadeMaps } from './shared/cascade'
import { createConditionalCellStyleHandler } from './shared/cellStyles' import { createConditionalCellStyleHandler } from './shared/cellStyles'
import { import {
useExtraFilters, useExtraFilters,
@ -354,60 +349,27 @@ const Tree = (props: TreeProps) => {
const onEditorPreparing = useCallback( const onEditorPreparing = useCallback(
(editor: TreeListTypes.EditorPreparingEvent) => { (editor: TreeListTypes.EditorPreparingEvent) => {
if (editor.parentType !== 'dataRow' || !editor.dataField || !gridDto) return if (!gridDto) return
prepareEditingEditor({ editor, gridDto, mode, cascadeMaps, editingFormInstanceRef })
const columnFormat = gridDto.columnFormats.find(
(column) => column.fieldName === editor.dataField,
)
const isNewRow = Boolean((editor as any).row?.isNewRow) || mode === 'new'
if (columnFormat?.editorOptions) {
try {
editor.editorOptions = {
...editor.editorOptions,
...JSON.parse(columnFormat.editorOptions),
}
} catch (err) {
console.error('EditorOptions parse error:', columnFormat.fieldName, err)
}
}
const hasReadOnlyEditorOption = editor.editorOptions?.readOnly === true
const hasDisabledEditorOption = editor.editorOptions?.disabled === true
if (
(isNewRow && columnFormat?.allowAdding === false) ||
(!isNewRow && columnFormat?.allowEditing === false)
) {
editor.editorOptions.readOnly = true
} else if (isNewRow && columnFormat?.allowAdding === true) {
if (!hasReadOnlyEditorOption) editor.editorOptions.readOnly = false
if (!hasDisabledEditorOption) editor.editorOptions.disabled = false
}
const cascadeInfo = cascadeMaps.cascadeFieldsMap.get(editor.dataField)
if (cascadeInfo) {
attachCascadeBehavior({ editor, gridDto, cascadeInfo, maps: cascadeMaps })
}
// phoneCountry maskesi ülke alanını bu olaydan izler; DataGrid formu fieldDataChanged yayınlamaz.
attachFieldValueBroadcast(editor)
if (columnFormat?.editOrderNo != null && columnFormat.editorScript) {
attachEditorValueChangedScript({
editor,
editorScript: columnFormat.editorScript,
fieldName: columnFormat.fieldName,
editingFormInstanceRef,
getActiveEditingForm,
})
}
attachEditorButtonContext(editor)
}, },
[gridDto, cascadeMaps, mode], [gridDto, cascadeMaps, mode],
) )
// Şablon fonksiyonları DevExtreme'de bir kez bağlanır; mod ref üzerinden okunur.
const modeRef = useRef(mode)
modeRef.current = mode
const adaptCustomEditorCell = useMemo(
() =>
createCustomEditorCellAdapter({
getGridDto: () => gridDto,
getMode: () => modeRef.current,
cascadeMaps,
editingFormInstanceRef,
}),
[gridDto, cascadeMaps],
)
/* ======================= /* =======================
COLUMNS & DATASOURCE COLUMNS & DATASOURCE
======================= */ ======================= */
@ -653,9 +615,18 @@ const Tree = (props: TreeProps) => {
items: editingFormItems, items: editingFormItems,
}} }}
/> />
<Template name="cellEditTagBox" render={TagBoxEditorComponent} /> <Template
<Template name="cellEditGridBox" render={GridBoxEditorComponent} /> name="cellEditTagBox"
<Template name="cellEditImageUpload" render={ImageUploadEditorComponent} /> render={(cell: any) => TagBoxEditorComponent(adaptCustomEditorCell(cell))}
/>
<Template
name="cellEditGridBox"
render={(cell: any) => GridBoxEditorComponent(adaptCustomEditorCell(cell))}
/>
<Template
name="cellEditImageUpload"
render={(cell: any) => ImageUploadEditorComponent(adaptCustomEditorCell(cell))}
/>
<Template <Template
name="cellEditImageViewer" name="cellEditImageViewer"
render={(data: any) => ( render={(data: any) => (

View file

@ -1,30 +0,0 @@
import { ReactElement } from 'react'
import { SharedGridBoxEditor } from '@/views/shared/SharedGridBoxEditor'
const GridBoxEditorComponent = (cellElement: any): ReactElement => {
const col = cellElement.column
if (!col) {
return <></>
}
const options = col.extras.gridBoxOptions
const lookup = col?.lookup
const dataSource: any =
typeof lookup?.dataSource === 'function'
? lookup.dataSource(cellElement.row)
: (lookup?.dataSource ?? [])
return (
<SharedGridBoxEditor
value={cellElement.value}
dataSource={dataSource}
lookup={lookup}
options={options}
editorOptions={col.extras.editorOptions}
dropDownOptions={{ width: options?.width, hideOnOutsideClick: true }}
onCommit={(nextValue) => cellElement.setValue(nextValue)}
setInitialValueOnContentReady
/>
)
}
export { GridBoxEditorComponent }

View file

@ -1,273 +0,0 @@
import type { ReactElement } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { FaCamera, FaSpinner, FaUpload } from 'react-icons/fa'
import { Button } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization'
import {
hideImageHoverPreview,
openImageInNewTab,
showImageHoverPreview,
thumbnailSource,
} from '../../form/editors/imageHoverPreview'
import { normalizeImageSize, parseJsonObject } from '../shared/imageValue'
import Input from '@/components/ui/Input'
import { supportsPhotoCapture } from '@/utils/device'
type ImageUploadOptions = {
accept?: string
/** Mobilde dosya seçici yerine kamerayı açar. */
capture?: 'environment' | 'user'
fileFieldName?: string
height?: number | string
multiple?: boolean | string
uploadUrl?: string
width?: number | string
}
/**
* Yüklenen değerin ham biçimini korur; ayraçlara göre bölmez.
* (Görüntüleme tarafındaki `normalizeImageValue` ile bilinçli olarak farklıdır:
* burada kullanıcı tarafından yazılmış virgüllü URL'ler bozulmamalıdır.)
*/
const normalizeUploadValue = (value: unknown): string[] => {
if (!value) return []
if (Array.isArray(value)) return value.flatMap(normalizeUploadValue)
if (typeof value === 'string') {
const trimmed = value.trim()
if (!trimmed) return []
if (trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed)
if (Array.isArray(parsed)) return parsed.flatMap(normalizeUploadValue)
} catch {
return [trimmed]
}
}
return [trimmed]
}
return [String(value)].filter(Boolean)
}
const resolveOptions = (column: any): ImageUploadOptions => {
const configuredOptions = column?.extras?.imageUploadOptions
if (configuredOptions && Object.keys(configuredOptions).length > 0) {
return configuredOptions
}
const parsedOptions = parseJsonObject(column?.extras?.editorOptions ?? column?.editorOptions)
return parsedOptions ?? {}
}
const resolveUploadedUrl = (data: unknown) => {
if (typeof data === 'string') return data
if (!data || typeof data !== 'object') return ''
const record = data as Record<string, unknown>
return String(record.url ?? record.fileUrl ?? record.path ?? '').trim()
}
const toBase64 = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = reject
reader.readAsDataURL(file)
})
const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
const { translate } = useLocalization()
const col = cellElement.column
const [uploading, setUploading] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
const cameraInputRef = useRef<HTMLInputElement>(null)
const [urls, setUrls] = useState<string[]>(() => normalizeUploadValue(cellElement.value))
useEffect(() => {
setUrls(normalizeUploadValue(cellElement.value))
}, [cellElement.value])
const options = useMemo(() => resolveOptions(col), [col])
if (!col) {
return <></>
}
// JSON'dan string olarak gelebilir: "true" / "false"
const isMultiple: boolean = options.multiple === true || options.multiple === 'true'
const thumbW = normalizeImageSize(options.width, 40)
const thumbH = normalizeImageSize(options.height, 40)
// Alanda `capture` acikca ayarliysa kullanicinin istedigi tek davranis odur;
// aksi halde dokunmatik cihazda dosya seciciye ek olarak kamera sunulur.
const showCameraButton = !options.capture && supportsPhotoCapture()
const removeImage = (index: number) => {
const newUrls = urls.filter((_, i) => i !== index)
setUrls(newUrls)
if (isMultiple) {
cellElement.setValue(newUrls.length > 0 ? newUrls : null)
} else {
cellElement.setValue(newUrls[0] ?? null)
}
}
const handleFiles = async (files: FileList | null) => {
if (!files?.length) return
setUploading(true)
try {
// isMultiple=true ise mevcut url'leri koru, üstüne ekle; false ise sıfırla
const uploadedUrls: string[] = isMultiple ? [...urls] : []
for (const file of Array.from(files)) {
if (options.uploadUrl) {
// HTTP upload
const formData = new FormData()
const fileFieldName: string = options.fileFieldName ?? 'file'
formData.append(fileFieldName, file)
const res = await fetch(options.uploadUrl, { method: 'POST', body: formData })
if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
const data = await res.json()
const url = resolveUploadedUrl(data)
uploadedUrls.push(url)
} else {
// uploadUrl yapılandırılmamışsa base64 olarak sakla
const base64 = await toBase64(file)
uploadedUrls.push(base64)
}
}
const newUrls = uploadedUrls.filter(Boolean)
setUrls(newUrls)
const newValue = isMultiple ? newUrls : (newUrls[newUrls.length - 1] ?? null)
cellElement.setValue(newValue)
} catch (e) {
console.error('ImageUploadEditorComponent upload error:', e)
} finally {
setUploading(false)
if (inputRef.current) inputRef.current.value = ''
// Ayni fotografi ikinci kez cekmek degisiklik olayini tetiklemezdi.
if (cameraInputRef.current) cameraInputRef.current.value = ''
}
}
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
{urls.map((url, i) => (
<div key={i} style={{ position: 'relative', display: 'inline-block' }}>
<img
src={thumbnailSource(url)}
alt=""
style={{
width: thumbW,
height: thumbH,
objectFit: 'cover',
borderRadius: 4,
border: '1px solid #ddd',
cursor: 'zoom-in',
}}
onMouseEnter={(event) => showImageHoverPreview(url, event)}
onMouseMove={(event) => showImageHoverPreview(url, event)}
onMouseLeave={hideImageHoverPreview}
onClick={() => openImageInNewTab(url)}
/>
<Button
type="button"
title={translate('::App.Platform.Delete')}
variant="plain"
shape="circle"
className="!absolute !h-[18px] !w-[18px] !bg-red-600 !p-0 !text-[11px] !text-white hover:!bg-red-700 active:!bg-red-800"
style={{
position: 'absolute',
top: -6,
right: -6,
background: '#dc3545',
color: '#fff',
border: 'none',
borderRadius: '50%',
width: 18,
height: 18,
cursor: 'pointer',
fontSize: 11,
lineHeight: '18px',
padding: 0,
textAlign: 'center',
}}
onClick={() => removeImage(i)}
>
×
</Button>
</div>
))}
<Input
unstyle
ref={inputRef}
type="file"
accept={options.accept ?? 'image/*'}
capture={options.capture}
multiple={isMultiple}
style={{ display: 'none' }}
onChange={(e) => handleFiles(e.target.files)}
/>
{/* Kamera ayrı bir input'tur: `capture` dosya seçiciyi kapattığı için
tek input ile hem galeri hem kamera sunulamaz. */}
{showCameraButton && (
<Input
ref={cameraInputRef}
unstyle
type="file"
accept={options.accept ?? 'image/*'}
capture="environment"
style={{ display: 'none' }}
onChange={(e) => handleFiles(e.target.files)}
/>
)}
<Button
type="button"
title={translate('::App.ImageUpload.Upload')}
disabled={uploading}
variant="solid"
shape="none"
className="!h-auto !rounded !px-2.5 !py-1 !text-[13px]"
style={{
padding: '4px 10px',
background: uploading ? '#aaa' : '#0078d4',
color: '#fff',
border: 'none',
borderRadius: 4,
cursor: uploading ? 'not-allowed' : 'pointer',
fontSize: 13,
display: 'flex',
alignItems: 'center',
gap: 4,
}}
onClick={() => inputRef.current?.click()}
>
{uploading ? (
<FaSpinner style={{ fontSize: 14 }} className="spin" />
) : (
<FaUpload style={{ fontSize: 14 }} />
)}
</Button>
{showCameraButton && (
<Button
type="button"
title={translate('::App.ImageUpload.TakePhoto')}
disabled={uploading}
variant="solid"
shape="none"
className="!h-auto !rounded !bg-[#0078d4] !px-2.5 !py-1 !text-[13px] !text-white"
onClick={() => cameraInputRef.current?.click()}
>
<FaCamera style={{ fontSize: 14 }} />
</Button>
)}
</div>
)
}
export { ImageUploadEditorComponent }

View file

@ -1,114 +0,0 @@
import { ReactElement } from 'react'
import { Button } from '@/components/ui'
import {
hideImageHoverPreview,
openImageInNewTab,
showImageHoverPreview,
} from '../../form/editors/imageHoverPreview'
import {
NO_IMAGE,
normalizeImageSize,
normalizeImageValue,
parseJsonObject,
} from '../shared/imageValue'
const resolveTemplateValue = (templateData: any) => {
const dataField =
templateData?.dataField ||
templateData?.item?.dataField ||
templateData?.editorOptions?.name ||
templateData?.name
const fieldCandidates = String(dataField || '')
.split(':')
.reduce<string[]>(
(fields, field) => {
const value = field.trim()
return value ? [...fields, value] : fields
},
dataField ? [String(dataField)] : [],
)
const formDataSources = [
templateData?.component?.option?.('formData'),
templateData?.formData,
templateData?.data,
templateData?.fallbackFormData,
].filter((source) => source && typeof source === 'object')
for (const formData of formDataSources) {
for (const field of fieldCandidates) {
if (Object.prototype.hasOwnProperty.call(formData, field)) {
return formData[field]
}
}
const actualKey = Object.keys(formData).find((key) =>
fieldCandidates.some((field) => key.toLowerCase() === field.toLowerCase()),
)
if (actualKey) {
return formData[actualKey]
}
}
return templateData?.value ?? templateData?.editorOptions?.value
}
const resolveTemplateOptions = (templateData: any) => {
const itemOptions =
templateData?.editorOptions?.imageUploadOptions ??
templateData?.imageUploadOptions ??
parseJsonObject(templateData?.editorOptions?.editorOptions) ??
parseJsonObject(templateData?.editorOptions)
return itemOptions && typeof itemOptions === 'object' ? itemOptions : {}
}
const ImageViewerEditorComponent = (templateData: any): ReactElement => {
const value = resolveTemplateValue(templateData)
const options = resolveTemplateOptions(templateData)
const urls = normalizeImageValue(value)
const thumbW = normalizeImageSize(options?.width, 80)
const thumbH = normalizeImageSize(options?.height, 80)
if (!urls.length) {
return <div className="text-gray-400 px-1 py-2">-</div>
}
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: 4 }}>
{urls.map((url, index) => (
<Button
key={`${url}-${index}`}
type="button"
onClick={() => openImageInNewTab(url)}
variant="plain"
shape="none"
className="!border-0 !bg-transparent !p-0 hover:!bg-transparent active:!bg-transparent dark:hover:!bg-transparent dark:active:!bg-transparent"
style={{ cursor: 'zoom-in' }}
>
<img
src={url}
alt=""
onMouseEnter={(event) => showImageHoverPreview(url, event)}
onMouseMove={(event) => showImageHoverPreview(url, event)}
onMouseLeave={hideImageHoverPreview}
style={{
width: thumbW,
height: thumbH,
objectFit: 'cover',
borderRadius: 4,
display: 'block',
}}
onError={({ currentTarget }) => {
currentTarget.onerror = null
currentTarget.src = NO_IMAGE
}}
/>
</Button>
))}
</div>
)
}
export { ImageViewerEditorComponent }

View file

@ -1,42 +0,0 @@
import TagBox from 'devextreme-react/tag-box'
import { ReactElement } from 'react'
import { createColoredTagRender } from '../../shared/lookupColor'
const TagBoxEditorComponent = (cellElement: any): ReactElement => {
const col = cellElement.column
if (!col) {
return <></>
}
const options = col.extras.tagBoxOptions
const val = Array.isArray(cellElement.value) ? cellElement.value : [cellElement.value]
const lookup = col?.lookup
const dataSource =
typeof lookup?.dataSource === 'function'
? lookup.dataSource(cellElement.row)
: (lookup?.dataSource ?? [])
return (
<TagBox
{...col.extras.editorOptions}
defaultValue={val}
dataSource={dataSource}
valueExpr={lookup?.valueExpr}
displayExpr={lookup?.displayExpr}
tagRender={createColoredTagRender(lookup?.displayExpr)}
showClearButton={options?.showClearButton}
showSelectionControls={options?.showSelectionControls ?? true}
maxDisplayedTags={options?.maxDisplayedTags}
showMultiTagOnly={options?.showMultiTagOnly}
applyValueMode={options?.applyValueMode ?? 'useButtons'}
searchEnabled={options?.searchEnabled}
acceptCustomValue={options?.acceptCustomValue}
onValueChanged={(e) => {
cellElement.setValue(typeof e.value === 'string' ? e.value?.trim() : e.value)
}}
onSelectionChanged={cellElement.component.updateDimensions}
></TagBox>
)
}
export { TagBoxEditorComponent }

View file

@ -0,0 +1,172 @@
import type { ReactElement } from 'react'
import type { ImageUploadOptionsDto } from '@/proxy/form/models'
import { GridBoxEditorComponent as FormGridBoxEditor } from '../../form/editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent as FormImageUploadEditor } from '../../form/editors/ImageUploadEditorComponent'
import { ImageViewerEditorComponent as FormImageViewerEditor } from '../../form/editors/ImageViewerEditorComponent'
import { TagBoxEditorComponent as FormTagBoxEditor } from '../../form/editors/TagBoxEditorComponent'
import { parseJsonObject } from '../shared/imageValue'
/**
* Liste düzenleme hücresini (DataGrid/TreeList `editCellTemplate`, CardView form şablonu) form
* sayfasının editör bileşenlerine bağlayan adaptörler. Editörlerin tek uygulaması
* `form/editors/*`tir; burada yalnızca hücre verisi bileşen prop'larına çevrilir.
*/
/**
* Ekranın editorOptions'ı + hücre bağlamının salt-okunur/devre dışı kararı. Bağlamın geri
* kalanı (`value`, `onValueChanged`…) editörün kendi değer akışını ezeceği için alınmaz.
*/
const getCellEditorOptions = (cellElement: any) => {
const { readOnly, disabled } = cellElement.editorOptions ?? {}
return {
...cellElement.column?.extras?.editorOptions,
...(readOnly !== undefined ? { readOnly } : {}),
...(disabled !== undefined ? { disabled } : {}),
}
}
/** Kolonun lookup kaynağı; cascade ebeveynleri düzenlenen satırdan okunur. */
const getCellLookupDataSource = (cellElement: any) => {
const lookup = cellElement.column?.lookup
return typeof lookup?.dataSource === 'function'
? lookup.dataSource(cellElement.row)
: (lookup?.dataSource ?? [])
}
/** Liste düzenleme hücresi; editör form sayfasıyla aynı bileşendir. */
const TagBoxEditorComponent = (cellElement: any): ReactElement => {
const col = cellElement.column
if (!col) {
return <></>
}
return (
<FormTagBoxEditor
setDefaultValue
value={cellElement.value}
options={col.extras.tagBoxOptions}
col={col.colData}
editorOptions={{
...getCellEditorOptions(cellElement),
dataSource: getCellLookupDataSource(cellElement),
onSelectionChanged: cellElement.component.updateDimensions,
}}
onValueChanged={(value) => cellElement.setValue(value)}
/>
)
}
/** Liste düzenleme hücresi; editör form sayfasıyla aynı bileşendir. */
const GridBoxEditorComponent = (cellElement: any): ReactElement => {
const col = cellElement.column
if (!col) {
return <></>
}
return (
<FormGridBoxEditor
value={cellElement.value}
options={col.extras.gridBoxOptions}
col={col.colData}
editorOptions={{
...getCellEditorOptions(cellElement),
dataSource: getCellLookupDataSource(cellElement),
}}
onValueChanged={(value) => cellElement.setValue(value)}
/>
)
}
const resolveOptions = (column: any): ImageUploadOptionsDto => {
const configuredOptions = column?.extras?.imageUploadOptions
if (configuredOptions && Object.keys(configuredOptions).length > 0) {
return configuredOptions
}
return parseJsonObject(column?.extras?.editorOptions ?? column?.editorOptions) ?? {}
}
/** Liste düzenleme hücresi (Grid, Tree, CardView); editör form sayfasıyla aynı bileşendir. */
const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
const col = cellElement.column
if (!col) {
return <></>
}
return (
<FormImageUploadEditor
value={cellElement.value}
options={resolveOptions(col)}
editorOptions={getCellEditorOptions(cellElement)}
onValueChanged={(value) => cellElement.setValue(value)}
/>
)
}
const resolveTemplateValue = (templateData: any) => {
const dataField =
templateData?.dataField ||
templateData?.item?.dataField ||
templateData?.editorOptions?.name ||
templateData?.name
const fieldCandidates = String(dataField || '')
.split(':')
.reduce<string[]>(
(fields, field) => {
const value = field.trim()
return value ? [...fields, value] : fields
},
dataField ? [String(dataField)] : [],
)
const formDataSources = [
templateData?.component?.option?.('formData'),
templateData?.formData,
templateData?.data,
templateData?.fallbackFormData,
].filter((source) => source && typeof source === 'object')
for (const formData of formDataSources) {
for (const field of fieldCandidates) {
if (Object.prototype.hasOwnProperty.call(formData, field)) {
return formData[field]
}
}
const actualKey = Object.keys(formData).find((key) =>
fieldCandidates.some((field) => key.toLowerCase() === field.toLowerCase()),
)
if (actualKey) {
return formData[actualKey]
}
}
return templateData?.value ?? templateData?.editorOptions?.value
}
const resolveTemplateOptions = (templateData: any) => {
const itemOptions =
templateData?.editorOptions?.imageUploadOptions ??
templateData?.imageUploadOptions ??
parseJsonObject(templateData?.editorOptions?.editorOptions) ??
parseJsonObject(templateData?.editorOptions)
return itemOptions && typeof itemOptions === 'object' ? itemOptions : {}
}
/** Liste düzenleme şablonu (Grid, Tree, CardView); görünüm form sayfasıyla aynı bileşendir. */
const ImageViewerEditorComponent = (templateData: any): ReactElement => (
<FormImageViewerEditor
value={resolveTemplateValue(templateData)}
options={resolveTemplateOptions(templateData)}
/>
)
export {
GridBoxEditorComponent,
ImageUploadEditorComponent,
ImageViewerEditorComponent,
TagBoxEditorComponent,
}

View file

@ -1,19 +1,21 @@
/** /**
* Lookup cascade (bağımlı açılır liste) davranışı. * Lookup cascade (bağımlı açılır liste) davranışı.
* Grid ve Tree aynı kuralları paylaşır. * Grid, Tree ve form sayfası aynı kuralları paylaşır.
*/ */
import type { GridDto } from '@/proxy/form/models' import type { ColumnFormatDto, GridDto } from '@/proxy/form/models'
import { getActiveEditingForm } from './editingForm'
export type CascadeInfo = { type CascadeInfo = {
parentFields: string[] parentFields: string[]
childFields?: string[]
} }
export type CascadeMaps = { export type CascadeMaps = {
/** fieldName -> cascade bilgisi */ /** child fieldName -> cascade bilgisi */
cascadeFieldsMap: Map<string, CascadeInfo> cascadeFieldsMap: Map<string, CascadeInfo>
/** parentField -> etkilediği child alan adları */ /** parentField -> etkilediği child alan adları */
parentToChildrenMap: Map<string, Set<string>> parentToChildrenMap: Map<string, Set<string>>
/** fieldName -> değişince boşaltılacak alanlar */
clearFieldsMap: Map<string, string[]>
} }
const splitFields = (value?: string | null) => const splitFields = (value?: string | null) =>
@ -22,76 +24,105 @@ const splitFields = (value?: string | null) =>
.map((field) => field.trim()) .map((field) => field.trim())
.filter(Boolean) .filter(Boolean)
export const getCascadeParentFieldsOf = (colFormat?: Pick<ColumnFormatDto, 'lookupDto'>) =>
splitFields(colFormat?.lookupDto?.cascadeParentFields)
/**
* Bir alan değiştiğinde boşaltılacak alanlar: ebeveyni bu alan olan çocuklar
* (`CascadeParentFields`) ile alanın kendi `CascadeEmptyFields` listesi.
*/
export const getCascadeClearFields = (
columns: (Pick<ColumnFormatDto, 'fieldName' | 'lookupDto'> | undefined)[],
fieldName: string,
) => {
const fields = new Set<string>()
columns.forEach((column) => {
if (!column?.fieldName) return
if (column.fieldName === fieldName) {
splitFields(column.lookupDto?.cascadeEmptyFields).forEach((field) => fields.add(field))
} else if (getCascadeParentFieldsOf(column).includes(fieldName)) {
fields.add(column.fieldName)
}
})
fields.delete(fieldName)
return [...fields]
}
/** Ebeveynlerinden biri boşsa çocuk kilitlenir (bool ebeveynde `false` da boştur). */
export const isCascadeParentEmpty = (parentFields: string[], data?: Record<string, any>) =>
parentFields.some((parentField) => !data?.[parentField])
/** Ekranın cascade ilişkilerini tek geçişte çıkarır; `clearFieldsMap` `getCascadeClearFields` ile aynıdır. */
export const createCascadeMaps = (gridDto?: GridDto): CascadeMaps => { export const createCascadeMaps = (gridDto?: GridDto): CascadeMaps => {
const cascadeFieldsMap = new Map<string, CascadeInfo>() const cascadeFieldsMap = new Map<string, CascadeInfo>()
const parentToChildrenMap = new Map<string, Set<string>>() const parentToChildrenMap = new Map<string, Set<string>>()
const clearFieldSets = new Map<string, Set<string>>()
const addClearField = (fieldName: string, clearField: string) => {
if (fieldName === clearField) return
const fields = clearFieldSets.get(fieldName) ?? new Set<string>()
fields.add(clearField)
clearFieldSets.set(fieldName, fields)
}
gridDto?.columnFormats.forEach((col) => { gridDto?.columnFormats.forEach((col) => {
if (!col.fieldName || !col.lookupDto?.cascadeParentFields) return const fieldName = col.fieldName
if (!fieldName) return
const parentFields = splitFields(col.lookupDto.cascadeParentFields) splitFields(col.lookupDto?.cascadeEmptyFields).forEach((field) =>
const childFields = col.lookupDto.cascadeEmptyFields addClearField(fieldName, field),
? splitFields(col.lookupDto.cascadeEmptyFields) )
: undefined
cascadeFieldsMap.set(col.fieldName, { parentFields, childFields }) const parentFields = getCascadeParentFieldsOf(col)
if (!parentFields.length) return
cascadeFieldsMap.set(fieldName, { parentFields })
parentFields.forEach((parentField) => { parentFields.forEach((parentField) => {
const children = parentToChildrenMap.get(parentField) ?? new Set<string>() const children = parentToChildrenMap.get(parentField) ?? new Set<string>()
children.add(col.fieldName!) children.add(fieldName)
parentToChildrenMap.set(parentField, children) parentToChildrenMap.set(parentField, children)
addClearField(parentField, fieldName)
}) })
}) })
return { cascadeFieldsMap, parentToChildrenMap } const clearFieldsMap = new Map<string, string[]>(
[...clearFieldSets].map(([fieldName, fields]) => [fieldName, [...fields]]),
)
return { cascadeFieldsMap, parentToChildrenMap, clearFieldsMap }
} }
type AttachCascadeParams = { /** Alan cascade zincirinde (ebeveyn ya da çocuk olarak) yer alıyor mu? */
editor: any export const hasCascadeBehavior = (maps: CascadeMaps, fieldName: string) =>
gridDto: GridDto maps.cascadeFieldsMap.has(fieldName) ||
cascadeInfo: CascadeInfo maps.parentToChildrenMap.has(fieldName) ||
maps: CascadeMaps maps.clearFieldsMap.has(fieldName)
}
/** const getEditingRow = (grid: any) => {
* `onEditorPreparing` içinde cascade davranışını editöre bağlar:
* - parent değiştiğinde child alanları temizler,
* - etkilenen child editörlerin `disabled` durumunu günceller,
* - form ilk açıldığında parent'ı boş olan alanı devre dışı bırakır.
*/
export const attachCascadeBehavior = ({
editor,
gridDto,
cascadeInfo,
maps,
}: AttachCascadeParams) => {
const { cascadeFieldsMap, parentToChildrenMap } = maps
const affectedChildren = parentToChildrenMap.get(editor.dataField)
const previousHandler = editor.editorOptions.onValueChanged
editor.editorOptions.onValueChanged = (e: any) => {
previousHandler?.(e)
const grid = editor.component
const rowKey = grid.option('editing.editRowKey') const rowKey = grid.option('editing.editRowKey')
const rowIndex = grid.getRowIndexByKey(rowKey) return {
rowIndex: grid.getRowIndexByKey(rowKey) as number,
// Parent değiştiğinde bağımlı alanları temizle. rowData: grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data as
if (cascadeInfo.childFields && rowIndex >= 0) { | Record<string, any>
cascadeInfo.childFields.forEach((childField) => grid.cellValue(rowIndex, childField, null)) | undefined,
}
} }
if (!affectedChildren?.size) return /** Parent değiştikten sonra etkilenen çocuk editörlerin kilidini yeniler. */
const updateChildDisabledStates = (
grid: any,
gridDto: GridDto,
maps: CascadeMaps,
children: Iterable<string>,
rowData: Record<string, any>,
) => {
const form = getActiveEditingForm(grid)
if (!form?.getEditor) return
const formInstance = grid.option('editing.form') as any for (const childFieldName of children) {
if (!formInstance?.getEditor) return const childInfo = maps.cascadeFieldsMap.get(childFieldName)
if (!childInfo) continue
const rowData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data
if (!rowData) return
affectedChildren.forEach((childFieldName) => {
const childInfo = cascadeFieldsMap.get(childFieldName)
if (!childInfo) return
try { try {
const childColumn = gridDto.columnFormats.find( const childColumn = gridDto.columnFormats.find(
@ -100,25 +131,75 @@ export const attachCascadeBehavior = ({
const childEditorOptions = childColumn?.editorOptions const childEditorOptions = childColumn?.editorOptions
? JSON.parse(childColumn.editorOptions) ? JSON.parse(childColumn.editorOptions)
: {} : {}
const childEditor = formInstance.getEditor(childFieldName)
childEditor?.option( form
.getEditor(childFieldName)
?.option(
'disabled', 'disabled',
childEditorOptions?.disabled === true || childEditorOptions?.disabled === true ||
childInfo.parentFields.some((parentField) => !rowData[parentField]), isCascadeParentEmpty(childInfo.parentFields, rowData),
) )
} catch (err) { } catch (err) {
console.error(`Cascade child editor güncellenemedi: "${childFieldName}"`, err) console.error(`Cascade child editor güncellenemedi: "${childFieldName}"`, err)
} }
}) }
} }
// İlk açılışta parent'ı dolu değilse editörü kilitle. /**
const grid = editor.component * Bir alanın değeri değiştiğinde cascade kurallarını düzenlenen satıra uygular:
const rowKey = grid.option('editing.editRowKey') * bağımlı alanları boşaltır ve çocuk editörlerin kilidini yeniler. Grid editörleri
const rowData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data * (`onEditorPreparing`) ve platform editörleri (TagBox, GridBox…) aynı yoldan geçer.
*/
export const applyCascadeChange = (
grid: any,
gridDto: GridDto,
maps: CascadeMaps,
fieldName: string,
value: any,
) => {
if (!grid?.getVisibleRows) return
if (rowData && cascadeInfo.parentFields.some((parentField) => !rowData[parentField])) { const { rowIndex, rowData } = getEditingRow(grid)
const clearFields = maps.clearFieldsMap.get(fieldName) ?? []
if (rowIndex >= 0) {
clearFields.forEach((childField) => grid.cellValue(rowIndex, childField, null))
}
const affectedChildren = maps.parentToChildrenMap.get(fieldName)
if (!affectedChildren?.size || !rowData) return
const nextRowData = {
...rowData,
[fieldName]: value,
...Object.fromEntries(clearFields.map((field) => [field, null])),
}
updateChildDisabledStates(grid, gridDto, maps, affectedChildren, nextRowData)
}
type AttachCascadeParams = {
editor: any
gridDto: GridDto
maps: CascadeMaps
}
/**
* `onEditorPreparing` içinde cascade davranışını editöre bağlar:
* - değer değişince bağımlı alanları temizler ve çocukların kilidini yeniler,
* - form ilk açıldığında parent'ı boş olan alanı devre dışı bırakır.
*/
export const attachCascadeBehavior = ({ editor, gridDto, maps }: AttachCascadeParams) => {
const fieldName = editor.dataField as string
const previousHandler = editor.editorOptions.onValueChanged
editor.editorOptions.onValueChanged = (e: any) => {
previousHandler?.(e)
applyCascadeChange(editor.component, gridDto, maps, fieldName, e.value)
}
const cascadeInfo = maps.cascadeFieldsMap.get(fieldName)
const { rowData } = getEditingRow(editor.component)
if (cascadeInfo && rowData && isCascadeParentEmpty(cascadeInfo.parentFields, rowData)) {
editor.editorOptions.disabled = true editor.editorOptions.disabled = true
} }
} }

View file

@ -9,7 +9,7 @@ export const isTouchLikeDevice = () =>
typeof window !== 'undefined' && typeof window !== 'undefined' &&
(window.matchMedia?.('(pointer: coarse)').matches || window.matchMedia?.('(hover: none)').matches) (window.matchMedia?.('(pointer: coarse)').matches || window.matchMedia?.('(hover: none)').matches)
export const isMobileViewport = () => const isMobileViewport = () =>
typeof window !== 'undefined' && window.matchMedia?.('(max-width: 767px)').matches typeof window !== 'undefined' && window.matchMedia?.('(max-width: 767px)').matches
/** Popup'ın mobil davranış kurallarıyla açılıp açılmayacağını belirler. */ /** Popup'ın mobil davranış kurallarıyla açılıp açılmayacağını belirler. */

View file

@ -27,7 +27,7 @@ import {
setFormEditingExtraItemValues, setFormEditingExtraItemValues,
} from '../Utils' } from '../Utils'
export type EditorOptionsWithButtons = { buttons?: any[] } & Record<string, any> type EditorOptionsWithButtons = { buttons?: any[] } & Record<string, any>
/** /**
* `editorOptions.buttons` içinde bu değeri taşıyan `onClick`, kamerayla barkod * `editorOptions.buttons` içinde bu değeri taşıyan `onClick`, kamerayla barkod
@ -44,6 +44,41 @@ const scanBarcodeIntoEditor = async (e: any) => {
} }
} }
/**
* Yapılandırmadan gelen buton `onClick` değerlerini fonksiyona çevirir: barkod token'ı ya da
* fonksiyon metni. Liste popup'ı ve form sayfası aynı çözümü kullanır; nesneler yerinde
* değiştirilmez, aynı tanım iki kez çözülse de metin kaybolmaz.
*/
export const resolveEditorButtons = (buttons?: any[]) =>
buttons?.map((button: any) => {
const onClick = button?.options?.onClick
if (typeof onClick !== 'string') return button
return {
...button,
options: {
...button.options,
// Yapılandırmadan gelen fonksiyon tanımı (platform tasarımı gereği).
onClick: onClick === SCAN_BARCODE_TOKEN ? scanBarcodeIntoEditor : eval(`(${onClick})`),
},
}
})
/** Buton tıklamasına kayıt bağlamını (formData, fieldName, mode…) ekler. */
export const withButtonContext = (
buttons: any[] | undefined,
getContext: () => Record<string, any>,
) =>
buttons?.map((button: any) => {
const onClick = button?.options?.onClick
if (typeof onClick !== 'function') return button
return {
...button,
options: { ...button.options, onClick: (e: any) => onClick({ ...e, ...getContext() }) },
}
})
/* ------------------------------------------------------------------ * /* ------------------------------------------------------------------ *
* Satır anahtarı yardımcıları * Satır anahtarı yardımcıları
* ------------------------------------------------------------------ */ * ------------------------------------------------------------------ */
@ -400,6 +435,9 @@ const dateTimeEditorDefaults = {
displayFormat: 'shortDateShortTime', displayFormat: 'shortDateShortTime',
} }
/** Tarih editörünün klavye/temizleme davranışı; liste popup'ı ve form sayfası aynı açılır. */
const dateBoxDefaults = { useMaskBehavior: true, openOnFieldClick: true, showClearButton: true }
/** Kolonun DB tipine göre tarih editörü varsayılanlarını döner. */ /** Kolonun DB tipine göre tarih editörü varsayılanlarını döner. */
const getDateEditorDefaults = (sourceDbType?: DbTypeEnum) => { const getDateEditorDefaults = (sourceDbType?: DbTypeEnum) => {
if (sourceDbType === DbTypeEnum.Date) return dateEditorDefaults if (sourceDbType === DbTypeEnum.Date) return dateEditorDefaults
@ -429,10 +467,26 @@ export const resolveEditorType = (editorType2?: EditorType2) => {
export const withEditorTypeDefaults = <T extends Record<string, any>>( export const withEditorTypeDefaults = <T extends Record<string, any>>(
editorType2: EditorType2 | undefined, editorType2: EditorType2 | undefined,
editorOptions: T, editorOptions: T,
): T => sourceDbType?: DbTypeEnum,
editorType2 === 'dxHtmlEditor' ): T => {
? mergeHtmlEditorOptions(editorOptions) if (editorType2 === 'dxHtmlEditor') return mergeHtmlEditorOptions(editorOptions)
: withPhoneEditorOptions(editorOptions)
const dateDefaults = {
...(editorType2 === 'dxDateBox' ? dateBoxDefaults : {}),
...(getDateEditorDefaults(sourceDbType) ?? {}),
}
return withPhoneEditorOptions({ ...dateDefaults, ...editorOptions })
}
/** Lookup alanının editör varsayılanları; veri kaynağını liste kolonu ya da form sayfası verir. */
export const getLookupEditorDefaults = (colFormat: ColumnFormatDto) =>
colFormat.lookupDto?.dataSourceType
? {
valueExpr: colFormat.lookupDto.valueExpr?.toLowerCase(),
displayExpr: colFormat.lookupDto.displayExpr?.toLowerCase(),
searchEnabled: true,
}
: {}
type FormItemMapperParams = { type FormItemMapperParams = {
mode: RowMode mode: RowMode
@ -477,9 +531,7 @@ export const createFormItemMapper = ({
console.error('EditorOptions parse error:', fieldName, err) console.error('EditorOptions parse error:', fieldName, err)
} }
const defaultEditorOptions: EditorOptionsWithButtons = { const defaultEditorOptions: EditorOptionsWithButtons = getLookupEditorDefaults(colFormat)
...(getDateEditorDefaults(colFormat.sourceDbType) ?? {}),
}
if (colFormat.placeHolder) { if (colFormat.placeHolder) {
defaultEditorOptions.placeholder = translate('::' + colFormat.placeHolder) defaultEditorOptions.placeholder = translate('::' + colFormat.placeHolder)
@ -495,22 +547,14 @@ export const createFormItemMapper = ({
? { readOnly: true } ? { readOnly: true }
: {} : {}
const editorOptions: EditorOptionsWithButtons = withEditorTypeDefaults(colFormat.editorType2, { const editorOptions: EditorOptionsWithButtons = withEditorTypeDefaults(
...defaultEditorOptions, colFormat.editorType2,
...parsedEditorOptions, { ...defaultEditorOptions, ...parsedEditorOptions, ...forcedEditorOptions },
...forcedEditorOptions, colFormat.sourceDbType,
}) )
if (editorOptions.buttons) { if (editorOptions.buttons) {
editorOptions.buttons = editorOptions.buttons.map((btn: any) => { editorOptions.buttons = resolveEditorButtons(editorOptions.buttons)
if (btn?.options?.onClick === SCAN_BARCODE_TOKEN) {
btn.options.onClick = scanBarcodeIntoEditor
} else if (typeof btn?.options?.onClick === 'string') {
// Yapılandırmadan gelen fonksiyon tanımı (platform tasarımı gereği).
btn.options.onClick = eval(`(${btn.options.onClick})`)
}
return btn
})
} }
const item: SimpleItemWithColData = { const item: SimpleItemWithColData = {

View file

@ -6,12 +6,24 @@
*/ */
import type { MutableRefObject } from 'react' import type { MutableRefObject } from 'react'
import { executeEditorScript } from '@/utils/editorScriptRuntime' import { executeEditorScript } from '@/utils/editorScriptRuntime'
import { attachFieldValueBroadcast } from '@/utils/phoneFormat'
import type { GridDto } from '@/proxy/form/models' import type { GridDto } from '@/proxy/form/models'
import type { RowMode } from '../../form/types'
import { getEditingFormFields } from '../Utils' import { getEditingFormFields } from '../Utils'
import { import {
applyCascadeChange,
attachCascadeBehavior,
type CascadeMaps,
hasCascadeBehavior,
isCascadeParentEmpty,
} from './cascade'
import {
getActiveEditingForm,
getValueByField, getValueByField,
resolveEditorButtons,
setFormEditorReadOnly, setFormEditorReadOnly,
shouldRunEditorScriptOnContentReady, shouldRunEditorScriptOnContentReady,
withButtonContext,
} from './editingForm' } from './editingForm'
type EditingFormHandlerParams = { type EditingFormHandlerParams = {
@ -199,18 +211,16 @@ type EditorValueChangedScriptParams = {
editorScript: string editorScript: string
fieldName?: string fieldName?: string
editingFormInstanceRef: MutableRefObject<any> editingFormInstanceRef: MutableRefObject<any>
getActiveEditingForm: (component: any) => any
} }
/** /**
* `onEditorPreparing` içinde editör script'ini `onValueChanged` zincirine ekler. * `onEditorPreparing` içinde editör script'ini `onValueChanged` zincirine ekler.
*/ */
export const attachEditorValueChangedScript = ({ const attachEditorValueChangedScript = ({
editor, editor,
editorScript, editorScript,
fieldName, fieldName,
editingFormInstanceRef, editingFormInstanceRef,
getActiveEditingForm,
}: EditorValueChangedScriptParams) => { }: EditorValueChangedScriptParams) => {
const previousHandler = editor.editorOptions.onValueChanged const previousHandler = editor.editorOptions.onValueChanged
const editorDataField = editor.dataField const editorDataField = editor.dataField
@ -245,22 +255,161 @@ export const attachEditorValueChangedScript = ({
} }
/** /**
* Editör butonlarının `onClick` çağrılarına satır bağlamını (formData, rowKey…) ekler. * Editör butonlarının `onClick` çağrılarına kayıt bağlamını (formData, fieldName, mode, rowKey…)
* ekler. Form sayfası aynı bağlamı `withButtonContext` ile verir.
*/ */
export const attachEditorButtonContext = (editor: any) => { const attachEditorButtonContext = (editor: any, mode: RowMode) => {
if (!editor.editorOptions?.buttons) return if (!editor.editorOptions?.buttons) return
editor.editorOptions.buttons = editor.editorOptions.buttons.map((btn: any) => { editor.editorOptions.buttons = withButtonContext(editor.editorOptions.buttons, () => {
if (typeof btn?.options?.onClick !== 'function') return btn
const originalClick = btn.options.onClick
btn.options.onClick = (e: any) => {
const grid = editor.component const grid = editor.component
const { rowKey, rowIndex } = getEditingRowIndex(grid) const { rowKey, rowIndex } = getEditingRowIndex(grid)
const formData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data || {} const formData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data || {}
return { formData, fieldName: editor.dataField, mode, rowKey, rowIndex }
originalClick({ ...e, formData, fieldName: editor.dataField, rowKey, rowIndex })
}
return btn
}) })
} }
type PrepareEditingEditorParams = {
editor: any
gridDto: GridDto
mode: RowMode
cascadeMaps: CascadeMaps
editingFormInstanceRef: MutableRefObject<any>
}
/**
* Grid ve Tree `onEditorPreparing` ortak gövdesi: ekranın editorOptions'ı, ekleme/düzenleme
* kilidi, cascade, telefon maskesi, editör script'i ve buton bağlamı.
*/
export const prepareEditingEditor = ({
editor,
gridDto,
mode,
cascadeMaps,
editingFormInstanceRef,
}: PrepareEditingEditorParams) => {
if (editor.parentType !== 'dataRow' || !editor.dataField) return
const columnFormat = gridDto.columnFormats.find((column) => column.fieldName === editor.dataField)
const isNewRow = Boolean(editor.row?.isNewRow) || mode === 'new'
if (columnFormat?.editorOptions) {
try {
const parsedEditorOptions = JSON.parse(columnFormat.editorOptions)
editor.editorOptions = {
...editor.editorOptions,
...parsedEditorOptions,
// Ham tanımdaki buton onClick'i metindir; form sayfasıyla aynı çözümden geçer.
...(parsedEditorOptions.buttons
? { buttons: resolveEditorButtons(parsedEditorOptions.buttons) }
: {}),
}
} catch (err) {
console.error('EditorOptions parse error:', columnFormat.fieldName, err)
}
}
const hasReadOnlyEditorOption = editor.editorOptions?.readOnly === true
const hasDisabledEditorOption = editor.editorOptions?.disabled === true
if (
(isNewRow && columnFormat?.allowAdding === false) ||
(!isNewRow && columnFormat?.allowEditing === false)
) {
editor.editorOptions.readOnly = true
} else if (isNewRow && columnFormat?.allowAdding === true) {
if (!hasReadOnlyEditorOption) editor.editorOptions.readOnly = false
if (!hasDisabledEditorOption) editor.editorOptions.disabled = false
}
if (hasCascadeBehavior(cascadeMaps, editor.dataField)) {
attachCascadeBehavior({ editor, gridDto, maps: cascadeMaps })
}
// phoneCountry maskesi ülke alanını bu olaydan izler; DataGrid formu fieldDataChanged yayınlamaz.
attachFieldValueBroadcast(editor)
// Yalnızca düzenleme formunda yer alan alanların script'i çalıştırılır.
if (columnFormat?.editOrderNo != null && columnFormat.editorScript) {
attachEditorValueChangedScript({
editor,
editorScript: columnFormat.editorScript,
fieldName: columnFormat.fieldName,
editingFormInstanceRef,
})
}
attachEditorButtonContext(editor, mode)
}
type CustomEditorCellAdapterParams = {
getGridDto: () => GridDto | undefined
getMode: () => RowMode
cascadeMaps: CascadeMaps
editingFormInstanceRef: MutableRefObject<any>
}
/**
* Platform editörleri (TagBox, GridBox, ImageUpload) DataGrid'de `editCellTemplate` ile çizilir;
* `onEditorPreparing` onlar için çalışmaz. Bu adaptör hücre bağlamına form sayfasındaki
* davranışı ekler: salt-okunur/devre dışı durumu, cascade ve editör script'i.
*/
export const createCustomEditorCellAdapter =
({ getGridDto, getMode, cascadeMaps, editingFormInstanceRef }: CustomEditorCellAdapterParams) =>
(cell: any) => {
const gridDto = getGridDto()
const fieldName: string | undefined = cell?.column?.dataField
if (!gridDto || !fieldName || !cell.setValue) return cell
const grid = cell.component
const columnFormat = gridDto.columnFormats.find((column) => column.fieldName === fieldName)
const isNewRow = Boolean(cell.row?.isNewRow) || getMode() === 'new'
const rowData: Record<string, any> = cell.row?.data ?? {}
const columnEditorOptions = cell.column?.extras?.editorOptions ?? {}
// Form item'ının editorOptions'ı yetki/URL kilidini ve script'in readOnly kararını taşır.
const form = editingFormInstanceRef.current ?? getActiveEditingForm(grid)
let itemEditorOptions: Record<string, any> | undefined
try {
itemEditorOptions = form?.itemOption?.(fieldName)?.editorOptions
} catch {
itemEditorOptions = undefined
}
const readOnly =
itemEditorOptions && 'readOnly' in itemEditorOptions
? itemEditorOptions.readOnly === true
: columnEditorOptions.readOnly === true ||
(isNewRow
? columnFormat?.canCreate === false || columnFormat?.allowAdding === false
: columnFormat?.canUpdate === false || columnFormat?.allowEditing === false)
const cascadeInfo = cascadeMaps.cascadeFieldsMap.get(fieldName)
const disabled =
columnEditorOptions.disabled === true ||
(!!cascadeInfo && isCascadeParentEmpty(cascadeInfo.parentFields, rowData))
const setValue = (value: any) => {
cell.setValue(value)
applyCascadeChange(grid, gridDto, cascadeMaps, fieldName, value)
if (columnFormat?.editOrderNo == null || !columnFormat.editorScript) return
const { rowIndex } = getEditingRowIndex(grid)
const activeForm = editingFormInstanceRef.current ?? getActiveEditingForm(grid)
try {
executeEditorScript(columnFormat.editorScript, {
formData: { ...rowData, [fieldName]: value },
e: { component: activeForm, dataField: fieldName, value, scriptEvent: 'valueChanged' },
editor: { dataField: fieldName, component: grid },
runtimeSetEditorReadOnly: (field: string, flag: boolean) =>
setFormEditorReadOnly(activeForm, field, flag),
setFormData: createGridCellWriter(grid, rowIndex),
})
} catch (err) {
console.error('Script exec error', fieldName, err)
}
}
return { ...cell, editorOptions: { ...cell.editorOptions, readOnly, disabled }, setValue }
}

View file

@ -33,7 +33,7 @@ export const normalizeImageSize = (value: unknown, fallback: number) => {
} }
/** Obje formatındaki görsel değerinden (url/src/fileUrl/path/value) kaynağı çıkarır. */ /** Obje formatındaki görsel değerinden (url/src/fileUrl/path/value) kaynağı çıkarır. */
export const getImageSource = (value: unknown) => { const getImageSource = (value: unknown) => {
if (!value) return '' if (!value) return ''
if (typeof value === 'string') return value.trim() if (typeof value === 'string') return value.trim()
@ -49,7 +49,7 @@ const isProbablyBase64Image = (value: string) =>
value.length > 80 && /^[A-Za-z0-9+/]+={0,2}$/.test(value) value.length > 80 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
/** Ham base64 içeriğini data URL'e çevirir; zaten URL ise dokunmaz. */ /** Ham base64 içeriğini data URL'e çevirir; zaten URL ise dokunmaz. */
export const toImageSource = (value: string) => { const toImageSource = (value: string) => {
const trimmed = value.trim() const trimmed = value.trim()
if (!trimmed) return '' if (!trimmed) return ''
if (trimmed.startsWith('data:image/') || !isProbablyBase64Image(trimmed)) return trimmed if (trimmed.startsWith('data:image/') || !isProbablyBase64Image(trimmed)) return trimmed

View file

@ -7,7 +7,7 @@ import type { GridDto } from '@/proxy/form/models'
import { workflowService } from '@/services/workflow.service' import { workflowService } from '@/services/workflow.service'
import { getPersistedInsertedKey } from './editingForm' import { getPersistedInsertedKey } from './editingForm'
export const showWorkflowMessages = (messages: string[] = []) => { const showWorkflowMessages = (messages: string[] = []) => {
if (!messages.length) return if (!messages.length) return
toast.push( toast.push(

View file

@ -18,6 +18,7 @@ import {
} from '@/proxy/form/models' } from '@/proxy/form/models'
import { addCss, getEditingFormFields, safeJsonParse } from './Utils' import { addCss, getEditingFormFields, safeJsonParse } from './Utils'
import { clearLookupCache, clearLookupCacheByPrefix } from '../shared/lookupCache' import { clearLookupCache, clearLookupCacheByPrefix } from '../shared/lookupCache'
import { createCascadeMaps } from './shared/cascade'
import { useLookupDataSource } from '../form/useLookupDataSource' import { useLookupDataSource } from '../form/useLookupDataSource'
import { import {
NO_IMAGE, NO_IMAGE,
@ -593,6 +594,7 @@ const useListFormColumns = ({
return columns return columns
} }
const { clearFieldsMap: cascadeClearFieldsMap } = createCascadeMaps(gridDto)
const workflowApprovalStatusField = const workflowApprovalStatusField =
gridDto.gridOptions?.workflowDto?.approvalStatusFieldName?.toLowerCase() gridDto.gridOptions?.workflowDto?.approvalStatusFieldName?.toLowerCase()
columnFormats.forEach((colData) => { columnFormats.forEach((colData) => {
@ -748,15 +750,15 @@ const useListFormColumns = ({
dataSource: (o) => lookupDataSource(o, colData), dataSource: (o) => lookupDataSource(o, colData),
} }
//cascadeEmptyFields verisi dolu ise bu kolon/field bir parent field dir // Değeri değişince boşaltılacak alanlar (çocuklar + CascadeEmptyFields); popup ve
if (colData.lookupDto.cascadeEmptyFields) { // satır içi düzenleme form sayfasıyla aynı kuralı izler.
// parent field guncellendigi zaman bu fonksiyon cagrilir const cascadeClearFields = cascadeClearFieldsMap.get(colData.fieldName)
if (cascadeClearFields?.length) {
column.setCellValue = function (rowData: any, value: any) { column.setCellValue = function (rowData: any, value: any) {
if (!colData.fieldName) return if (!colData.fieldName) return
//console.log({ rowData, value, colData }) rowData[colData.fieldName] =
rowData[colData.fieldName] = Array.isArray(value) ? value[0] : value Array.isArray(value) && !column.extras?.multiValue ? value[0] : value
// cascadeEmptyFields alani aralarinda virgul olacak sekilde bosaltilmak istenen alanlari saklar cascadeClearFields.forEach((emptyField) => {
colData?.lookupDto?.cascadeEmptyFields?.split(',').forEach((emptyField: any) => {
rowData[emptyField] = null rowData[emptyField] = null
}) })
} }

View file

@ -154,7 +154,7 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
// ─── MenuAddDialog ──────────────────────────────────────────────────────────── // ─── MenuAddDialog ────────────────────────────────────────────────────────────
export interface MenuAddDialogProps { interface MenuAddDialogProps {
isOpen: boolean isOpen: boolean
onClose: () => void onClose: () => void
initialParentCode: string initialParentCode: string

View file

@ -17,8 +17,6 @@ type SharedGridBoxEditorProps = {
editorOptions?: any editorOptions?: any
dropDownOptions?: any dropDownOptions?: any
onValueChanged?: (value: any) => void onValueChanged?: (value: any) => void
onCommit?: (value: any) => void
setInitialValueOnContentReady?: boolean
} }
const normalizeValue = (value: any) => (Array.isArray(value) ? value : [value]) const normalizeValue = (value: any) => (Array.isArray(value) ? value : [value])
@ -46,18 +44,11 @@ const SharedGridBoxEditor = ({
editorOptions, editorOptions,
dropDownOptions, dropDownOptions,
onValueChanged, onValueChanged,
onCommit,
setInitialValueOnContentReady,
}: SharedGridBoxEditorProps): ReactElement => { }: SharedGridBoxEditorProps): ReactElement => {
const gridRef = useRef<DataGridRef>(null) const gridRef = useRef<DataGridRef>(null)
const currentValueRef = useRef<any>(normalizeValue(value)) const currentValueRef = useRef<any>(normalizeValue(value))
currentValueRef.current = normalizeValue(value) currentValueRef.current = normalizeValue(value)
const commitValue = (nextValue: any) => {
currentValueRef.current = nextValue
onCommit?.(nextValue)
}
return ( return (
<DropDownBox <DropDownBox
{...editorOptions} {...editorOptions}
@ -69,9 +60,7 @@ const SharedGridBoxEditor = ({
showClearButton={options?.showClearButton} showClearButton={options?.showClearButton}
acceptCustomValue={options?.acceptCustomValue} acceptCustomValue={options?.acceptCustomValue}
onContentReady={(e) => { onContentReady={(e) => {
if (setInitialValueOnContentReady) {
e.component.option('value', currentValueRef.current) e.component.option('value', currentValueRef.current)
}
editorOptions?.onContentReady?.(e) editorOptions?.onContentReady?.(e)
}} }}
@ -81,7 +70,6 @@ const SharedGridBoxEditor = ({
onValueChanged?.(nextValue) onValueChanged?.(nextValue)
if (e.value === null) { if (e.value === null) {
commitValue(null)
e.component.close() e.component.close()
gridRef.current?.instance().deselectAll() gridRef.current?.instance().deselectAll()
} }
@ -106,7 +94,6 @@ const SharedGridBoxEditor = ({
if (options?.selectionMode === 'single') { if (options?.selectionMode === 'single') {
const nextValue = event.data.key const nextValue = event.data.key
dropDown.component.option('value', nextValue) dropDown.component.option('value', nextValue)
commitValue(nextValue)
dropDown.component.close() dropDown.component.close()
} }
}} }}
@ -138,7 +125,6 @@ const SharedGridBoxEditor = ({
.map((item: any) => item.key) .map((item: any) => item.key)
dropDown.component.option('value', selectedValue) dropDown.component.option('value', selectedValue)
commitValue(selectedValue)
dropDown.component.close() dropDown.component.close()
}} }}
/> />

View file

@ -23,7 +23,7 @@ const headerValues = [false, 1, 2, 3, 4, 5, 6]
type ToolbarItem = NonNullable<NonNullable<HtmlEditorProperties['toolbar']>['items']>[number] type ToolbarItem = NonNullable<NonNullable<HtmlEditorProperties['toolbar']>['items']>[number]
export const htmlToolbarItemNames = [ const htmlToolbarItemNames = [
'undo', 'undo',
'redo', 'redo',
'separator', 'separator',

View file

@ -18,7 +18,7 @@ export const findLookupColor = (lookup: any, value: any): string | undefined =>
} }
/** Açık zeminde beyaz yazı okunmadığı için yazı rengi zeminin parlaklığına göre seçilir. */ /** Açık zeminde beyaz yazı okunmadığı için yazı rengi zeminin parlaklığına göre seçilir. */
export const readableTextColor = (color: string): string => { const readableTextColor = (color: string): string => {
const hex = color.replace('#', '') const hex = color.replace('#', '')
const full = const full =
hex.length === 3 hex.length === 3