Grid, Tree, FormDevexpress komponentlerin yetenekleri eşitlendi
This commit is contained in:
parent
7e5322d831
commit
3922c46539
30 changed files with 784 additions and 804 deletions
25
.github/instructions/lowcode.instructions.md
vendored
25
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -1490,9 +1490,28 @@ cascade eksik yetenek değildir ve analizde `Ready to Start`'ı engellemez. Tam
|
|||
| `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 |
|
||||
|
||||
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
|
||||
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'ı
|
||||
`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/*` |
|
||||
| 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}` |
|
||||
| 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. |
|
||||
|
||||
#### 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` →
|
||||
`{ 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,
|
||||
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
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -446,9 +446,13 @@ ve atanan kullanıcı alanlarından kurulur.
|
|||
`dxDropDownBox`, `dxGridBox`, `dxAutocomplete`, `dxColorBox`, `dxHtmlEditor`, `dxSlider`,
|
||||
`dxRangeSlider` — ayrıca görsel yükleme/görüntüleme editörleri.
|
||||
- **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
|
||||
`{"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.
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
`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.
|
||||
- **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
|
||||
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ç
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -19,7 +19,7 @@ import FormFieldOrderPanel from './FormFieldOrderPanel'
|
|||
import type { FormFieldOrder } from './shared/formItems'
|
||||
|
||||
/** Form sayfasının kullanıcı bazlı alan sırası (`useGridData`). */
|
||||
export type FormFieldOrderControl = {
|
||||
type FormFieldOrderControl = {
|
||||
value?: FormFieldOrder
|
||||
preview: (order: FormFieldOrder | undefined) => void
|
||||
save: (order: FormFieldOrder | undefined) => Promise<void>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import {
|
|||
setFormEditorReadOnly,
|
||||
shouldRunEditorScriptOnContentReady,
|
||||
translateCaption,
|
||||
withButtonContext,
|
||||
} from '../list/shared/editingForm'
|
||||
import { getCascadeClearFields, isCascadeParentEmpty } from '../list/shared/cascade'
|
||||
import { isTouchLikeDevice } from '../list/shared/editPopup'
|
||||
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ışı. */
|
||||
const handleCustomEditorChange = (formItem: SimpleItemWithColData, value: any) => {
|
||||
if (!formItem.dataField) 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)
|
||||
runEditorScript(formItem, value)
|
||||
}
|
||||
|
|
@ -182,9 +200,6 @@ const FormDevExpress = (props: {
|
|||
...(index !== undefined && mode !== 'view' && !isTouchLikeDevice()
|
||||
? { autoFocus: index === 1 }
|
||||
: {}),
|
||||
...(formItem.editorType === 'dxDateBox'
|
||||
? { useMaskBehavior: true, openOnFieldClick: true, showClearButton: true }
|
||||
: {}),
|
||||
...(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
|
||||
? {
|
||||
buttons: configuredButtons.map((button: any) => {
|
||||
// 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,
|
||||
fieldName: formItem.dataField,
|
||||
mode,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}),
|
||||
buttons: withButtonContext(configuredButtons, () => ({
|
||||
formData: formDataRef.current,
|
||||
fieldName: formItem.dataField,
|
||||
mode,
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
|
|
@ -266,7 +268,7 @@ const FormDevExpress = (props: {
|
|||
editor.option(
|
||||
'disabled',
|
||||
item.editorOptions?.disabled === true ||
|
||||
parentFields.some((parentField) => !formDataRef.current?.[parentField]),
|
||||
isCascadeParentEmpty(parentFields, formDataRef.current),
|
||||
)
|
||||
} catch (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. */
|
||||
const renderCustomEditor = (formItem: SimpleItemWithColData): ReactNode => {
|
||||
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)
|
||||
|
||||
switch (formItem.editorType2) {
|
||||
|
|
@ -359,7 +369,6 @@ const FormDevExpress = (props: {
|
|||
<TagBoxEditorComponent
|
||||
value={value || EMPTY_VALUES}
|
||||
setDefaultValue={false}
|
||||
values={formData}
|
||||
options={formItem.tagBoxOptions}
|
||||
col={formItem.colData}
|
||||
onValueChanged={onChange}
|
||||
|
|
@ -370,7 +379,6 @@ const FormDevExpress = (props: {
|
|||
return (
|
||||
<GridBoxEditorComponent
|
||||
value={value || EMPTY_VALUES}
|
||||
values={formData}
|
||||
options={formItem.gridBoxOptions}
|
||||
col={formItem.colData}
|
||||
onValueChanged={onChange}
|
||||
|
|
@ -477,14 +485,8 @@ const FormDevExpress = (props: {
|
|||
// 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.
|
||||
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.
|
||||
flattenFormGroups(formItemsRef.current)
|
||||
.filter((item) => getCascadeParentFields(item).includes(e.dataField!))
|
||||
.forEach((child) => {
|
||||
newFormData[child.dataField!] = null
|
||||
})
|
||||
const newFormData = withCascadeCleared({ ...formData, [e.dataField]: e.value }, e.dataField)
|
||||
|
||||
formDataSnapshotRef.current = { ...newFormData }
|
||||
formDataRef.current = newFormData
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { ColumnFormatDto, GridBoxOptionsDto } from '@/proxy/form/models'
|
|||
import { ReactElement } from 'react'
|
||||
import { SharedGridBoxEditor } from '@/views/shared/SharedGridBoxEditor'
|
||||
|
||||
/** Form sayfası ve liste düzenleme hücresinin ortak GridBox editörü. */
|
||||
const GridBoxEditorComponent = ({
|
||||
value,
|
||||
options,
|
||||
|
|
@ -11,7 +12,6 @@ const GridBoxEditorComponent = ({
|
|||
}: {
|
||||
value: any
|
||||
options?: GridBoxOptionsDto
|
||||
values: any // all row data
|
||||
onValueChanged: (e: any) => void
|
||||
col?: ColumnFormatDto
|
||||
editorOptions: any
|
||||
|
|
@ -22,9 +22,13 @@ const GridBoxEditorComponent = ({
|
|||
<SharedGridBoxEditor
|
||||
value={value}
|
||||
dataSource={editorOptions?.dataSource}
|
||||
lookup={lookupDto}
|
||||
lookup={{
|
||||
valueExpr: lookupDto?.valueExpr?.toLowerCase(),
|
||||
displayExpr: lookupDto?.displayExpr?.toLowerCase(),
|
||||
}}
|
||||
options={options}
|
||||
editorOptions={editorOptions}
|
||||
dropDownOptions={{ width: options?.width, hideOnOutsideClick: true }}
|
||||
onValueChanged={onValueChanged}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,51 @@ import { normalizeImageSize } from '../../list/shared/imageValue'
|
|||
import Input from '@/components/ui/Input'
|
||||
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 = ({
|
||||
value,
|
||||
options,
|
||||
|
|
@ -25,7 +70,7 @@ const ImageUploadEditorComponent = ({
|
|||
editorOptions?: any
|
||||
}): ReactElement => {
|
||||
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
|
||||
const resolvedOptions: ImageUploadOptionsDto = options ?? editorOptions ?? {}
|
||||
|
|
@ -40,10 +85,7 @@ const ImageUploadEditorComponent = ({
|
|||
// aksi halde dokunmatik cihazda dosya seciciye ek olarak kamera sunulur.
|
||||
const showCameraButton = !resolvedOptions.capture && supportsPhotoCapture()
|
||||
|
||||
const valueUrls = useMemo<string[]>(
|
||||
() => (Array.isArray(value) ? value : [value]).filter(Boolean) as string[],
|
||||
[value],
|
||||
)
|
||||
const valueUrls = useMemo<string[]>(() => normalizeUploadValue(value), [value])
|
||||
|
||||
const [urls, setUrls] = useState<string[]>(valueUrls)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
|
@ -63,14 +105,6 @@ const ImageUploadEditorComponent = ({
|
|||
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) => {
|
||||
if (!files?.length) return
|
||||
|
||||
|
|
@ -87,7 +121,7 @@ const ImageUploadEditorComponent = ({
|
|||
const res = await fetch(resolvedOptions.uploadUrl, { method: 'POST', body: formData })
|
||||
if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
|
||||
const data = await res.json()
|
||||
uploadedUrls.push(String(data?.url ?? data?.fileUrl ?? data?.path ?? data ?? '').trim())
|
||||
uploadedUrls.push(resolveUploadedUrl(data))
|
||||
} else {
|
||||
// uploadUrl yoksa base64 olarak sakla
|
||||
const base64 = await toBase64(file)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ const TagBoxEditorComponent = ({
|
|||
value: any
|
||||
setDefaultValue: boolean
|
||||
options?: TagBoxOptionsDto
|
||||
values: any // all row data
|
||||
onValueChanged: (e: any) => void
|
||||
col?: ColumnFormatDto
|
||||
editorOptions: any
|
||||
|
|
@ -27,11 +26,6 @@ const TagBoxEditorComponent = ({
|
|||
[lookupDto?.displayExpr],
|
||||
)
|
||||
|
||||
// const dataSource =
|
||||
// typeof lookup?.dataSource === 'function'
|
||||
// ? lookup.dataSource(values)
|
||||
// : (lookup?.dataSource ?? [])
|
||||
|
||||
return (
|
||||
<TagBox
|
||||
{...editorOptions}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,18 @@ import type { GroupItem } from 'devextreme/ui/form'
|
|||
import type { ColumnFormatDto, GridDto } from '@/proxy/form/models'
|
||||
import { getEditingFormGroups } from '../../list/Utils'
|
||||
import {
|
||||
getLookupEditorDefaults,
|
||||
isFormItemVisible,
|
||||
resolveEditorButtons,
|
||||
resolveEditorType,
|
||||
withEditorTypeDefaults,
|
||||
} from '../../list/shared/editingForm'
|
||||
import { getCascadeParentFieldsOf } from '../../list/shared/cascade'
|
||||
import type { GridColumnData } from '../../list/GridColumnData'
|
||||
import type { RowMode, SimpleItemWithColData } from '../types'
|
||||
|
||||
/** 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) => [
|
||||
...(item?.dataField ? [item] : []),
|
||||
...flattenFormItems(item?.items || []),
|
||||
|
|
@ -30,10 +33,7 @@ export const flattenFormGroups = (groups: GroupItem[] = []): SimpleItemWithColDa
|
|||
|
||||
/** Bir alanın cascade parent alan adlarını döner. */
|
||||
export const getCascadeParentFields = (item: SimpleItemWithColData): string[] =>
|
||||
item.colData?.lookupDto?.cascadeParentFields
|
||||
?.split(',')
|
||||
.map((field: string) => field.trim())
|
||||
.filter(Boolean) ?? []
|
||||
getCascadeParentFieldsOf(item.colData)
|
||||
|
||||
type BuildFormItemsParams = {
|
||||
gridDto: GridDto
|
||||
|
|
@ -77,16 +77,18 @@ const toFormItem = (
|
|||
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
|
||||
? {
|
||||
...getLookupEditorDefaults(colFormat),
|
||||
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 = {
|
||||
canRead: colFormat.canRead ?? false,
|
||||
canUpdate: colFormat.canUpdate ?? false,
|
||||
|
|
@ -99,10 +101,11 @@ const toFormItem = (
|
|||
editorType2: colFormat.editorType2,
|
||||
editorType: resolveEditorType(colFormat.editorType2),
|
||||
colSpan: colFormat.colSpan,
|
||||
editorOptions: withEditorTypeDefaults(colFormat.editorType2, {
|
||||
...lookupEditorOptions,
|
||||
...parsedEditorOptions,
|
||||
}),
|
||||
editorOptions: withEditorTypeDefaults(
|
||||
colFormat.editorType2,
|
||||
{ ...lookupEditorOptions, ...parsedEditorOptions },
|
||||
colFormat.sourceDbType,
|
||||
),
|
||||
// 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.
|
||||
colData: structuredClone(colFormat),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const isEmptyValue = (value: unknown) => value === undefined || value === null |
|
|||
* 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.
|
||||
*/
|
||||
export const buildCascadeFilters = (lookupDto: any, rowData: any): any[] | null => {
|
||||
const buildCascadeFilters = (lookupDto: any, rowData: any): any[] | null => {
|
||||
const parentFields = splitFields(lookupDto?.cascadeParentFields)
|
||||
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]))
|
||||
}
|
||||
|
||||
export const createLookupStaticDataSource = (
|
||||
const createLookupStaticDataSource = (
|
||||
load: () => any,
|
||||
filter: any[] | null,
|
||||
cacheKey: string,
|
||||
|
|
@ -65,7 +65,7 @@ export const createLookupStaticDataSource = (
|
|||
filter,
|
||||
})
|
||||
|
||||
export const createLookupQueryDataSource = (
|
||||
const createLookupQueryDataSource = (
|
||||
listFormCode?: string,
|
||||
listFormFieldName?: string,
|
||||
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. */
|
||||
const toJsonText = (value: unknown) => (value === null ? 'null' : JSON.stringify(String(value)).slice(1, -1))
|
||||
|
||||
export const createLookupApiDataSource = (
|
||||
const createLookupApiDataSource = (
|
||||
listFormCode?: string,
|
||||
lookupQuery?: string,
|
||||
filters?: any[] | null,
|
||||
|
|
|
|||
|
|
@ -41,8 +41,7 @@ import { NotePanel } from '../form/notes/NotePanel'
|
|||
import SubForms from '../form/SubForms'
|
||||
import GridFilterDialogs from './GridFilterDialogs'
|
||||
import { GridExtraFilterToolbar } from './GridExtraFilterToolbar'
|
||||
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
||||
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
|
||||
import { ImageUploadEditorComponent, ImageViewerEditorComponent } from './editors/cellEditors'
|
||||
import {
|
||||
autoNumber,
|
||||
extractSearchParamsFields,
|
||||
|
|
|
|||
|
|
@ -37,10 +37,7 @@ import SubForms from '../form/SubForms'
|
|||
import { RowMode } from '../form/types'
|
||||
import { GridColumnData } from './GridColumnData'
|
||||
import GridFilterDialogs from './GridFilterDialogs'
|
||||
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
||||
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
||||
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
|
||||
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
||||
import { GridBoxEditorComponent, ImageUploadEditorComponent, ImageViewerEditorComponent, TagBoxEditorComponent } from './editors/cellEditors'
|
||||
import { useFilters } from './useFilters'
|
||||
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
|
||||
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
|
||||
|
|
@ -63,16 +60,14 @@ import {
|
|||
buildUpdatePayload,
|
||||
createFormItemMapper,
|
||||
expandJsonFormFields,
|
||||
getActiveEditingForm,
|
||||
getValueByField,
|
||||
} from './shared/editingForm'
|
||||
import {
|
||||
attachEditorButtonContext,
|
||||
attachEditorValueChangedScript,
|
||||
createCustomEditorCellAdapter,
|
||||
createEditingFormHandlers,
|
||||
prepareEditingEditor,
|
||||
} from './shared/editingFormScripts'
|
||||
import { attachFieldValueBroadcast } from '@/utils/phoneFormat'
|
||||
import { attachCascadeBehavior, createCascadeMaps } from './shared/cascade'
|
||||
import { createCascadeMaps } from './shared/cascade'
|
||||
import { createConditionalCellStyleHandler } from './shared/cellStyles'
|
||||
import {
|
||||
useExtraFilters,
|
||||
|
|
@ -343,61 +338,27 @@ const Grid = (props: GridProps) => {
|
|||
|
||||
const onEditorPreparing = useCallback(
|
||||
(editor: DataGridTypes.EditorPreparingEvent<any, any>) => {
|
||||
if (editor.parentType !== 'dataRow' || !editor.dataField || !gridDto) return
|
||||
|
||||
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)
|
||||
if (!gridDto) return
|
||||
prepareEditingEditor({ editor, gridDto, mode, cascadeMaps, editingFormInstanceRef })
|
||||
},
|
||||
[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
|
||||
======================= */
|
||||
|
|
@ -665,9 +626,18 @@ const Grid = (props: GridProps) => {
|
|||
items: editingFormItems,
|
||||
}}
|
||||
/>
|
||||
<Template name="cellEditTagBox" render={TagBoxEditorComponent} />
|
||||
<Template name="cellEditGridBox" render={GridBoxEditorComponent} />
|
||||
<Template name="cellEditImageUpload" render={ImageUploadEditorComponent} />
|
||||
<Template
|
||||
name="cellEditTagBox"
|
||||
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
|
||||
name="cellEditImageViewer"
|
||||
render={(data: any) => (
|
||||
|
|
|
|||
|
|
@ -31,10 +31,7 @@ import PageTitle from '@/components/shared/PageTitle'
|
|||
import { RowMode } from '../form/types'
|
||||
import { GridColumnData } from './GridColumnData'
|
||||
import GridFilterDialogs from './GridFilterDialogs'
|
||||
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
||||
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
||||
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
|
||||
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
||||
import { GridBoxEditorComponent, ImageUploadEditorComponent, ImageViewerEditorComponent, TagBoxEditorComponent } from './editors/cellEditors'
|
||||
import { useFilters } from './useFilters'
|
||||
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
|
||||
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
|
||||
|
|
@ -59,16 +56,14 @@ import {
|
|||
buildUpdatePayload,
|
||||
createFormItemMapper,
|
||||
expandJsonFormFields,
|
||||
getActiveEditingForm,
|
||||
getValueByField,
|
||||
} from './shared/editingForm'
|
||||
import {
|
||||
attachEditorButtonContext,
|
||||
attachEditorValueChangedScript,
|
||||
createCustomEditorCellAdapter,
|
||||
createEditingFormHandlers,
|
||||
prepareEditingEditor,
|
||||
} from './shared/editingFormScripts'
|
||||
import { attachFieldValueBroadcast } from '@/utils/phoneFormat'
|
||||
import { attachCascadeBehavior, createCascadeMaps } from './shared/cascade'
|
||||
import { createCascadeMaps } from './shared/cascade'
|
||||
import { createConditionalCellStyleHandler } from './shared/cellStyles'
|
||||
import {
|
||||
useExtraFilters,
|
||||
|
|
@ -354,60 +349,27 @@ const Tree = (props: TreeProps) => {
|
|||
|
||||
const onEditorPreparing = useCallback(
|
||||
(editor: TreeListTypes.EditorPreparingEvent) => {
|
||||
if (editor.parentType !== 'dataRow' || !editor.dataField || !gridDto) return
|
||||
|
||||
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)
|
||||
if (!gridDto) return
|
||||
prepareEditingEditor({ editor, gridDto, mode, cascadeMaps, editingFormInstanceRef })
|
||||
},
|
||||
[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
|
||||
======================= */
|
||||
|
|
@ -653,9 +615,18 @@ const Tree = (props: TreeProps) => {
|
|||
items: editingFormItems,
|
||||
}}
|
||||
/>
|
||||
<Template name="cellEditTagBox" render={TagBoxEditorComponent} />
|
||||
<Template name="cellEditGridBox" render={GridBoxEditorComponent} />
|
||||
<Template name="cellEditImageUpload" render={ImageUploadEditorComponent} />
|
||||
<Template
|
||||
name="cellEditTagBox"
|
||||
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
|
||||
name="cellEditImageViewer"
|
||||
render={(data: any) => (
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
172
ui/src/views/list/editors/cellEditors.tsx
Normal file
172
ui/src/views/list/editors/cellEditors.tsx
Normal 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,
|
||||
}
|
||||
|
|
@ -1,19 +1,21 @@
|
|||
/**
|
||||
* 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[]
|
||||
childFields?: string[]
|
||||
}
|
||||
|
||||
export type CascadeMaps = {
|
||||
/** fieldName -> cascade bilgisi */
|
||||
/** child fieldName -> cascade bilgisi */
|
||||
cascadeFieldsMap: Map<string, CascadeInfo>
|
||||
/** parentField -> etkilediği child alan adları */
|
||||
parentToChildrenMap: Map<string, Set<string>>
|
||||
/** fieldName -> değişince boşaltılacak alanlar */
|
||||
clearFieldsMap: Map<string, string[]>
|
||||
}
|
||||
|
||||
const splitFields = (value?: string | null) =>
|
||||
|
|
@ -22,103 +24,182 @@ const splitFields = (value?: string | null) =>
|
|||
.map((field) => field.trim())
|
||||
.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 => {
|
||||
const cascadeFieldsMap = new Map<string, CascadeInfo>()
|
||||
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) => {
|
||||
if (!col.fieldName || !col.lookupDto?.cascadeParentFields) return
|
||||
const fieldName = col.fieldName
|
||||
if (!fieldName) return
|
||||
|
||||
const parentFields = splitFields(col.lookupDto.cascadeParentFields)
|
||||
const childFields = col.lookupDto.cascadeEmptyFields
|
||||
? splitFields(col.lookupDto.cascadeEmptyFields)
|
||||
: undefined
|
||||
splitFields(col.lookupDto?.cascadeEmptyFields).forEach((field) =>
|
||||
addClearField(fieldName, field),
|
||||
)
|
||||
|
||||
cascadeFieldsMap.set(col.fieldName, { parentFields, childFields })
|
||||
const parentFields = getCascadeParentFieldsOf(col)
|
||||
if (!parentFields.length) return
|
||||
|
||||
cascadeFieldsMap.set(fieldName, { parentFields })
|
||||
parentFields.forEach((parentField) => {
|
||||
const children = parentToChildrenMap.get(parentField) ?? new Set<string>()
|
||||
children.add(col.fieldName!)
|
||||
children.add(fieldName)
|
||||
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 }
|
||||
}
|
||||
|
||||
/** Alan cascade zincirinde (ebeveyn ya da çocuk olarak) yer alıyor mu? */
|
||||
export const hasCascadeBehavior = (maps: CascadeMaps, fieldName: string) =>
|
||||
maps.cascadeFieldsMap.has(fieldName) ||
|
||||
maps.parentToChildrenMap.has(fieldName) ||
|
||||
maps.clearFieldsMap.has(fieldName)
|
||||
|
||||
const getEditingRow = (grid: any) => {
|
||||
const rowKey = grid.option('editing.editRowKey')
|
||||
return {
|
||||
rowIndex: grid.getRowIndexByKey(rowKey) as number,
|
||||
rowData: grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data as
|
||||
| Record<string, any>
|
||||
| undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
||||
for (const childFieldName of children) {
|
||||
const childInfo = maps.cascadeFieldsMap.get(childFieldName)
|
||||
if (!childInfo) continue
|
||||
|
||||
try {
|
||||
const childColumn = gridDto.columnFormats.find(
|
||||
(column) => column.fieldName === childFieldName,
|
||||
)
|
||||
const childEditorOptions = childColumn?.editorOptions
|
||||
? JSON.parse(childColumn.editorOptions)
|
||||
: {}
|
||||
|
||||
form
|
||||
.getEditor(childFieldName)
|
||||
?.option(
|
||||
'disabled',
|
||||
childEditorOptions?.disabled === true ||
|
||||
isCascadeParentEmpty(childInfo.parentFields, rowData),
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(`Cascade child editor güncellenemedi: "${childFieldName}"`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bir alanın değeri değiştiğinde cascade kurallarını düzenlenen satıra uygular:
|
||||
* bağımlı alanları boşaltır ve çocuk editörlerin kilidini yeniler. Grid editörleri
|
||||
* (`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
|
||||
|
||||
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
|
||||
cascadeInfo: CascadeInfo
|
||||
maps: CascadeMaps
|
||||
}
|
||||
|
||||
/**
|
||||
* `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,
|
||||
* - 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,
|
||||
cascadeInfo,
|
||||
maps,
|
||||
}: AttachCascadeParams) => {
|
||||
const { cascadeFieldsMap, parentToChildrenMap } = maps
|
||||
const affectedChildren = parentToChildrenMap.get(editor.dataField)
|
||||
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)
|
||||
|
||||
const grid = editor.component
|
||||
const rowKey = grid.option('editing.editRowKey')
|
||||
const rowIndex = grid.getRowIndexByKey(rowKey)
|
||||
|
||||
// Parent değiştiğinde bağımlı alanları temizle.
|
||||
if (cascadeInfo.childFields && rowIndex >= 0) {
|
||||
cascadeInfo.childFields.forEach((childField) => grid.cellValue(rowIndex, childField, null))
|
||||
}
|
||||
|
||||
if (!affectedChildren?.size) return
|
||||
|
||||
const formInstance = grid.option('editing.form') as any
|
||||
if (!formInstance?.getEditor) return
|
||||
|
||||
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 {
|
||||
const childColumn = gridDto.columnFormats.find(
|
||||
(column) => column.fieldName === childFieldName,
|
||||
)
|
||||
const childEditorOptions = childColumn?.editorOptions
|
||||
? JSON.parse(childColumn.editorOptions)
|
||||
: {}
|
||||
const childEditor = formInstance.getEditor(childFieldName)
|
||||
|
||||
childEditor?.option(
|
||||
'disabled',
|
||||
childEditorOptions?.disabled === true ||
|
||||
childInfo.parentFields.some((parentField) => !rowData[parentField]),
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(`Cascade child editor güncellenemedi: "${childFieldName}"`, err)
|
||||
}
|
||||
})
|
||||
applyCascadeChange(editor.component, gridDto, maps, fieldName, e.value)
|
||||
}
|
||||
|
||||
// İlk açılışta parent'ı dolu değilse editörü kilitle.
|
||||
const grid = editor.component
|
||||
const rowKey = grid.option('editing.editRowKey')
|
||||
const rowData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data
|
||||
|
||||
if (rowData && cascadeInfo.parentFields.some((parentField) => !rowData[parentField])) {
|
||||
const cascadeInfo = maps.cascadeFieldsMap.get(fieldName)
|
||||
const { rowData } = getEditingRow(editor.component)
|
||||
if (cascadeInfo && rowData && isCascadeParentEmpty(cascadeInfo.parentFields, rowData)) {
|
||||
editor.editorOptions.disabled = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const isTouchLikeDevice = () =>
|
|||
typeof window !== 'undefined' &&
|
||||
(window.matchMedia?.('(pointer: coarse)').matches || window.matchMedia?.('(hover: none)').matches)
|
||||
|
||||
export const isMobileViewport = () =>
|
||||
const isMobileViewport = () =>
|
||||
typeof window !== 'undefined' && window.matchMedia?.('(max-width: 767px)').matches
|
||||
|
||||
/** Popup'ın mobil davranış kurallarıyla açılıp açılmayacağını belirler. */
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import {
|
|||
setFormEditingExtraItemValues,
|
||||
} 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
|
||||
|
|
@ -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ı
|
||||
* ------------------------------------------------------------------ */
|
||||
|
|
@ -400,6 +435,9 @@ const dateTimeEditorDefaults = {
|
|||
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. */
|
||||
const getDateEditorDefaults = (sourceDbType?: DbTypeEnum) => {
|
||||
if (sourceDbType === DbTypeEnum.Date) return dateEditorDefaults
|
||||
|
|
@ -429,10 +467,26 @@ export const resolveEditorType = (editorType2?: EditorType2) => {
|
|||
export const withEditorTypeDefaults = <T extends Record<string, any>>(
|
||||
editorType2: EditorType2 | undefined,
|
||||
editorOptions: T,
|
||||
): T =>
|
||||
editorType2 === 'dxHtmlEditor'
|
||||
? mergeHtmlEditorOptions(editorOptions)
|
||||
: withPhoneEditorOptions(editorOptions)
|
||||
sourceDbType?: DbTypeEnum,
|
||||
): T => {
|
||||
if (editorType2 === 'dxHtmlEditor') return mergeHtmlEditorOptions(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 = {
|
||||
mode: RowMode
|
||||
|
|
@ -477,9 +531,7 @@ export const createFormItemMapper = ({
|
|||
console.error('EditorOptions parse error:', fieldName, err)
|
||||
}
|
||||
|
||||
const defaultEditorOptions: EditorOptionsWithButtons = {
|
||||
...(getDateEditorDefaults(colFormat.sourceDbType) ?? {}),
|
||||
}
|
||||
const defaultEditorOptions: EditorOptionsWithButtons = getLookupEditorDefaults(colFormat)
|
||||
|
||||
if (colFormat.placeHolder) {
|
||||
defaultEditorOptions.placeholder = translate('::' + colFormat.placeHolder)
|
||||
|
|
@ -495,22 +547,14 @@ export const createFormItemMapper = ({
|
|||
? { readOnly: true }
|
||||
: {}
|
||||
|
||||
const editorOptions: EditorOptionsWithButtons = withEditorTypeDefaults(colFormat.editorType2, {
|
||||
...defaultEditorOptions,
|
||||
...parsedEditorOptions,
|
||||
...forcedEditorOptions,
|
||||
})
|
||||
const editorOptions: EditorOptionsWithButtons = withEditorTypeDefaults(
|
||||
colFormat.editorType2,
|
||||
{ ...defaultEditorOptions, ...parsedEditorOptions, ...forcedEditorOptions },
|
||||
colFormat.sourceDbType,
|
||||
)
|
||||
|
||||
if (editorOptions.buttons) {
|
||||
editorOptions.buttons = editorOptions.buttons.map((btn: any) => {
|
||||
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
|
||||
})
|
||||
editorOptions.buttons = resolveEditorButtons(editorOptions.buttons)
|
||||
}
|
||||
|
||||
const item: SimpleItemWithColData = {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,24 @@
|
|||
*/
|
||||
import type { MutableRefObject } from 'react'
|
||||
import { executeEditorScript } from '@/utils/editorScriptRuntime'
|
||||
import { attachFieldValueBroadcast } from '@/utils/phoneFormat'
|
||||
import type { GridDto } from '@/proxy/form/models'
|
||||
import type { RowMode } from '../../form/types'
|
||||
import { getEditingFormFields } from '../Utils'
|
||||
import {
|
||||
applyCascadeChange,
|
||||
attachCascadeBehavior,
|
||||
type CascadeMaps,
|
||||
hasCascadeBehavior,
|
||||
isCascadeParentEmpty,
|
||||
} from './cascade'
|
||||
import {
|
||||
getActiveEditingForm,
|
||||
getValueByField,
|
||||
resolveEditorButtons,
|
||||
setFormEditorReadOnly,
|
||||
shouldRunEditorScriptOnContentReady,
|
||||
withButtonContext,
|
||||
} from './editingForm'
|
||||
|
||||
type EditingFormHandlerParams = {
|
||||
|
|
@ -199,18 +211,16 @@ type EditorValueChangedScriptParams = {
|
|||
editorScript: string
|
||||
fieldName?: string
|
||||
editingFormInstanceRef: MutableRefObject<any>
|
||||
getActiveEditingForm: (component: any) => any
|
||||
}
|
||||
|
||||
/**
|
||||
* `onEditorPreparing` içinde editör script'ini `onValueChanged` zincirine ekler.
|
||||
*/
|
||||
export const attachEditorValueChangedScript = ({
|
||||
const attachEditorValueChangedScript = ({
|
||||
editor,
|
||||
editorScript,
|
||||
fieldName,
|
||||
editingFormInstanceRef,
|
||||
getActiveEditingForm,
|
||||
}: EditorValueChangedScriptParams) => {
|
||||
const previousHandler = editor.editorOptions.onValueChanged
|
||||
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
|
||||
|
||||
editor.editorOptions.buttons = editor.editorOptions.buttons.map((btn: any) => {
|
||||
if (typeof btn?.options?.onClick !== 'function') return btn
|
||||
|
||||
const originalClick = btn.options.onClick
|
||||
btn.options.onClick = (e: any) => {
|
||||
const grid = editor.component
|
||||
const { rowKey, rowIndex } = getEditingRowIndex(grid)
|
||||
const formData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data || {}
|
||||
|
||||
originalClick({ ...e, formData, fieldName: editor.dataField, rowKey, rowIndex })
|
||||
}
|
||||
return btn
|
||||
editor.editorOptions.buttons = withButtonContext(editor.editorOptions.buttons, () => {
|
||||
const grid = editor.component
|
||||
const { rowKey, rowIndex } = getEditingRowIndex(grid)
|
||||
const formData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data || {}
|
||||
return { formData, fieldName: editor.dataField, mode, rowKey, rowIndex }
|
||||
})
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
export const getImageSource = (value: unknown) => {
|
||||
const getImageSource = (value: unknown) => {
|
||||
if (!value) return ''
|
||||
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)
|
||||
|
||||
/** 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()
|
||||
if (!trimmed) return ''
|
||||
if (trimmed.startsWith('data:image/') || !isProbablyBase64Image(trimmed)) return trimmed
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { GridDto } from '@/proxy/form/models'
|
|||
import { workflowService } from '@/services/workflow.service'
|
||||
import { getPersistedInsertedKey } from './editingForm'
|
||||
|
||||
export const showWorkflowMessages = (messages: string[] = []) => {
|
||||
const showWorkflowMessages = (messages: string[] = []) => {
|
||||
if (!messages.length) return
|
||||
|
||||
toast.push(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from '@/proxy/form/models'
|
||||
import { addCss, getEditingFormFields, safeJsonParse } from './Utils'
|
||||
import { clearLookupCache, clearLookupCacheByPrefix } from '../shared/lookupCache'
|
||||
import { createCascadeMaps } from './shared/cascade'
|
||||
import { useLookupDataSource } from '../form/useLookupDataSource'
|
||||
import {
|
||||
NO_IMAGE,
|
||||
|
|
@ -593,6 +594,7 @@ const useListFormColumns = ({
|
|||
return columns
|
||||
}
|
||||
|
||||
const { clearFieldsMap: cascadeClearFieldsMap } = createCascadeMaps(gridDto)
|
||||
const workflowApprovalStatusField =
|
||||
gridDto.gridOptions?.workflowDto?.approvalStatusFieldName?.toLowerCase()
|
||||
columnFormats.forEach((colData) => {
|
||||
|
|
@ -748,15 +750,15 @@ const useListFormColumns = ({
|
|||
dataSource: (o) => lookupDataSource(o, colData),
|
||||
}
|
||||
|
||||
//cascadeEmptyFields verisi dolu ise bu kolon/field bir parent field dir
|
||||
if (colData.lookupDto.cascadeEmptyFields) {
|
||||
// parent field guncellendigi zaman bu fonksiyon cagrilir
|
||||
// Değeri değişince boşaltılacak alanlar (çocuklar + CascadeEmptyFields); popup ve
|
||||
// satır içi düzenleme form sayfasıyla aynı kuralı izler.
|
||||
const cascadeClearFields = cascadeClearFieldsMap.get(colData.fieldName)
|
||||
if (cascadeClearFields?.length) {
|
||||
column.setCellValue = function (rowData: any, value: any) {
|
||||
if (!colData.fieldName) return
|
||||
//console.log({ rowData, value, colData })
|
||||
rowData[colData.fieldName] = Array.isArray(value) ? value[0] : value
|
||||
// cascadeEmptyFields alani aralarinda virgul olacak sekilde bosaltilmak istenen alanlari saklar
|
||||
colData?.lookupDto?.cascadeEmptyFields?.split(',').forEach((emptyField: any) => {
|
||||
rowData[colData.fieldName] =
|
||||
Array.isArray(value) && !column.extras?.multiValue ? value[0] : value
|
||||
cascadeClearFields.forEach((emptyField) => {
|
||||
rowData[emptyField] = null
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ export function IconPickerField({ value, onChange, invalid }: IconPickerFieldPro
|
|||
|
||||
// ─── MenuAddDialog ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface MenuAddDialogProps {
|
||||
interface MenuAddDialogProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
initialParentCode: string
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ type SharedGridBoxEditorProps = {
|
|||
editorOptions?: any
|
||||
dropDownOptions?: any
|
||||
onValueChanged?: (value: any) => void
|
||||
onCommit?: (value: any) => void
|
||||
setInitialValueOnContentReady?: boolean
|
||||
}
|
||||
|
||||
const normalizeValue = (value: any) => (Array.isArray(value) ? value : [value])
|
||||
|
|
@ -46,18 +44,11 @@ const SharedGridBoxEditor = ({
|
|||
editorOptions,
|
||||
dropDownOptions,
|
||||
onValueChanged,
|
||||
onCommit,
|
||||
setInitialValueOnContentReady,
|
||||
}: SharedGridBoxEditorProps): ReactElement => {
|
||||
const gridRef = useRef<DataGridRef>(null)
|
||||
const currentValueRef = useRef<any>(normalizeValue(value))
|
||||
currentValueRef.current = normalizeValue(value)
|
||||
|
||||
const commitValue = (nextValue: any) => {
|
||||
currentValueRef.current = nextValue
|
||||
onCommit?.(nextValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropDownBox
|
||||
{...editorOptions}
|
||||
|
|
@ -69,9 +60,7 @@ const SharedGridBoxEditor = ({
|
|||
showClearButton={options?.showClearButton}
|
||||
acceptCustomValue={options?.acceptCustomValue}
|
||||
onContentReady={(e) => {
|
||||
if (setInitialValueOnContentReady) {
|
||||
e.component.option('value', currentValueRef.current)
|
||||
}
|
||||
e.component.option('value', currentValueRef.current)
|
||||
|
||||
editorOptions?.onContentReady?.(e)
|
||||
}}
|
||||
|
|
@ -81,7 +70,6 @@ const SharedGridBoxEditor = ({
|
|||
onValueChanged?.(nextValue)
|
||||
|
||||
if (e.value === null) {
|
||||
commitValue(null)
|
||||
e.component.close()
|
||||
gridRef.current?.instance().deselectAll()
|
||||
}
|
||||
|
|
@ -106,7 +94,6 @@ const SharedGridBoxEditor = ({
|
|||
if (options?.selectionMode === 'single') {
|
||||
const nextValue = event.data.key
|
||||
dropDown.component.option('value', nextValue)
|
||||
commitValue(nextValue)
|
||||
dropDown.component.close()
|
||||
}
|
||||
}}
|
||||
|
|
@ -138,7 +125,6 @@ const SharedGridBoxEditor = ({
|
|||
.map((item: any) => item.key)
|
||||
|
||||
dropDown.component.option('value', selectedValue)
|
||||
commitValue(selectedValue)
|
||||
dropDown.component.close()
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const headerValues = [false, 1, 2, 3, 4, 5, 6]
|
|||
|
||||
type ToolbarItem = NonNullable<NonNullable<HtmlEditorProperties['toolbar']>['items']>[number]
|
||||
|
||||
export const htmlToolbarItemNames = [
|
||||
const htmlToolbarItemNames = [
|
||||
'undo',
|
||||
'redo',
|
||||
'separator',
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
export const readableTextColor = (color: string): string => {
|
||||
const readableTextColor = (color: string): string => {
|
||||
const hex = color.replace('#', '')
|
||||
const full =
|
||||
hex.length === 3
|
||||
|
|
|
|||
Loading…
Reference in a new issue